GroupBy item and Sum Quantities of items - c#

I have an array of orders within each order is an array of items. How do I group all the orders by item name and get the sum total of items ordered. In this case output would be :
Output
Item01 : quantity = 2;
Item02 : quantity = 45;
GetOrders
public Order[] GetOrders()
{
Order[] orders = new Order[]
{
new Order
{
id = 1,
orderLines = new OrderLine[]
{
new OrderLine
{
itemName = "Item 01",
quantity = 1
},
new OrderLine
{
itemName = "Item 02",
quantity = 3
},
},
},
new Order
{
id = 2,
orderLines = new OrderLine[]
{
new OrderLine
{
itemName = "Item 01",
quantity = 1
},
new OrderLine
{
itemName = "Item 02",
quantity = 42
}
}
}
};
...
I tried the following:
foreach (var order in orders)
{
foreach (var orderline in order.orderLines.GroupBy(x => x.itemName).Select(group => new
{
Metric = group.Key,
Count = group.Count()
}))
{
Console.WriteLine("{0} {1}", orderline.Metric, orderline.Count);
}
}
but it just returns 1 for each item. I am relatively new to programming , so be easy on me.Thanks

To get the sum total of all items ordered, use the following query:
var results =
(from order in orders
from orderLine in order.orderLines
group orderLine by orderLine.itemName into orderLineGrouping
let totalQuantity = orderLineGrouping.Sum(ol => ol.quantity)
select new { itemName = orderLineGrouping.Key, metric = totalQuantity }).ToList();
results.ForEach(resultItem => Console.WriteLine("{0} {1}", resultItem.itemName, resultItem.metric);

Flatten orderlines
group the item name and quantities for output string
Select the output string using grouped key and sum of grouped quantities
See code:
var output = orders.SelectMany(x => x.orderLines)
.GroupBy(x => x.itemName, x => x.quantity)
.Select(x => $"{x.Key} : quantity = {x.Sum(y => y)}");

Flatten OrderLines
You need to familiarise your self with Enumerable.SelectMany its one of the most useful methods around
SelectMany, from LINQ, collapses many elements into a single
collection. The resulting collection is of another element type. We
specify how an element is transformed into a collection of other
elements.
var summary = orders.Where(x => x.OrderLines != null) // Check for null as there seems to be null orderlines in your model
.SelectMany(x => x.OrderLines) // Flatten
.GroupBy(x => x.itemName) // Group
.Select(group => new // Project
{
ItemName = group.Key,
TotalQuantity = group.Sum(x => x.quantity)
})
.ToList(); // To List
Tip : use appropriate casing for itemName and quantity
Capitalization Conventions
The following table summarizes the capitalization rules for
identifiers and provides examples for the different types of
identifiers.
Sorry my OCD just kicked in

Related

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.

LINQ multiple group by and then getting the first group by value count

I have a linq query like followin:
var _transactionsList = TransactionsData
.GroupBy(x => new { x.ItemID, x.Title, x.GalleryURL })
.Select(pr => new TransactionsTabResults
{
ItemID = pr.Key.ItemID,
Title = pr.Key.Title,
GalleryURL = pr.Key.GalleryURL,
ItemPrice = pr.OrderByDescending(a => a.TransactionDate).First().ItemPrice,
TotalSoldItems = pr.Count(),
TotalRevenuePerItem = pr.Sum(y => y.ItemPrice),
AveragePrice = pr.Average(y => y.ItemPrice),
}).ToList();
I'm trying to fetch the total sold items value by grouping it by like this:
ItemID Sales ItemName
1 1 Item1
1 3 Item1
1 5 Item1
1 6 Item1
2 2 Item2
2 2 Item2
2 2 Item2
2 2 Item2
The desired output would be:
ItemID Sales ItemName
1 15 Item1
2 8 Item2
The query above that I wrote gives me wrong values for total sales by saying:
TotalSoldItems = pr.Count(),
How can I count, or sum all the sales of one Item which has unique ID(this is what I'm grouping by)...
What am I doing wrong??
You are using GroubBy wrong way. You create new unique object every time. So your .GroupBy(x => new { x.ItemID, x.Title, x.GalleryURL }) and .Select(x => new { Key = new { x.ItemID, x.Title, x.GalleryURL}, Value =x }) means the same
If you need unique Id then group by Id only
TransactionsData
.GroupBy(x => x.ItemID)
.Select(pr => new TransactionsTabResults
{
ItemID = pr.Key,
Title = pr.First().Title,
GalleryURL = pr.First().GalleryURL,
ItemPrice = pr.OrderByDescending(a => a.TransactionDate).First().ItemPrice,
TotalSoldItems = pr.Count(),
TotalRevenuePerItem = pr.Sum(y => y.ItemPrice),
AveragePrice = pr.Average(y => y.ItemPrice),
}).ToList();
Advice
Optimize your LINQ. You are iterating through collections many times. This is suggested code:
TransactionsData
.GroupBy(x => x.ItemID)
.Select(pr =>
{
var items = x.pr.ToArray;
var sum = items.Sum(y => y.ItemPrice);
return new TransactionsTabResults
{
ItemID = pr.Key,
Title = items[0].Title,
GalleryURL = items[0].GalleryURL,
ItemPrice = pr.Aggregate((max, cur)=>max.TransactionDate<cur.TransactionDate?cur:max).ItemPrice,
TotalSoldItems = items.Length,
TotalRevenuePerItem = sum,
AveragePrice = sum/items.Length,
};
}).ToList();

How to group and merge/flatten a list of anonymous objects in LINQ

I have a list of anonymous objects generated by a LINQ query that I do not have access to modify.
The objects have the following properties:
OrderId, RepId, FirstName, LastName, Address
Each "Rep" often places multiple orders, so there are a lot of rows where the only difference is the OrderId. There is a requirement that if the same Rep has placed multiple orders, to batch these together in groups of 6 with a new structure:
OrderId1, OrderId2, ..., OrderId6, RepId, FirstName, LastName, Address
But if the rep has placed say 8 orders, there would be a batch of 6 and a batch of 2. So the new objects don't always have the same number of properties.
I've started by grouping the initial result set by RepId, but I have no clue where to go next.
Is this possible using LINQ?
As your output have anonymous objects with different schema, that make the thing a little more complicate.
Ideally you should design your entity class to use list for orders instead of property like "OrderId1", "OrderId2"... That is not extensible and error prone. But for that specific question, we can combine LINQ and ExpandoObject to achieve this.
orders.GroupBy(order => order.RepId)
.SelectMany(orderGroup => orderGroup.Select((order, i) => new {
Order = order,
ReqId = orderGroup.Key,
SubGroupId = i / 6
}))
.GroupBy(h => new {
ReqId = h.ReqId,
SubGroupId = h.SubGroupId,
FirstName = h.Order.FirstName,
LastName = h.Order.LastName,
Address = h.Order.Address
})
.Select(orderWithRichInfo => {
dynamic dynamicObject = new ExpandoObject();
int i = 1;
foreach(var o in orderWithRichInfo)
{
((IDictionary<string, object>)dynamicObject).Add("OrderId" + i, o.Order.OrderId);
i++;
}
((IDictionary<string, object>)dynamicObject).Add("FirstName", orderWithRichInfo.Key.FirstName);
((IDictionary<string, object>)dynamicObject).Add("LastName", orderWithRichInfo.Key.LastName);
((IDictionary<string, object>)dynamicObject).Add("Address", orderWithRichInfo.Key.Address);
return dynamicObject;
});
Hope it helps.
First option.
If you want to get 6 OrderId-s as a list, you can create
class OrderBundle
{
public int RepId { get; set; }
public List<int> OrderIds { get; set; }
}
Group your items:
var orderBundels = orderList
.GroupBy(m => m.RepId)
.Select(g => new OrderBundle
{
RepId = g.Key,
OrderIds = g.Select(m => m.OrderId).ToList()
});
And then split them into groups:
List<OrderBundle> dividedOrderBundels = new List<OrderBundle>();
foreach (OrderBundle orderBundle in orderBundels)
{
int bundelCount = (int)Math.Ceiling(orderBundle.OrderIds.Count() / 6.0);
for (int i = 0; i < bundelCount; i++)
{
OrderBundle divided = new OrderBundle
{
RepId = orderBundle.RepId,
OrderIds = orderBundle.OrderIds.Skip(i * 6).Take(6).ToList()
};
dividedOrderBundels.Add(divided);
}
}
Second option:
You can achieve the same result without creating model like below:
var result = orderList
.GroupBy(m => m.RepId)
.SelectMany(g => g.Select((m, i) => new
{
RepId = g.Key,
FirstName = m.FirstName,
LastName = m.LastName,
Address = m.Address,
OrderId = m.OrderId,
BunleIndex = i / 6
}))
.GroupBy(m => m.BunleIndex)
.Select(g => new
{
RepId = g.Select(m => m.RepId).First(),
FirstName = g.Select(m => m.FirstName).First(),
LastName = g.Select(m => m.LastName).First(),
Address = g.Select(m => m.Address).First(),
OrderIds = g.Select(m => m.OrderId).ToList()
})
.ToList()

Delete the repeated Item inside a List of object C#

I want to compare the element of a list of object ,delete the repeated Item and increment the number of the quantity of that Item (C# code ), I don't know if I should use LinQ,For or foreach statement : I have a list of OrderItem I want to delete the OrderItem that have the same FK_ArtikelId and increment the Qantity of the OrderItem . Exp:
for (int i=1 ; i < lot.Count ; i ++)
{
for (j = i + 1; j <= lot.Count; j++)
{
if (lot[i].FK_ArticleId.Equals(lot[j].FK_ArticleId))
{
lot[i].Quantity += lot[j].Quantity;
lot.Remove(lot[j]);
}
}
}
You have to use the GroupBy linq method and process the resulting groups: given the class
public class Article
{
public int FK_ArticleId { get; set; }
public int Quantity { get; set; }
}
and the following list:
var list = new List<Article>()
{
new Article() {FK_ArticleId = 1, Quantity = 10}
, new Article() {FK_ArticleId = 1, Quantity = 10}
, new Article() {FK_ArticleId = 1, Quantity = 10}
, new Article() {FK_ArticleId = 2, Quantity = 100}
, new Article() {FK_ArticleId = 2, Quantity = 100}
, new Article() {FK_ArticleId = 3, Quantity = 1000}
};
The following linq query returns what you need:
list.GroupBy(a => a.FK_ArticleId)
.Select(g => new Article() {FK_ArticleId = g.Key, Quantity = g.Sum(a => a.Quantity)});
// article id 1, quantity 30
// article id 2, quantity 200
// article id 3, quantity 1000
If you don't want to create a new article, you can take the first of the resulting group and set its Quantity to the correct value:
var results = list.GroupBy(a => a.FK_ArticleId)
.Select(g =>
{
var firstArticleOfGroup = g.First();
firstArticleOfGroup.Quantity = g.Sum(a => a.Quantity);
return firstArticleOfGroup;
});
I didn't test but this should give you an idea of the power of linq...
var stuff = lot
.GroupBy(p => p.FK_ArticleId)
.Select(g => g)
.ToList();
This should give you groups of articleIDs whereby you can easily get counts, create new consolidated lists etc.
For starters you can't use foreach because you're modifying the list and the Enumerator will throw an exception. You can do the following with Linq:
var grouped = lot.GroupBy(x => x.FK_ArticleId).ToArray();
foreach(var group in grouped)
{
group.First().Quantity = group.Sum(item => item.Quantity);
}
Now, first item in each group will contain the sum of all the quantities of items with the same FK_ArticleId. Now, to get the results use this:
var results = grouped.Select(g => g.First());
At this point it's purely your decision whether to return the results as a separate collection or insert them into the original list. If you opt for the second approach, don't forget to clear the list first:
list.Clear();
list.AddRange(results);
EDIT
A more elegant solution to accumulating the Quantity property into the first item of each group would be the following:
data.GroupBy(x=>x.FK_ArticleId)
.Select(g => g.Aggregate((acc, item) =>
{
acc.Quantity = item.Quantity;
return acc;
}));
This is what I scrapped in LinqPad:

Combining elements in the same list

suppose I have a list that comes from a database like this:
List<Item> list =
{
new Item
{
TypeID = 2,
Count = 5
},
new Item
{
TypeID = 2,
Count = 7
},
new Item
{
TypeID = 5,
Count = 2
}
};
I would like to sum up all elements with the same TypeID so that I have a final result list with two elements only:
List<Item> list =
{
new Item
{
TypeID = 2,
Count = 12
},
new Item
{
TypeID = 5,
Count = 2
}
};
How can I achive this using LINQ?
Cheers
Simon
list.GroupBy(x=>x.TypeID)
.Select(x=>new Item(){TypeID=x.Key,Count=x.Sum(y=>y.Count) })
.ToList();
You can use GroupBy to group by TypeID first and then do Sum on each group:
var result = list.GroupBy(x => x.TypeID)
.Select(g => new Item() {
TypeId = g.Key,
Count = g.Sum(x => x.Count)
})
.ToList();
Linq extension method Union.
var mergedList = list1.Union(list2).ToList();

Categories

Resources