LINQ - Grouped list with condition - c#

Read How to use Linq to group every N number of rows already
But i would like to know further
Suppose a list contains 22 ITEM01 with quantity 10 for each, and 2 ITEM02 with quantity 50 for each
# |ITEM |QUANTITY
==================
1 |ITEM01| 10
2 |ITEM01| 10
3 |ITEM01| 10
. . .
. . .
22|ITEM01| 10
23|ITEM02| 50
24|ITEM02| 50
How to get the result like : (If count > 10, go to next row)
ITEM |QUANTITY
=================
ITEM01 | 100
ITEM01 | 100
ITEM01 | 10
ITEM01 | 10
ITEM01 | 10
ITEM02 | 50
ITEM02 | 50
Thanks for your help!

Check comments within code to see what's going on and what the query really does.
// input generation
var input = Enumerable.Range(1, 22)
.Select(x => new { ID = x, Item = "ITEM01", Quantity = 10 })
.Concat(Enumerable.Range(23, 2)
.Select(x => new { ID = x, Item = "ITEM02", Quantity = 50 }));
// query you're asking for
var output =
input.GroupBy(x => x.Item) // group by Item first
.Select(g => g.Select((v, i) => new { v, i }) // select indexes within group
.GroupBy(x => x.i / 10) // group items from group by index / 10
.Select(g2 => g2.Select(x => x.v)) // skip the indexes as we don't need them anymore
.SelectMany(g2 => // flatten second grouping results and apply Sum logic
g2.Count() == 10
// if there are 10 items in group return only one item with Quantity sum
? new[] { new { Item = g.Key, Quantity = g2.Sum(x => x.Quantity) } }
// if less than 10 items in group return the items as they are
: g2.Select(x => new { Item = g.Key, Quantity = x.Quantity })))
.SelectMany(g => g); // flatten all results

Related

Remove rows and adjust number

I am trying to remove objects from a list where a certain property value are identical to the previous/next objects property value. If an object are found I need to update the nested objects value.
Example:
Level | Text
1 | General
2 | Equipment
3 | Field Staff
2 | Scheduling
3 | Scheduling
4 | Deadlines
4 | Windows
1 | Specialities
In the example above I want to remove the second Scheduling and change the Deadlines Level to 3 as well as the Windows to 3.
I tried to look a head and compare with the next object in the list and also keep a counter but it didnt work.
int counter = 0;
for (int i = 0; i < notes.Count(); i++)
{
if (i <= notes.Count() - 1)
{
var currentRow = notes.ElementAt(i);
var nextRow = notes.ElementAt(i + 1);
if (currentRow.Text.Equals(nextRow.Text))
{
notes.Remove(nextRow);
counter++;
}
else
{
notes.ElementAt(i).Level = notes.ElementAt(i).Level - counter;
counter = 0;
}
}
}
Could anyone point me in the correct direction?
You can do it with Linq:
1 - Get distinct lines
var distinctList = notes
.GroupBy(p => p.Text)
.Select(v => v.First());
2 - get deleted level
IEnumerable<int> deletedLevel = notes
.Except(distinctList)
.Select(l => l.Level);
3 - update your distinct list
foreach(int level in deletedLevel)
{
distinctList
.Where(l => l.Level >= level + 1)
.ToList()
.ForEach(item => { item.Level -= 1; });
}
Result :
Level | Text
1 | General
2 | Equipment
3 | Field Staff
2 | Scheduling
3 | Deadlines
3 | Windows
1 | Specialities
i hope that will help you out
Try this:
var query = notesList.GroupBy(x => x.Text)
.Where(g => g.Count() > 1)
.Select(y => y.Key)
.Select(y => new { Element = y, Index = Array.FindIndex<Notes>(notesList.ToArray(), t => t.Text ==y) })
.ToList();
var filteredList = new List<Notes>();
foreach (var duplicate in query)
{
filteredList = notesList.Where((n, index) => index < duplicate.Index + 1).ToList();
var newElems = notesList.Where((n, index) => index > duplicate.Index + 1).Select(t =>
new Notes {Level = t.Level == 1 ? 1 : t.Level - 1, Text = t.Text});
filteredList.AddRange(newElems);
}

