using linq to merge items together in a collection - c#

I have a list of objects (call them type salesItems) Lets say these items have 50 properties, with Name, price, quantity being 3 of them). I would like to know how to merge the list together combining any salesItems by name using the following logic:
If there are multiple salesOrders that have the same Name:
Combine them into one SalesOrder with the same Name
Set the quantity to the sum of the quantities
Set the price, and all of the other properties using the values of the first
I would like to do with linq. I realize i could use a big for each c# loop instead.
If there are additional items in the list I would like to follow similar logic for those as well.
EX: A salesOrder list with (A,B,C,D,E)
A: Name=ball Price= 2.24 Quantity=1 (other values = bla bla)
B: Name= ball Price = 15.33 Quantity=3 (other values)
c: Name= bat Price = 22.14 Quantity=3 (other values)
D: Name= bat Price= 19.22 Quantity=2 (other values)
E: Name = ball Price=4.32 Quantity=2 (other values)
Result list I want 2 Sales orders in list (A,C) A: Name=ball Price=
2.24 Quantity=6 (other values = bla bla from a's properties) c: Name= bat Price = 22.14 Quantity=5 (other values from c's
properties)

You want linq's .GroupBy method!!!
I've defined your class as:
public class SalesOrder
{
public string Name { get; set; }
public double Price { get; set; }
public int Quantity { get; set; }
public SalesOrder(string Name, double Price, int Quantity)
{
this.Name = Name;
this.Price = Price;
this.Quantity = Quantity;
}
}
then I have created a list of your orders like this:
List<SalesOrder> Orders = new List<SalesOrder>()
{
new SalesOrder("Ball", 2.24, 1),
new SalesOrder("Ball", 15.33, 3),
new SalesOrder("Bat", 22.14, 3),
new SalesOrder("Bat", 19.22, 2),
new SalesOrder("Ball", 4.32, 2)
};
and grouped them by the name before selecting the values you want for each group into a new instance of the SalesOrder class like this:
List<SalesOrder> Combined_Orders = Orders
.GroupBy (o => o.Name)
.Select (o => new SalesOrder(o.Key, o.Select (x => x.Price).First(), o.Sum(x => x.Quantity)))
.ToList();
UPDATE: In response to OP's comment
As the real SalesOrder will have hundreds of properties, you can avoid typing them all out in the linq query by adding a constructor to the SalesOrder class that accepts the result of the group by as an argument, then do all the work in the constructor. While it doesn't stop you from having to type out all the properties, it does mean that its neatly abstracted away. Also this way it forces/enables you to decide on what to do with each of the properties (first/sum/average).
To do this you will need a second constructor that looks like this:
public SalesOrder(IGrouping<string, SalesOrder> Group)
{
this.Name = Group.Key;
this.Price = Group.First().Price;
this.Quantity = Group.Sum(g => g.Quantity);
// do all other properties here too
}
Then update the group by to look like this (note that only the result of the grouping "g" is passed into the constructor now):
List<SalesOrder> Combined_Orders = Orders
.GroupBy (o => o.Name)
.Select (g => new SalesOrder(g))
.ToList();

Hi You can use the following code,
class SalesItem
{
public string Name { get; set; }
public int Price { get; set; }
public int Quantity { get; set; }
}
class SalesOrder
{
public void LoadItems()
{
List<SalesItem> SalesItems = new List<SalesItem>();
SalesItem salesitem = new SalesItem()
{
Name = "Ball",
Price = 12,
Quantity = 1
};
SalesItems.Add(salesitem);
salesitem = new SalesItem()
{
Name = "Ball",
Price = 36,
Quantity = 3
};
SalesItems.Add(salesitem);
salesitem = new SalesItem()
{
Name = "Bat",
Price = 50,
Quantity = 1
};
SalesItems.Add(salesitem);
salesitem = new SalesItem()
{
Name = "Ball",
Price = 84,
Quantity = 7
};
SalesItems.Add(salesitem);
salesitem = new SalesItem()
{
Name = "Bat",
Price = 150,
Quantity = 3
};
SalesItems.Add(salesitem);
GroupOrders(SalesItems);
}
public List<SalesItem> GroupOrders(List<SalesItem> SalesItems)
{
var list = from item in SalesItems
group item by item.Name into orders
select new SalesItem
{
Name = orders.Key,
Price = orders.Sum(X=>X.Price),
Quantity = orders.Sum(X=>X.Quantity)
};
List<SalesItem> resultList = new List<SalesItem>();
foreach (SalesItem saleitem in list)
{
resultList.Add(saleitem);
}
return resultList;
}
}

