Order list by parent and child items - c#

I have a list of products that have to be ordered by parent then all children of that parent, then next parent, etc.
Product One
Child One
Child Two
Product Two
Child One
These products are all in one table with a parent id field, the child products have a parent id but the parent items can have a null parent (indicating that product is a top level product)
I was thinking something like the following:
var list = GetProductList();
var newList = new List<ProductDTO>();
var parents = from p in list
where p.Parent == null
select p.Id;
foreach (var parent in parents)
{
var tempList = new List<ProductDTO>();
tempList.Add(list.FirstOrDefault(x => x.Id == parent));
tempList.AddRange(list.Where(x => x.Parent == parent).OrderBy(x => x.Id));
newList.AddRange(tempList);
}
Any suggestions on how I would do this a little cleaner?

you could try something like that. Assuming parent is a nullable:
var sorted = list.OrderBy(x => x.parent ?? x.id).ThenBy(x=>x.id);
if its a string:
var sorted = list.OrderBy(x =>
{
if (x.parent == "null")
return x.id;
else
return Convert.ToInt32(x.parent);
}).ThenBy(x => x.id);

Given "Parent" is nullable property (assuming nullable int here). Following should give you parent-child related ordered list:
public class ProductDTO
{
public int Id { get; set; }
public string Name { get; set; }
public int? Parent { get; set; }
}
var products = new List<ProductDTO>();
products.Add(new ProductDTO() { Id = 1, Name = "Product One" });
products.Add(new ProductDTO() { Id = 2, Name = "Product Two" });
products.Add(new ProductDTO() { Id = 3, Name = "Product Three" });
products.Add(new ProductDTO() { Id = 4, Name = "Child One", Parent=1 });
products.Add(new ProductDTO() { Id = 5, Name = "Child Two", Parent = 2 });
products.Add(new ProductDTO() { Id = 6, Name = "Child One", Parent = 1 });
var ordered = products
.Where(p => p.Parent == null)
.OrderBy(p=> p.Id)
.Select(p => products
.Where(c => c.Parent == p.Id)
.OrderBy(c => c.Id))
.ToList();

You could do it this way:
list.ForEach(item =>
{
if (item.Parent == null)
{
orderedList.Add(item);
orderedList.AddRange(list.Where(child => child.Parent == item.Id));
}
});

I don't know if it's more cleaner but if you want a unique linq instruction you can try this :
var result = GetProductList().Where(p => p.Parent == null)
.SelectMany(p => list.Where(c => c.Parent == p.Id)
.Concat(new[] { p })
.OrderBy(c => c.Parent.HasValue)
.ThenBy(c => c.Id)
.ToList())
.ToList();

You should add a ParentId to the Product One and Product two, and the, will be easier to solve it.
If Parent One is 1, and Parent Two is 2, only do this
var result = parents.OrderBy(x => x.Parent).ThenBy(x => x.Id);

Maybe using linq this way:
var groupList = from c in products
where c.Parent.HasValue
group c by c.Parent into r
join p in products on r.Key equals p.Id
orderby p.Name
select new { Parent = p, Children = r };

It's very easy and complicated way, In 'res' variable you'll see this kind of situation - parent1 > child.1.1 > child.1.2 > parent2 > child.2.1 > child.2.2 > child.2.3 > parent3:
//items is a list of unsorted objects
var res = items.OrderBy(x =>
{
if (x.ParentId == null)
return x.Id;
else
return x.ParentId;
}).ThenBy(t => t.Id);

Related

Conditional statement inside WHERE clause

Consider the following records:
The outlined records are the same gameobject but they have different configuration names (the record where ConfigurationName is null is a copy from my original table).
string customName= "Config1";
string draftName = "Draft_" + id;
var list = db.JoinedGameObjects
.Where(x => x.TBUploads_ID == id)
.Where(x => x.ConfigurationName.Equals(customName) || x.ConfigurationName == null)
.Select(x => new GameObjectViewModel()
{
ID = x.ID,
GameObjectName = x.GameObjectName,
Category = x.Category,
Family = x.Family,
Type = x.Type,
MetaTypeJson = x.MetaTypeJson,
MaterialsJson = x.MaterialsJson,
MetadataVisibilityJson = x.MetadataVisibilityJson,
Visible = x.joined_Visible ?? x.Visible
}).ToList();
This code gets all records where ConfigurationName equals my customName variable or where ConfigurationName = null. However, this is not what I want.
I'll try to explain the desired outcome with an example.
I first want to check if there is a record with this ID with ConfigurationName equal to draftName (if true select this record), if not check if there is a record with this ID with ConfigurationName equal to customName (if true select this gameobject), if not take the record with this ID where ConfigurationName is null.
Can someone guide me in the right direction or provide a working example?
It seems like you already have a working concept, just add a draftName comparison to your Where condition.
It will apply conditions in order defined in Where function
.Where(x => x.ConfigurationName.Equals(draftName) || x.ConfigurationName.Equals(customName) || x.ConfigurationName == null)
Edit #1:
This should reflect what the OP wants.
For the sake of brevity, I used custom `Person` class and simplified the code example, so the concept should be easy to understand:
public class Person {
public int Id { get; set; }
public string Name { get; set; }
}
Then the rest:
var people = new List<Person>
{
new Person {Id= 1, Name = null},
new Person {Id= 1, Name = "Mike"},
new Person {Id= 1, Name = "John"},
new Person{Id = 2, Name= null},
new Person{Id = 2, Name= "Peter"},
};
//filter values
string filter1 = "Mike";
string filter2 = "John";
string filter3 = null;
var byPriority = people
.Where(p=> p.Name == filter1 || p.Name == filter2 || p.Name == filter3)
.OrderByDescending(p => p.Name == filter1)
.ThenByDescending(p => p.Name == filter2)
.ThenByDescending(p => p.Name == filter3);
var oneByIId = byPriority
.GroupBy(p => p.Id)
.Select(g => g.First())
.ToList();
The key here is to order the records by conditions priority. Then group the records and take the first one from each group
Edit #2
In some cases, LINQ to SQL may fail when comparing to `NULL`, in such a case, modify null comparing parts as this:
var byPriority = people
.Where(p=> p.Name == filter1 || p.Name == filter2 || object.Equals(p.Name,filter3))
.OrderByDescending(p => p.Name == filter1)
.ThenByDescending(p => p.Name == filter2)
.ThenByDescending(p => object.Equals(p.Name,filter3));
var oneByIId = byPriority
.GroupBy(p => p.Id)
.Select(g => g.First())
.ToList();

Linq query to return the cheapest product, only if it is unique

I have a list of Products, with the Price. I would like to get the the cheapest one only if it is unique. If there are more than one Product with the same lowest price, it should not return any.
In the sample below, for the uniqProductList the query should return the BestOne while for the dupProductList, no product should be returned.
How do I write the Linq query ?
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
public DateTime ExpiryDate { get; set; }
}
List<Product> uniqProductList = new List<Product>() {
new Product { Name = "GoodOne", Price = 12M },
new Product { Name = "NiceOne", Price = 12M },
new Product { Name = "ExpensiveOne", Price = 15M },
new Product { Name = "BestOne", Price = 9.99M }
};
List<Product> dupProductList = new List<Product>() {
new Product { Name = "GoodOne", Price = 12M },
new Product { Name = "NiceOne", Price = 12M },
new Product { Name = "ExpensiveOne", Price = 15M },
};
This is one way if you want to do it in a single query:
Product result = uniqProductList
.GroupBy(x => x.Price)
.OrderBy(x => x.Key)
.Take(1)
.FirstOrDefault(x => x.Count() == 1)?
.FirstOrDefault();
Group the results by price
Order by price so that the cheapest is the first result
Take the first result, since we aren't interested in the other ones
Return the grouping if there is only one result in the group
Return the value
There's almost certainly some other method that is faster, though.
You are looking for ArgMax which is not included into standard Linq but can be implemented manually with a help of Aggregate. Having a collection of the cheapest Products we can return null if we have more than 1 of them:
using System.Linq;
...
List<Product> source = ...
var bests = source
.Aggregate(new List<Product>(), (s, a) => {
if (s.Count <= 0 || s[0].Price == a.Price)
s.Add(a);
else if (a.Price <= s[0].Price) {
s.Clear();
s.Add(a);
}
return s;
});
Product best = bests.Count == 1 ? bests[1] : default(Product);
You could group by the elements by their price and getting the cheapest group:
var cheapestGrp = uniqProductList.GroupBy(i => i.Price).OrderBy(i => i.Key).First();
Then, based on the number of elements of the group, return the only element or return nothing:
if (cheapestGrp.Count() > 1)
return null;
else
return cheapestGrp.ToList().First();
result = Products.GroupBy(x => x.Price)
.Select(g => new { g.Name, Count = g.Count()
})
.Orderby(s.Count)
.Select(x.Name, x.Count).FirstOrDefault();
if(result.Count == 1){
return result;
}
else{
return null;
}
I would suggest this solution:
public Product? TryGetBestOne(IEnumerable<Product> products)
{
var bestProducts = products
.GroupBy(x => x.Price)
.OrderBy(x => x.Key)
.FirstOrDefault()?
.ToArray() ?? Array.Empty<Product>();
return bestProducts.Count() == 1 ? bestProducts.Single() : null;
}
You can use GroupBy and then use Where to get items where there is just one Count and then just sort in ascending order:
var result = uniqProductList
.GroupBy(u => u.Price)
.Select(grp => new { grp.Key, Count = grp.Count(), Items = grp.ToList() })
.Where(s => s.Count == 1)
.OrderBy(o=> o.Key)
.FirstOrDefault();
An example:
List<Product> uniqProductList = new List<Product>() {
new Product { Name = "GoodOne", Price = 12M },
new Product { Name = "NiceOne", Price = 12M },
new Product { Name = "ExpensiveOne", Price = 15M },
new Product { Name = "BestOne", Price = 9.99M }
};
List<Product> dupProductList = new List<Product>() {
new Product { Name = "GoodOne", Price = 12M },
new Product { Name = "NiceOne", Price = 12M },
new Product { Name = "ExpensiveOne", Price = 15M },
};
var result = uniqProductList
.GroupBy(u => u.Price)
.Select(grp => new { grp.Key, Count = grp.Count(), Items = grp.ToList() })
.Where(s => s.Count == 1)
.OrderBy(o=> o.Key)
.FirstOrDefault();
This is another solution :
var product = uniqProductList.OrderBy(a => a.Price)
.GroupBy(a => a.Price).FirstOrDefault()
.Aggregate(new List<Product>(), (result, item) =>
{
result.Add(item);
if (result.Count() > 1)
result = new List<Product>();
return result;
}).FirstOrDefault();
You can get the lowest price first, then you can group them.