Entity Framework - Select a fixed array of numbers as column of a query

How do I select an int[] to as a column of a EF-to-SQL query? Suppose I have another column, and I would like to count the number of times these numbers appear... result would look something like this:
int[] nos = { 1, 2, 9, 4 };
-----------------------------------
nos | Count
1 | 2
2 | 1
9 | 3
4 | 1
If I have a column SampleColumn with values:
-----------------------------------
SampleColumn
1
1
2
3
4
5
9
11
25
9
9
Something like this as my code (I honestly don't know how to approach it so I'm guessing it's like this):
var query = db.Table.Select(a => new { nos, a.SampleColumn.Count(b => b == nos } ).ToList();
I would like to be able to change nos as I prefer. Thanks.
you can group the list.
int[] nos = { 1, 2, 9, 4 };
var result = nos.GroupBy(x => x).Select(x => new { x.Key, Count = x.Count() });
your code
var query = db.Table.GroupBy(x => x.SampleColumn).Select(x => new { x.Key, Count = x.Count() });

How to get grouped values from List within List with LINQ

I'm looking to retrieve a list of the sum of property values in a list that is itself a property of another list, grouped by properties in the parent list, using LINQ.
To explain, I have a list of offers in a market with a trading date and hour of the day for a range of products, and a list of price and quantity bands within each offer. My classes are:
public class Offer
{
public DateTime TradingDate { get; set; }
public int HourOfDay { get; set; }
public string ProductName { get; set; }
public List<Band> OfferBands { get; set; }
}
public class Band
{
public decimal Price { get; set; }
public double Quantity { get; set; }
}
And what I'm looking to retrieve is the sum of Quantity for a certain Price for each TradingDate and HourOfDay, for every ProductName.
I haven't come up with a working solution, but as a start I'm trying something like (with a List<Offer> offers containing all offers, to retrieve quantities where the offer price < $10):
List<double> quantities = offers.SelectMany(o => o.Bands).Where(b => b.Price < 10).Select(b => b.Quantity)
But I don't know how to GroupBy the TradingDate and HourOfDay and retrieve the sum of Quantity. There can be multiple Offers with multiple OfferBands for different products, with various combinations of offer Prices, and I just want to get sum of Quantity for all products at a certain price grouped by date and time.
I could achieve this programmatically but I would like a LINQ solution. Thanks for your help.
Edit:
What I forgot to mention is that, where there are no Quantitys at the specified Price for a TradingDate and HourOfDay I would like to retrieve double.NaN (or 0).
With example data List<Offer> offers containing six Offers:
TradingDate | HourOfDay | ProductName | OfferBands
===================================================================
01/01/2017 | 1 | Chocolate | Price = 2, Quantity = 6
| | | Price = 5, Quantity = 10
-------------------------------------------------------------------
01/01/2017 | 2 | Chocolate | Price = 3, Quantity = 6
| | | Price = 5, Quantity = 20
-------------------------------------------------------------------
02/01/2017 | 1 | Chocolate | Price = 3, Quantity = 7
| | | Price = 6, Quantity = 9
-------------------------------------------------------------------
01/01/2017 | 1 | Cake | Price = 5, Quantity = 11
| | | Price = 8, Quantity = 3
-------------------------------------------------------------------
01/01/2017 | 2 | Cake | Price = 2, Quantity = 1
| | | Price = 8, Quantity = 4
-------------------------------------------------------------------
02/01/2017 | 1 | Cake | Price = 3, Quantity = 9
| | | Price = 5, Quantity = 13
-------------------------------------------------------------------
Selecting a sum of quantities for a given price, grouped by date and time, would give a List<double> output:
Where price >= 5
{ 24, 24, 22 }
Where price = 2
{ 6, 1, double.NaN }
Where price = 3
{ double.NaN, 6, 16 }
...where the output is the sum of quantities for all products at the specified prices for 01/01/2017 hour 1, 01/01/2017 hour 2, and 02/01/2017 hour 1.
Hopefully that is clear to follow.
First, you need to filter to get the desired Offers with the matching OfferBands.
You can create/pass-in a filter if you want to make this a function, I will just define it inline:
Func<Band, bool> filter = (Band b) => b.Price == 3;
Since you don't care about ProductName, I used an anonymous type, but you could use Offer instead. At this point, we throw out the empty slots as well:
var filteredOffers = offers.Select(o => new { TradingDate = o.TradingDate, HourOfDay = o.HourOfDay, OfferBands = o.OfferBands.Where(filter).ToList() }).Where(gb => gb.OfferBands.Count > 0);
Now, since you want to include empty slots for TradingDate+HourOfDay that are in the original data but were filtered out, group the filtered data and create a dictionary:
var mapQuantity = filteredOffers.GroupBy(o => new { o.TradingDate, o.HourOfDay })
.Select(og => new { og.Key.TradingDate, og.Key.HourOfDay, QuantitySum = og.Sum(o => o.OfferBands.Sum(ob => ob.Quantity)) })
.ToDictionary(og => new { og.TradingDate, og.HourOfDay }, og => og.QuantitySum);
Then, going back to the original offers group find all the distinct slots (TradingDate+HourOfDday) and match them up to the QuantitySum, filling empty slots with double.NaN and convert to a List:
var ans = offers.Select(o => new { o.TradingDate, o.HourOfDay }).Distinct().OrderBy(g => g.TradingDate).ThenBy(g => g.HourOfDay).Select(g => mapQuantity.TryGetValue(g, out var sumq) ? sumq : double.NaN).ToList();
After re-thinking, I realized you could simplify by preserving the slots that are empty in the filteredOffers and then set their values after grouping:
var filteredOffers = offers.Select(o => new { TradingDate = o.TradingDate, HourOfDay = o.HourOfDay, OfferBands = o.OfferBands.Where(filter).ToList() });
var ans = filteredOffers.GroupBy(o => new { o.TradingDate, o.HourOfDay })
.OrderBy(og => og.Key.TradingDate).ThenBy(og => og.Key.HourOfDay)
.Select(og => (og.Sum(o => o.OfferBands.Count) > 0 ? og.Sum(o => o.OfferBands.Sum(ob => ob.Quantity)) : double.NaN));
By using the IGrouping Key to remember the slots, you can simplify the query:
var ans = offers.GroupBy(o => new { o.TradingDate, o.HourOfDay }, o => o.OfferBands)
.OrderBy(obg => obg.Key.TradingDate).ThenBy(obg => obg.Key.HourOfDay)
.Select(obg => {
var filteredOBs = obg.SelectMany(ob => ob).Where(filter).ToList();
return filteredOBs.Count > 0 ? filteredOBs.Sum(b => b.Quantity) : double.NaN;
});
If you are willing to give up the double.NaN for zero instead, you can make this even simpler:
var ans = offers.GroupBy(o => new { o.TradingDate, o.HourOfDay }, o => o.OfferBands)
.OrderBy(obg => obg.Key.TradingDate).ThenBy(obg => obg.Key.HourOfDay)
.Select(obg => obg.SelectMany(ob => ob).Where(filter).Sum(b => b.Quantity));
Finally, to finish the dead horse off, some special extension methods can preserve the NaN returning property and use the simple query form:
public static class Ext {
static double ValuePreservingAdd(double a, double b) => double.IsNaN(a) ? b : double.IsNaN(b) ? a : a + b;
public static double ValuePreservingSum(this IEnumerable<double> src) => src.Aggregate(double.NaN, (a, b) => ValuePreservingAdd(a, b));
public static double ValuePreservingSum<T>(this IEnumerable<T> src, Func<T, double> select) => src.Select(s => select(s)).Aggregate(double.NaN, (a, b) => ValuePreservingAdd(a, b));
}
var ans = offers.GroupBy(o => new { o.TradingDate, o.HourOfDay }, o => o.OfferBands)
.OrderBy(obg => obg.Key.TradingDate).ThenBy(obg => obg.Key.HourOfDay)
.Select(obg => obg.SelectMany(ob => ob).Where(filter).ValuePreservingSum(b => b.Quantity));
I believe I've been able to manage the groupings you are after, though I haven't done the summation of the (quantity)*(whatever price matches some condition), as hopefully that is something that you can customize however you need to.
To get things grouped, I had to use several nested projections and do each grouping individually (it was actually quite fun to work this out, the big sticking point is that LINQ's IGrouping isn't as straightforward to use as you might expect, so each time I grouped I did a projection with a Select):
var projected = offers.GroupBy(x => x.ProductName)
.Select(x => new
{
ProductName = x.Key,
Dates = x.GroupBy(y => y.TradingDate).ToList()
.Select(y => new
{
TradingDate = y.Key,
Times = y.GroupBy(z => z.HourOfDay).ToList()
.Select(zx => new
{
Time = zx.Key,
Items = zx.ToList()
})
})
}).ToList();
Hopefully, this will give you enough to start on for doing your summation with whatever extra checks you need for 0 items, prices not high enough, and so on.
Note that this query is probably not the most efficient if you're working directly with a database - it probably pulls more information than it really needs to at this point. I don't know enough about what you're working on to begin to optimize it, though.
var offers = new List<Offer>();
// flatten the nested list linq-style
var flat = from x in offers
from y in x.OfferBands
select new {x.TradingDate, x.HourOfDay, x.ProductName, y.Price, y.Quantity};
var grouped = from x in flat
group x by new {x.TradingDate, x.HourOfDay, x.ProductName}
into g
select new
{
g.Key.TradingDate,
g.Key.HourOfDay,
g.Key.ProductName,
OfferBands = (from y in g
group y by new {y.Price}
into q
select new {Price = q.Key, Quantity = q.Sum(_ => _.Quantity)}).ToList()
};
foreach (var item in grouped)
{
Console.WriteLine(
"TradingDate = {0}, HourOfDay = {1}, ProductName = {2}",
item.TradingDate,
item.HourOfDay,
item.ProductName);
foreach (var offer in item.OfferBands)
Console.WriteLine(" Price = {0}, Qty = {1}", offer.Price, offer.Quantity);
}

Linq select all items where in list with groupby

I have the following data as a list:
raceId data position
1 A 0
1 B 0
1 F 1
1 J 0
2 A 2
2 F 1
3 A 0
3 J 2
3 M 1
3 V 3
I need to get the total (count) of races where there are ALL matching letters with the same raceid.
I.E a search on 'A' and 'J' = 2 (race's 1 and 3)
In addition I need to get the position data for each.
raceId data position
1 A 0
1 J 0
3 A 0
3 J 2
So far I have the following code.
var dataValues = new string[] { 'A', 'J' };
var races = raceData
.GroupBy( ac => ac.raceId )
.Select( grp => grp.First() )
.Where( t =>
dataValues
.All( s =>
dataValues
.Contains( t.data )
)
);
var racecount = races.count()
The issue is that this returns all raceId values where there is either letter in the data.
This should work for you:
var results = raceData.GroupBy(rd => rd.raceId)
.Where(g => dataValues.All(dv => g.Select(g2 => g2.data).Contains(dv)));
int raceCount = results.Count();
var results2 = results
.SelectMany(g => g)
.Where(rd => dataValues.Contains(rd.data));
raceCount will give you 2 and results2 will give you the 4 records you're expecting.
It works for me with your provided data anyway!
var groupedRaces = from r in raceData
group r by r.raceId into gp
select new { raceId = gp.Key, Datas = gp.Select(g => g.data).ToArray() };
var raceIds = from r in groupedRaces
where dataVals.All(mv => r.Datas.Contains(mv))
select r.raceId;
var races = from r in raceData
where raceIds.Contains(r.raceId) && dataVals.Contains(r.data)
select r;
Try this,
list.GroupBy(t => t.raceID).SelectMany(k => k).Where(x => dataValues.Contains(x.data))
.Select(f=> new { f.data ,f.position,f.raceID}).ToList();
Result,
Hope helps,

How to group list and split every n records?

I would like to know how to group item and split every N record by using LINQ
# |ITEM |QUANTITY
==================
1 |ITEM01| 10
2 |ITEM01| 10
3 |ITEM01| 10
. . .
. . .
22|ITEM01| 10
23|ITEM02| 50
24|ITEM02| 50
Suppose there's a list with 23 ITEM001 and 2 ITEM002
How to get
ITEM |QUANTITY
=================
ITEM001 | 100
ITEM001 | 100
ITEM001 | 20
ITEM002 | 100
Group by ITEM, if grouped > 10, go to next
Is there any way to achieve it? Thanks for you help!
Thanks for those nice guys help! Further question, now i would like to group the list like (grouped every 10 records, after grouping, if count does not reach 10, do not group), Sorry for my poor English :(
ITEM |QUANTITY
=================
ITEM01 | 100
ITEM01 | 100
ITEM01 | 10
ITEM01 | 10
ITEM01 | 10
ITEM02 | 50
ITEM02 | 50
Thanks for your help again!
var query =
items.GroupBy(item => item.Name)
.SelectMany(g => g.Select((item, index) => new { item, index })
.GroupBy(x => x.index / 10)
.Select(batch => new Item {
Name = batch.First().item.Name,
Quantity = batch.Sum(x => x.item.Quantity)
})).OrderBy(item => item.Name);
Group all items by name. That will give you two groups for your sample data.
Split each group into batches by 10 items, and select new aggregated item from each batch (batch can contain less than 10 items, as thirds batch for ITEM001).
Order results by item name.
This query can be simplified if you will use MoreLINQ (available from NuGet) Batch extension or write your own one:
var query =
items.GroupBy(item => item.Name)
.SelectMany(g => g.Batch(10)
.Select(batch => new Item {
Name = batch.First().Name,
Quantity = batch.Sum(item => item.Quantity)
})).OrderBy(item => item.Name);
Assume you have item class like this
public class Item
{
public string Name { get; set; }
public int Quantity { get; set; }
}
Then this sample list of items (constructed with NBuilder):
var items = Builder<Item>.CreateListOfSize(32)
.TheFirst(23)
.With(i => i.Name = "ITEM01")
.And(i => i.Quantity = 10)
.TheNext(9)
.With(i => i.Name = "ITEM02")
.And(i => i.Quantity = 50)
.Build();
Will give result:
[
{ Name: "ITEM01", Quantity: 100 },
{ Name: "ITEM01", Quantity: 100 },
{ Name: "ITEM01", Quantity: 30 },
{ Name: "ITEM02", Quantity: 450 }
]
NOTE: #Thejaka solution will give you five items in this case - there will be two ITEM02 items with quantity 350 and 100.
Note: Below answer groups items by sum, not count. Nevertheless, I leave it here for future reference. Maybe someone will ever have similar problem.
You can achieve it with the following LINQ query:
List<Item> items = new List<Item>()
{
new Item() { Name = "Item01", Quantity = 40 }, // 130
new Item() { Name = "Item01", Quantity = 70 },
new Item() { Name = "Item01", Quantity = 10 },
new Item() { Name = "Item01", Quantity = 10 },
new Item() { Name = "Item02", Quantity = 50 }, // 100
new Item() { Name = "Item02", Quantity = 50 },
new Item() { Name = "Item03", Quantity = 10 } // 10
};
var result =
// Group by Name, calculate total sum for each group
items.GroupBy(i => i.Name, (k, v) => new Item()
{
Name = k,
Quantity = v.Sum(i => i.Quantity)
})
// Split groups into 100 packages
.SelectMany(i => Enumerable.Range(0, (int)Math.Ceiling(i.Quantity / 100.0))
.Select(n => new Item()
{
Name = i.Name,
Quantity = i.Quantity > ((n + 1) * 100) ? 100 : i.Quantity - n * 100
}))
.ToList();
Then you have four elements in the list:
Item01 100
Item01 30
Item02 100
Item03 10

Categories