简体   繁体   English

如何使用 LINQ 从列表中的列表中获取分组值

[英]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.我正在寻找一个列表中的属性值总和列表,该列表本身是另一个列表的属性,使用 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 .而我在寻找什么检索是的总和Quantity有一定Price为每TradingDateHourOfDay ,每一个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<Offer> offers报价,以检索报价 < 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 .但我不知道如何对TradingDateHourOfDay进行GroupBy并检索Quantity的总和。 There can be multiple Offer s with multiple OfferBand s for different products, with various combinations of offer Price s, and I just want to get sum of Quantity for all products at a certain price grouped by date and time.可以有多个Offer s的多OfferBand S代表不同的产品,提供的各种组合Price s,而我只想得到的金额Quantity在一定的价格按日期和时间进行分组的所有产品。

I could achieve this programmatically but I would like a LINQ solution.我可以以编程方式实现这一点,但我想要一个 LINQ 解决方案。 Thanks for your help.谢谢你的帮助。

Edit:编辑:

What I forgot to mention is that, where there are no Quantity s at the specified Price for a TradingDate and HourOfDay I would like to retrieve double.NaN (or 0 ).我忘记提及的是,如果TradingDateHourOfDay的指定Price没有Quantity ,我想检索double.NaN (或0 )。

With example data List<Offer> offers containing six Offer s:使用包含六个Offer的示例数据List<Offer> offers Offer

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:选择给定价格的数量总和,按日期和时间分组,将给出List<double>输出:

Where price >= 5价格 >= 5

{ 24, 24, 22 }

Where price = 2其中价格 = 2

{ 6, 1, double.NaN }

Where price = 3价格 = 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. ...其中输出是 01/01/2017 小时 1、01/01/2017 小时 2 和 02/01/2017 小时 1 中所有产品在指定价格下的数量总和。

Hopefully that is clear to follow.希望这很清楚。

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):为了对事物进行分组,我不得不使用多个嵌套投影并单独进行每个分组(解决这个问题实际上很有趣,最大的症结在于 LINQ 的 IGrouping 不像您预期​​的那样易于使用,因此每个我分组的时候我用 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.希望这会给你足够的时间来开始你的求和,你需要对 0 件物品进行任何额外的检查,价格不够高,等等。

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);
        }

First, you need to filter to get the desired Offer s with the matching OfferBands .首先,您需要使用匹配的OfferBands进行过滤以获取所需的Offer

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.由于您不关心ProductName ,我使用了匿名类型,但您可以使用Offer代替。 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:现在,由于您想为原始数据中的TradingDate + HourOfDay包含空槽但被过滤掉,因此对过滤的数据进行分组并创建一个字典:

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 :然后,回到原始offers组,找到所有不同的槽( TradingDate + HourOfDday )并将它们与QuantitySum匹配,用double.NaN填充空槽并转换为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:重新思考后,我意识到您可以通过保留filteredOffers中为空的插槽来简化,然后在分组后设置它们的值:

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:通过使用IGrouping Key来记住插槽,您可以简化查询:

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:如果您愿意为零放弃double.NaN ,您可以使这更简单:

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:最后,为了结束死马,一些特殊的扩展方法可以保留NaN返回属性并使用简单的查询形式:

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));

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM