簡體   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