Finding parents in a tree hierarchy for a given child LINQ (lambda expression)

I have data with parent child relationships, finding all children for a given parent is straight forward using Linq, however I have problem other way around, finding all of parents when child is provided.
List<FlatData> elements = new List<FlatData>
{
new FlatData {Id = 1, ParentId = NULL ,Text = "Apple"},
new FlatData {Id = 2, ParentId = 1, Text = "Cat"},
new FlatData {Id = 3, ParentId = 2, Text = "Dog"},
new FlatData {Id = 4, ParentId = 3, Text = "Elephant"}
};
When Id 4 is given I need to able to reverse traverse and find all parents using LINQ (lambda expression).
You can use recursion to do something like this:
private IEnumerable<FlatData> FindAllParents(List<FlatData> all_data, FlatData child)
{
var parent = all_data.FirstOrDefault(x => x.Id == child.ParentId);
if (parent == null)
return Enumerable.Empty<FlatData>();
return new[] {parent}.Concat(FindAllParents(all_data, parent));
}
And use it like this:
int id = 4;
var child = elements.First(x => x.Id == id);
var parents = FindAllParents(elements, child).ToList();
This solution works, but if you have a large data set, then you should consider using a Dictionary<int,FlatData> to make it it faster to fetch a FlatData object given it's Id.
Here is how the method would look like in this case:
private IEnumerable<FlatData> FindAllParents(Dictionary<int,FlatData> all_data, FlatData child)
{
if(!all_data.ContainsKey(child.ParentId))
return Enumerable.Empty<FlatData>();
var parent = all_data[child.ParentId];
return new[] {parent}.Concat(FindAllParents(all_data, parent));
}
And here is how you would use it:
var dictionary = elements.ToDictionary(x => x.Id); //You need to do this only once to convert the list into a Dictionary
int id = 4;
var child = elements.First(x => x.Id == id);
var parents = FindAllParents(dictionary, child).ToList();
This works:
var parents = elements.ToDictionary(x => x.Id, x => x.ParentId);
Func<int, IEnumerable<int?>> getParents = null;
getParents = i =>
parents.ContainsKey(i)
? new [] { parents[i] }.Concat(parents[i].HasValue
? getParents(parents[i].Value)
: Enumerable.Empty<int?>())
: Enumerable.Empty<int?>();
If I ask for getParents(4) then I get this result:
A slightly simplified version that removes the null parent is this:
var parents =
elements
.Where(x => x.ParentId != null)
.ToDictionary(x => x.Id, x => x.ParentId.Value);
Func<int, IEnumerable<int>> getParents = null;
getParents = i =>
parents.ContainsKey(i)
? new [] { parents[i] }.Concat(getParents(parents[i]))
: Enumerable.Empty<int>();

How to use local variables in a lambda expression

