繁体   English   中英

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

[英]How to get grouped values from List within List with LINQ

我正在寻找一个列表中的属性值总和列表,该列表本身是另一个列表的属性,使用 LINQ 按父列表中的属性分组。

为了解释一下,我有一个市场上的报价清单,其中包含一系列产品的交易日期和时间,以及每个报价中的价格和数量范围的清单。 我的课程是:

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

而我在寻找什么检索是的总和Quantity有一定Price为每TradingDateHourOfDay ,每一个ProductName

我还没有想出一个可行的解决方案,但作为开始,我正在尝试类似的东西(使用包含所有报价的List<Offer> offers报价,以检索报价 < 10 美元的数量):

List<double> quantities = offers.SelectMany(o => o.Bands).Where(b => b.Price < 10).Select(b => b.Quantity)

但我不知道如何对TradingDateHourOfDay进行GroupBy并检索Quantity的总和。 可以有多个Offer s的多OfferBand S代表不同的产品,提供的各种组合Price s,而我只想得到的金额Quantity在一定的价格按日期和时间进行分组的所有产品。

我可以以编程方式实现这一点,但我想要一个 LINQ 解决方案。 谢谢你的帮助。

编辑:

我忘记提及的是,如果TradingDateHourOfDay的指定Price没有Quantity ,我想检索double.NaN (或0 )。

使用包含六个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
-------------------------------------------------------------------

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

价格 >= 5

{ 24, 24, 22 }

其中价格 = 2

{ 6, 1, double.NaN }

价格 = 3

{ double.NaN, 6, 16 }

...其中输出是 01/01/2017 小时 1、01/01/2017 小时 2 和 02/01/2017 小时 1 中所有产品在指定价格下的数量总和。

希望这很清楚。

我相信我已经能够管理您所追求的分组,尽管我还没有完成(数量)*(无论价格与某些条件相匹配的任何价格)的总和,因为希望这是您可以自定义的东西,但是您需要.

为了对事物进行分组,我不得不使用多个嵌套投影并单独进行每个分组(解决这个问题实际上很有趣,最大的症结在于 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();

希望这会给你足够的时间来开始你的求和,你需要对 0 件物品进行任何额外的检查,价格不够高,等等。

请注意,如果您直接使用数据库,此查询可能不是最有效的 - 它可能会提取比此时实际需要的更多信息。 不过,我对您正在做的工作了解得不够多,无法开始优化它。

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

首先,您需要使用匹配的OfferBands进行过滤以获取所需的Offer

如果你想让它成为一个函数,你可以创建/传入一个过滤器,我将直接定义它:

Func<Band, bool> filter = (Band b) => b.Price == 3;

由于您不关心ProductName ,我使用了匿名类型,但您可以使用Offer代替。 在这一点上,我们也扔掉空槽:

var filteredOffers = offers.Select(o => new { TradingDate = o.TradingDate, HourOfDay = o.HourOfDay, OfferBands = o.OfferBands.Where(filter).ToList() }).Where(gb => gb.OfferBands.Count > 0);

现在,由于您想为原始数据中的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);

然后,回到原始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();

重新思考后,我意识到您可以通过保留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));

通过使用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;
                });

如果您愿意为零放弃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));

最后,为了结束死马,一些特殊的扩展方法可以保留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