Related

Split List to Categories using Linq

I have class that is used to get and send data to database. Data example:
Id
Amount
ProductName
RecordDateTime
1
2
Fresh Apples
23/2/2021
2
3
Sweet Bananas
13/6/2021
3
1
Yellow Bananas
12/7/2021
4
7
Green Apples
31/5/2021
5
9
Juicy Apples
12/9/2021
6
4
Young Potato's
5/2/2021
7
5
Orange Carrots
4/6/2021
Class:
public class LogModel
{
public int Id { get; set; }
public int Amount { get; set; }
public string ProductName { get; set; }
public DateTime RecordDateTime { get; set; }
}
Then I have another class that is used for user customization so that the user can build their own category structure. This is treeview with user-built structure like:
- Categories
- Fruits
- Apples | Bananas
- Vegetables
- Potato's | Carrots
Here is the class:
public class CategoryModel : BaseViewModel
{
private ObservableCollection<CategoryModel> categoryItems;
public ObservableCollection<CategoryModel> CategoryItems
{
get => this.categoryItems;
set
{
this.categoryItems = value;
this.OnPropertyChanged();
}
}
private string itemName;
public string ItemName
{
get => this.itemName;
set
{
this.itemName = value;
this.OnPropertyChanged();
}
}
}
I need to create a new list that will assign each record from the database to own category based on string.Contains after splitting by |. I have managed to get method with Linq to sort only needed data to work with from BD. How to do actual assign of data to each category? No need to match my structure. Basic linq example would be enough.
public List<LogModel> GetCategorySplit(DateTime startDate, DateTime endDate)
{
using (var db = new SQLDBContext.SQLDBContext())
{
List<LogModel> result = db.LogModel
.Where(w => w.RecordDateTime >= startDate.Date && w.RecordDateTime.Date <= endDate.Date && w.Amount != 0)
.Select(
s => new LogModel
{
ProductName = s.ProductName,
Amount = s.Amount,
})
.ToList();
// I have tried some foreach loop after that, but it is not so clear in my head how this process
// should go overall
//foreach (var item in result)
//{
// if (ExtensionMethods.StringContains(item.ProductName, value))
// {
// }
//}
return result;
}
}
Expected Output:
Fruits 22
Vegetables 9
P.S. Please comment if something is unclear, I am quite new here.
In order to be able to test my solution I've used in-memory data structures.
var dataSource = new List<LogModel>
{
new LogModel { Id = 1, Amount = 2, ProductName = "Fresh Apples", RecordDateTime = new DateTime(2021, 02, 23)},
new LogModel { Id = 2, Amount = 3, ProductName = "Sweet Bananas", RecordDateTime = new DateTime(2021, 06, 13)},
new LogModel { Id = 3, Amount = 1, ProductName = "Yellow Bananas", RecordDateTime = new DateTime(2021, 07, 12)},
new LogModel { Id = 4, Amount = 7, ProductName = "Green Apples", RecordDateTime = new DateTime(2021, 05, 31)},
new LogModel { Id = 5, Amount = 9, ProductName = "Juicy Apples", RecordDateTime = new DateTime(2021, 09, 12)},
new LogModel { Id = 6, Amount = 4, ProductName = "Young Potato's", RecordDateTime = new DateTime(2021, 02, 05)},
new LogModel { Id = 7, Amount = 5, ProductName = "Orange Carrots", RecordDateTime = new DateTime(2021, 06, 04)}
};
var categories = new List<CategoryModel>
{
new CategoryModel
{
ItemName = "Fruits",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Apples"},
new CategoryModel {ItemName = "Bananas"}
}
},
new CategoryModel
{
ItemName = "Vegetables",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Potato's"},
new CategoryModel {ItemName = "Carrots"}
}
}
};
The grouping logic can be written like this:
var topLevelQuantities = new Dictionary<string, int>();
foreach (var topLevelCategory in categories)
{
var filters = topLevelCategory.CategoryItems.Select(leafLevel => leafLevel.ItemName);
var count = dataSource.Where(log => filters.Any(filter => log.ProductName.Contains(filter)))
.Sum(log => log.Amount);
topLevelQuantities.Add(topLevelCategory.ItemName, count);
}
Here I iterate through the top level categories foreach (var topLevelCategory in categories)
I assumed the the graph depth is 2 (so there are top level and leaf level entities)
Then I gather the leafs' ItemNames into the filters
I perform a filtering based on the filters dataSource.Where( ... filters.Any(...))
And finally I calculate the Sum of the filtered LogModels' Amount
In order to be able to pass the accumulated data to another function/layer/whatever I've used a Dictionary<string, int>
I've used the following command to examine the result:
foreach (var (categoryName, categoryQuantity) in topLevelQuantities.Select(item => (item.Key, item.Value)))
{
Console.WriteLine($"{categoryName}: {categoryQuantity}");
}
NOTES
Please bear in mind that this solution was designed against in-memory data. So, after data has been fetched from the database. (If you want to perform this on the database level then that requires another type of solution.)
Please also bear in mind that this solution requires multiple iterations over the dataSource. So, if the dataSource is very large (or there are lots of top level categories) than the solution might not perform well.
UPDATE: When the depth of the category hierarchy is 3.
If we can assume that there can be only 1 top-level entity then we should not need to change too much:
var rootCategory = new CategoryModel
{
ItemName = "Categories",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel
{
ItemName = "Fruits",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Apples"},
new CategoryModel {ItemName = "Bananas"}
}
},
new CategoryModel
{
ItemName = "Vegetables",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Potato's"},
new CategoryModel {ItemName = "Carrots"}
}
}
}
};
var midLevelQuantities = new Dictionary<string, int>();
foreach (var midLevelCategory in rootCategory.CategoryItems)
{
...
}
If there can be multiple top level categories:
var categories = new List<CategoryModel>
{
new CategoryModel
{
ItemName = "Categories",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel
{
ItemName = "Fruits",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Apples"},
new CategoryModel {ItemName = "Bananas"}
}
},
new CategoryModel
{
ItemName = "Vegetables",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Potato's"},
new CategoryModel {ItemName = "Carrots"}
}
}
}
}
};
then we need to use recursive graph traversal.
I've introduced the following helper class to store the calculations' result:
public class Report
{
public string Name { get; }
public string Parent { get; }
public double Quantity { get; }
public Report(string name, string parent, double quantity)
{
Name = name;
Parent = parent;
Quantity = quantity;
}
}
The traversal can be implemented like this:
private static List<Report> GetReports(CategoryModel category, string parent, List<Report> summary)
{
if (category.CategoryItems == null || category.CategoryItems.Count == 0)
{
var count = dataSource.Where(log => log.ProductName.Contains(category.ItemName)).Sum(log => log.Amount);
summary.Add(new Report(category.ItemName, parent,count));
return summary;
}
foreach (var subCategory in category.CategoryItems)
{
summary = GetReports(subCategory, category.ItemName, summary);
}
var subTotal = summary.Where(s => s.Parent == category.ItemName).Sum(s => s.Quantity);
summary.Add(new Report(category.ItemName, parent, subTotal));
return summary;
}
In the if block we handle that case when we are at the leaf level
That's where we perform the queries
In the foreach block we iterate through all of its children and we are calling the same function recursively
After the foreach loop we calculate the current category's subTotal by aggregating its children's Quantities
Please note: This design assumes that the category names are unique.
To display the results we need yet another recursive function:
private static void DisplayResult(int depth, IEnumerable<CategoryModel> categories, List<Report> report)
{
foreach (var category in categories)
{
var indentation = new string('\t', depth);
var data =report.Single(r => r.Name == category.ItemName);
Console.WriteLine($"{indentation}{data.Name}: {data.Quantity}");
if (category.CategoryItems == null || category.CategoryItems.Count == 0)
continue;
DisplayResult((byte)(depth +1), category.CategoryItems, report);
}
}
Based on the category's level (depth) we calculate the indentation
We lookup the corresponding report based on the ItemName property of the category
We print out the report
If the current category does not have subcategories then we move to the next item
Otherwise we call the same function for the subcategories recursively
Let's put all things together
var report = new List<Report>();
foreach (var category in categories)
{
var subReport = GetReports(category, "-", new List<Report>());
report.AddRange(subReport);
}
DisplayResult(0, categories, report);
The output will be:
Categories: 31
Fruits: 22
Apples: 18
Bananas: 4
Vegetables: 9
Potato's: 4
Carrots: 5
So you have a displayed sequence of CategoryModels, with ItemNames like "Fruits", "Vegetables", etc.
Every CategoryModel has some sub-CategoryModels in property CategoryItems. In your example, CategoryModel "Fruits" has CategoryItems with ItemNames like "Apples" and "Bananas". "Vegetables has sub category ItemNames like "Potato's" and "Carrots".
You forgot to tell us, can a CategoryItem with ItemName "Apples" also have sub-categories, so you have sub-sub-categories? And can these sub-sub-categories have more sub-sub-sub-categories?
I need to create a new list that will assign each LogModel from the database to a CategoryModel.
How do you decide which LogModels belongs to "Apples". Is it if LogModel.ProductName contains "Apples"?
Apparently you need an extend class LogModel with a method that says: "Yes, a Juicy Apple is an Apple", or "No, a Tasty Pomodoro is not an Apple", neither is a "Granny Smith"
By the way: do you see the flaw in your requirement: why is a "Granny Smith" not an apple, like a "Juicy Apple"?
But let's design for change: if you later want to change how you match a LogModel with a CategoryModel, changes will be small.
Luckily, you already fetched the LogModels that you want to process from the database, so we can work "AsEnumerable", instead of "AsQueryable". This gives us more freedom in the functions that we can use.
public static bool IsMatch(LogModel log, CateGoryModel category)
{
// TODO: compare log with category, and decide whether it is a match
}
Usage will be as if IsMatch is a method of LogModel:
LogModel log = ...
CategoryModel category = ...
bool isMatch = IsMatch(log, category);
Your current requirement seems to be that a log matches a category, if the ProductName of the log contains the ItemName of the category. So if "Juicy Apples" contains "Apples" (not sure if you want case sensitivity)
public static bool IsMatch(LogModel log, CateGoryModel category)
{
// TODO: decide what to do if log == null, category == null.
// TODO: decide what to do if log.ProductName == null or category.ItemName == null
return log.ProductName.Contains(category.ItemName, StringComparison.CurrentCulture);
}
If later you decide that you don't check on names, but for instance on a FruitType, than changes will only have to be in this method, nowhere else.
Why is fruits 22, and vegetables 9?
Well, fruits has apples and bananas, and the sum of all matching apples (according to the method defined above) and all matching bananas is 22. Similarly: vegetables is potato's and carrots and the sum of all matching potato's (4) and matching carrots (5) is 9
So for every Category, we take all SubCategories, and find the LogModels that match. We sum all Amounts of the matching LogModels.
So as a matter of fact, you would like to extend class CategoryModel with a method that takes all fetched LogModels and returns the Amount of items. Something like this:
class CategoryModel
{
...
public int GetAmount(IEnumerable<LogModel> logModels)
{
...
}
}
If you don't want or if you cannot add this method to CategoryModel, you can always create an extension method. For those who are not familiar with extension methods, see Extension Methods Demystified
public static int GetAmount(this CategoryModel category, IEnumerable<LogModel> logModels)
{
// TODO: exception if items null or empty
int amount = logModels.Where(log => IsMatch(log, category)
.Select(log => log.Amount)
.Sum();
// Add all amounts of the subCategories:
if (category.CategoryItems != null && category.CategoryItems.Count != 0)
{
amount += category.CategoryItems
.Select(catgoryItem => categoryItem.GetAmounts(logModels))
.Sum();
}
return amount;
}
Nota bene: this method uses recursion, so it works even if you have sub-sub-sub-... categories.
Usage:
var fetchedLogModels = db.LogModels.Where(...).Select(...).ToList();
IEnumerable<CategoryModel> categories = ...
categoryies are "fruits" and "vegetables" etc.
var result = categories.Select(category => new
{
CategoryName = category.ItemName,
Amount = category.GetAmounts(fetchedLogModels),
});
Well, doesn't that seem like some nice piece of code? Every difficulty is hidden somewhere deep inside and the code is easily changeable and unit testable. If you want to change how you make your Matches, or want to change that GetAmounts is not recursive anymore, or that it becomes a method of CategoryModels: none of the users have to change.

C#: find objects within array with same id and sum their quantity values. Add those to the new object

I have an array or list of objects returned from the Database. Let's take this as an example:
this is the class:
public class products
{
public string name { get; set; }
public int quantity { get; set; }
public long id{ get; set; }
}
List<Product> prod = new List<Product>()
prod = (call to the DB to get data back...)
array is returned with object of products
What I need is to loop through this array and add up the quantities for the same ids. Meaning, add up 2+7 for id 3 and add that data to another object so that new object would have something like: total: 9, id: 3
then same again for id 5, total: 7, id: 5
and so on.
I am at a loss of the right way to do this. I was looking at Linq but the only way I used it is by providing specific values. Please help me with this
`
foreach (var p in prod){ Now what do i do?}
`
The easiest way is with GroupBy and Sum (Both use System.Linq):
List<Product> products = new List<Product>()
{
new Product(){Id = 1, Cost = 20.0M },
new Product(){Id = 1, Cost = 30.0M },
new Product(){Id = 2, Cost = 20.0M },
new Product(){Id = 3, Cost = 20.0M },
new Product(){Id = 3, Cost = 5.0M }
};
products.GroupBy(g => g.Id).ToList().ForEach(e =>
{
Console.WriteLine($"Total: {
e.Sum(s => s.Cost)} for {e.Key}");
});
Edit
With the new information provided: You can do this to a concrete class:
public class Product
{
public string name { get; set; }
public int quantity { get; set; }
public long id{ get; set; }
}
List<Product> products = new List<Product>();
var products = (call to the DB to get data back...).GroupBy(g => g.Id).ToList().ForEach(e =>
{
products.Add(new Product()
{
Id = e.Key,
Quantity = e.Sum(s => s.Quantity)
})
});
Per your code snippet, prod is single product of type products.
So, assuming your code which invokes database call returns prod list something like below:
List<products> prod = new List<products>();
prod = _productRepository.GetProductData(prodId);
You can use linq GroupBy (please include System.Linq to use GroupBy linq extension method) to find the total quantity of each product like below:
var anonymousList = prod.GroupBy(p => p.id).Select(g => new {id = g.Key, totalQuantity = g.Sum(p => p.quantity)}).ToList()
The above returns anonymous list of objects where each object contains id and totalQuantity.
If you are interested in dictionary of product id vs totalQuantity, then use something like below:
Dictionary<long, int> dictionary = prod.GroupBy(p => p.id).ToDictionary(k => k.Key, v => v.Sum(p => p.quantity));
UPDATE based on comments discussion:
You can invoke GroupBy on prod without verifying the count. No exception will be thrown.

Check the sum of a property of list items against another property

I have a list of items on an order, along with the order totals. I'm trying to find a way to add up all of the quantities that have shipped and compare that against the total order quantity, to see if there are any "backordered".
I get a list of PartInfo back, which includes all of the shipments of this product for an order.
public class PartInfo
{
public int OrderId { get; set; }
public string Name { get; set; }
public string TrackingId { get; set; }
public int ShippedQty { get; set; }
public int OrderTotal { get; set; }
}
If I use the following data:
List<PartInfo> PartList = new List<PartInfo>();
PartList.Add(new PartInfo() { OrderId = "1031",
Name = "Watch",
TrackingId = "1Z204E380338943508",
ShippedQty = 1,
OrderTotal = 4});
PartList.Add(new PartInfo() { OrderId = "1031",
Name = "Watch",
TrackingId = "1Z51062E6893884735",
ShippedQty = 2,
OrderTotal = 4});
How can I use LINQ to compare the total ShippedQty to the OrderTotal?
A direct answer could be something like this:
var backOrdered = partList.GroupBy(p => new { p.OrderId, p.OrderTotal })
.Select(g => new
{
g.Key.OrderId,
g.Key.OrderTotal,
TotalShipped = g.Sum(pi => pi.ShippedQty)
})
.Where(x => x.TotalShipped < x.OrderTotal);
Assuming that OrderId and OrderTotal are always linked, so you can group by them and always have one group per OrderId.
But as I said in a comment, if the data comes from a database there may be better ways to get the data, esp. when there is an Order with a collection navigation property containing PartInfos.
My reading is that the ShippedQty is for the single item (Name) for the order, so you want to group the items by the OrderId and count the quantity shipped. In that case, you can use group by LINQ:
var groupResults = PartList.GroupBy(
// Group by OrderId
x => x.OrderId,
// Select collection of PartInfo based on the OrderId
x => x,
// For each group based on the OrderId, create a new anonymous type
// and sum the total number of items shipped.
(x,y) => new {
OrderId = x,
ShippedTotal = y.Sum(z => z.ShippedQty),
OrderTotal = y.First().OrderTotal
});
For the sample data, this gives a single result, an anonymous type with three int properties (from C# interactive console):
f__AnonymousType0#3<int, int, int>> { { OrderId = 1031, ShippedTotal = 3, OrderTotal = 4 } }
You can then filter out the results to see where the order quantity is less than the order total
groupResults.Where(x => x.ShippedTotal < x.OrderTotal) ...

Grouping Deeply Nested Objects Using LINQ

I have the following classes:
public class Item
{
public int Id { get; set; }
public string Sku { get; set; }
public List<Price> Prices { get; set; }
}
public class Price
{
public int Id { get; set; }
public double Cost { get; set; }
public PriceList List { get; set; }
}
public class PriceList
{
public int Id { get; set; }
public string FileName { get; set; }
public DateTime ImportDateTime { get; set; }
}
Essentially, it's for a list of items [Item] in which each item has a list of prices [List<Price> Prices] (one-to-many), and each price has one price list [PriceList List] (one-to-one).
What I need is a list of all price lists.
It seems like what I need is a group to a "grandchild", basically, grouping by the PriceList's Id (which is under Price, which in turn is under Item in the list).
So, as an example, if I have five price lists, it should return five rows.
I achieved it doing the following long way:
List<PriceList> priceLists = new List<PriceList>();
foreach (Item item in items)
{
foreach (Price price in item.Prices)
{
PriceList list = price.List;
if (!priceLists.Any(x => x.Id == list.Id))
{
priceLists.Add(list);
}
}
}
How can it be achieved using LINQ?
UPDATE:
Here's a basic sample set:
PriceList priceList1 = new PriceList { Id = 1, FileName = "Price List 1.csv" };
PriceList priceList2 = new PriceList { Id = 2, FileName = "Price List 2.csv" };
Price price1 = new Price { Id = 1, Cost = 2.65, List = priceList1 };
Price price2 = new Price { Id = 2, Cost = 14.23, List = priceList2 };
Price price3 = new Price { Id = 3, Cost = 29.01, List = priceList1 };
Price price4 = new Price { Id = 4, Cost = 1, List = priceList2 };
Price price5 = new Price { Id = 5, Cost = 56.12, List = priceList1 };
Item item1 = new Item { Id = 1, Sku = "item1", Prices = new List<Price> { price1, price2 } };
Item item2 = new Item { Id = 2, Sku = "item2", Prices = new List<Price> { price3, price4 } };
Item item3 = new Item { Id = 3, Sku = "item3", Prices = new List<Price> { price5 } };
List<Item> itemsList = new List<Item> { item1, item2, item3 };
Let's start from the bottom upwards:
I have a list of Item (itemsList), which contains three Items.
Each Item has a list of Price.
Each Price has one PriceList.
To clarify the reasoning behind all this: The user imports a spreadsheet of prices (price list) they get periodically from their supplier, prices fluctuate and each price list has different prices for the same items. Hence the reason of having an item with many prices and each price has its price list which was used to upload it (there's more information included in the PriceList class which I omitted to keep it simple).
I hope I'm clear.
This seems to be what you want, using an extension to do Distinct by a lambda expression:
var ans = itemsList.SelectMany(item => item.Prices.Select(price => price.List)).DistinctBy(price => price.Id);
The extension is as follows:
public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> src, Func<T, TKey> keyFun) {
var seenKeys = new HashSet<TKey>();
foreach (T e in src)
if (seenKeys.Add(keyFun(e)))
yield return e;
}
If you changed your approach to use a HashSet<Int> to track the seen ids you would probably be a tiny bit more efficient than LINQ.
It looks like you can combine a SelectMany (to get all the Prices lists) with a Select (to get all the associated PriceList properties):
List<PriceList> priceLists = items.SelectMany(i => i.Prices).Select(p => p.List).ToList();
Edit
The above will return all the PriceLists, but since many Items could reference the same PriceList, then you need a way to filter on the PriceList.Id field.
One way to do that would be to override the Equals and GetHashCode property of the PriceList object. If you truly consider two PriceList objects to be equal if they have the same Id, then you could do the following:
public class PriceList
{
public int Id { get; set; }
public string FileName { get; set; }
public DateTime ImportDateTime { get; set; }
public override bool Equals(object obj)
{
var other = obj as PriceList;
return Id == other?.Id;
}
public override int GetHashCode()
{
return Id;
}
}
This allows you to use the Linq method, Distinct on your prices (it will call the Equals method to distinguish between PriceLists in the results:
List<PriceList> priceLists = items
.SelectMany(i => i.Prices)
.Select(i => i.List)
.Distinct()
.ToList();

How to query and sort a complex type consisting of IEnumerable<T> using LINQ?

Trying to figure out how to query an IEnumerable<T> using LINQ. The following simple example without IEnumerable works fine:
class Category
{
public string Title { get; set; }
public NameValue SubCategory { get; set; }
}
class NameValue
{
public string Name { get; set; }
public string Value { get; set; }
}
private static void testLinq()
{
Category[] categories = {
new Category { Title ="Abc", SubCategory = new NameValue { Name = "A", Value = "5"} },
new Category { Title ="Xyz", SubCategory = new NameValue { Name = "B", Value = "10" } }
};
IEnumerable<Category> q = categories.OrderBy(c => c.Title).ThenBy(c => c.SubCategory.Name);
foreach (Category c in q)
{
Console.WriteLine("{0} - {1}", c.Title, c.SubCategory.Name);
}
}
When I change the signature to have an IENumerable<NameValue> instead then I cannot access c.SubCategory.Name:
class Category
{
public string Title { get; set; }
public IEnumerable<NameValue> SubCategory { get; set; }
}
// For example, below does not compile:
IEnumerable<Category> q = categories.OrderBy(c => c.Title).ThenBy(c => c.SubCategory.Name);
// Also, this initialization of course won't work either
Category[] categories = {
new Category { Title ="Abc", SubCategory = new NameValue { Name = "A", Value = "5"} },
new Category { Title ="Xyz", SubCategory = new NameValue { Name = "B", Value = "10" } }
};
The error is:
IEnumerable' does not contain a definition for 'Name' and no extension method 'Name' accepting a first argument of type 'IEnumerable' could be found (are you missing a using directive or an assembly reference?)
Do I need to do a cast of some sort?
Update:
Output should be something like:
Abc (category)
A (sub)
B (sub)
C (...)
Xyz
B
K
M
Xyz2
A
Q
Z
In SQL I would do like something like this:
SELECT c.Title, s.Name, s.Value FROM Categories c
INNER JOIN SubCategory s ON
c.CategoryID = s.CategoryID
ORDER BY c.Title, s.Name -- sorting first on Category.Title, then on SubCategory.Name
Your SubCategory will be a collection so you cannot do it using ThenBy. You need to order the Category(s) and then order their SubCategory(s) like this. Note I added two SubCategory(s) to the first Category but their order is not correct. After we order them, then they will be correct:
Category[] categories = {
new Category { Title ="Abc", SubCategory = new List<NameValue>
{ new NameValue { Name = "B", Value = "5"},
new NameValue { Name = "A", Value = "5"} } },
new Category { Title ="Xyz", SubCategory = new List<NameValue>
{ new NameValue { Name = "A", Value = "10" } } }};
// First order by categories
var cats = categories.OrderBy(c => c.Title)
// Then create a new category and order by sub categories
.Select(x => new Category { Title = x.Title,
SubCategory = x.SubCategory.OrderBy(y => y.Name) });
If you can get away with only sorting the children when you need to use them, sorting by the parent and then sorting the children upon use like this would be fairly efficient:
public void DisplayA(A value)
{
Console.WriteLine(value.Name);
foreach (var child in value.Children.OrderBy(c => c.Name))
{
Console.WriteLine(string.Format("- {0}", child.Name));
}
}
Or if you want to avoid that, you could add a sorted property to the class. Since it's Linq, it will only be evaluated when you iterate through the list.
public class A
{
public string Name { get; set; }
public IEnumerable<B> Children { get; set; }
public IEnumerable<B> SortedChildren { get { return Children.OrderBy(ca => ca.Name); } }
}
public class B
{
public string Name { get; set; }
}
If they don't work for you, you could try these, but they won't be so efficient since you're creating new objects.
// This will flatten it into a single object, sorted by one field and the the other. Since this is Linq, it will create these new flattened objects each time you iterate through the IEnumerable.
public IEnumerable<FlattenedA> GetSortedFlattened(IEnumerable<A> collection)
{
var flattened = collection.SelectMany(a => a.Children.Select(ca => new FlattenedA() { Name = a.Name, SubName = ca.Name }));
var sorted = flattened.OrderBy(f => f.Name).ThenBy(f => f.SubName);
return sorted;
}
// This will return objects of A, where the child enumerable has been replaced with an OrderBy. Again this will return new objects each time you iterate through. Only when you iterate through the children will they be sorted.
public IEnumerable<A> GetSortedNonFlattened(IEnumerable<A> collection)
{
var withSortedChildren = collection.Select(a => new A() { Name = a.Name, Children = a.Children.OrderBy(ca => ca.Name) });
var sorted = withSortedChildren.OrderBy(a => a.Name);
return sorted;
}
public class FlattenedA
{
public string Name { get;set }
public string SubName { get; set; }
}
public class A
{
public string Name { get; set; }
public IEnumerable<B> Children { get; set; }
}
when you are setting it as IEnumerable you can't do this
SubCategory = new NameValue { Name = "A", Value = "5"}
you should use some implementation of IEnumerable,
like List<>
so it should be something like this
SubCategory = new List<NameValue>{new NameValue { Name = "A", Value = "5"}, addmore here};
and for your order linq, i would do this,
var OrderedCategories = categories.select(g =>
new Category{ Name = g.Name, subcategories = g.subcategories.orderby(h => h.Name) });
That's because your SubCategory now is no longer a simple single instance of NameValue, but rather an enumeration of those. So now you need to specify how to .ThenBy over a collection of .Names.

Categories

Resources