I have 2 list object of type of some class,
class person
{
public string id { get; set; }
public string name { get; set; }
}
List<person> pr = new List<person>();
pr.Add(new person { id = "2", name = "rezoan" });
pr.Add(new person { id = "5", name = "marman" });
pr.Add(new person { id = "3", name = "prithibi" });
List<person> tem = new List<person>();
tem.Add(new person { id = "1", name = "rezoan" });
tem.Add(new person { id = "2", name = "marman" });
tem.Add(new person { id = "1", name = "reja" });
tem.Add(new person { id = "3", name = "prithibi" });
tem.Add(new person { id = "3", name = "prithibi" });
Now i have to get all the ids from "pr" ListObject that has no entry or odd number of entries in the "tem" ListObejct. using lamda.
To do this i have used,
HashSet<string> inconsistantIDs = new HashSet<string>(pr.Select(p => p.id).Where(p => tem.FindAll(t => t.id == p).Count == 0 || tem.FindAll(t => t.id == p).Count % 2 != 0));
and it works fine.
but you can see from the code i have used tem.FindAll(t => t.id == p).Count twice to comapre with ==0 and %2!=0.
Is there any way to use tem.FindAll(t => t.id == p).Count once and
save it to a temporary variable and then compare this variable with
==0 and %2!=0.
More simply i just want to use it once for two condition here.
Use a statement lambda instead of an expression lambda
var inconsistantIDs = new HashSet<string>(
pr.Select(p => p.id).Where(p =>
{
var count = tem.FindAll(t => t.id == p).Count;
return count == 0 || count % 2 != 0;
}
));
Perhaps simply:
var query = pr.Where(p => { int c = tem.Count(p2 => p.id == p2.id); return c == 0 || c % 2 != 0; });
returns two persons:
2 "rezoan"
5 "marman"
Besides statement lambda you can use let clause:
HashSet<string> inconsistantIDs = new HashSet<string>(
from p in pr
let count = tem.FindAll(t => t.id == p).Count
where count == 0 || count % 2 != 0
select p.id
);
HashSet<string> inconsistantIDs = new HashSet<string>(
pr.Select(p => new { Id = p.id, Cnt = tem.FindAll(t => t.id == p.id).Count() })
.Where(p => p.Cnt == 0 || p.Cnt % 2 != 0)
.Select(p => p.Id);
On a side note, strictly performance wise, you would get better performance if you created a hash mapping of each ID to its count and then search it in a loop.
Right now you have a O(n*m) algorithm, which would be reduced to O(n+m):
// create a map (id -> count), O(m) operation
var dictionary = new Dictionary<string, int>();
foreach (var p in tem)
{
var counter = 0;
dictionary.TryGetValue(p.id, out counter);
counter++;
dictionary[p.id] = counter;
}
// search the map, O(n) operation
var results = new HashSet<string>();
foreach (var p in pr)
{
var counter = 0;
dictionary.TryGetValue(p.id, out counter);
if (counter == 0 || counter % 2 != 0)
results.Add(p.id);
}

Use List.ToLookup()

I have a List like the following:
var products = new List<Product>
{
new Product { Id = 1, Category = "Electronics", Value = 15.0 },
new Product { Id = 2, Category = "Groceries", Value = 40.0 },
new Product { Id = 3, Category = "Garden", Value = 210.3 },
new Product { Id = 4, Category = "Pets", Value = 2.1 },
new Product { Id = 5, Category = "Electronics", Value = 19.95 },
new Product { Id = 6, Category = "Pets", Value = 5.50 },
new Product { Id = 7, Category = "Electronics", Value = 250.0 },
};
I want to group by category and get the sum of 'Values' belonging to that category..
Example: Electronics: 284.95
While I can do this in some other way, I want to learn usage of Look-Up.
Is it possible to get these 2 values (category and Value) in a Look-Up? If yes, How can I do that?
When you retrieve by key from a Lookup, it behaves just like a grouping, so you can do things like this:
var productLookup = products.ToLookup(p => p.Category);
var electronicsTotalValue = productLookup["Electronics"].Sum(p => p.Value);
var petsTotalValue = productLookup["Pets"].Sum(p => p.Value);
//etc
var totalValue = products.Sum(p => p.Value);
// I wouldn't use the Lookup here, the line above makes more sense and would execute faster
var alsoTotalValue = productLookup.Sum(grp => grp.Sum(p => p.Value));
You probably want to use ToDictionary() instead of ToLookup
var dict = products
.GroupBy(p => p.Category)
.ToDictionary(grp => grp.Key, grp => grp.Sum(p => p.Value));
foreach(var item in dict)
{
Console.WriteLine("{0} = {1}", item.Key, item.Value);
}
You don't need a Lookup. You can do this with just a query:
var results =
from p in products
group p by p.Category into g
select new
{
Category = g.Key,
TotalValue = g.Sum(x => x.Value)
};
var rez = products.ToLookup(k => k.Category, v => v.Value).Select(k=>new KeyValuePair<string, double>(k.Key, k.Sum()));

Categories

Resources