【问题标题】:Calculate a total for each record in a list based on filtered list根据过滤列表计算列表中每条记录的总数
【发布时间】:2021-04-23 15:31:12
【问题描述】:

我有以下示例列表:

Index No Path A B C Amount
1 1000 1000 a b c 700
2 1001 1000.1001 a b c 100
3 1001 1000.1001 a b d 200

我需要遍历列表,对于每条记录,我需要在列表中计算一个值并根据特定条件将其存储在一个新列中:

  • 过滤路径包含记录的的记录
  • 在 A、B 和 C 列中过滤具有相同值的记录
  • 计算Amount的总和并保存为TotalAmount

举个例子:

Index No Path A B C Amount TotalAmount
1 1000 1000 a b c 700 800
2 1001 1000.1001 a b c 100 100
3 1001 1000.1001 a b d 200 200

对于第一条记录,我需要在列表中查找 Path 包含记录的 No (1000) 并且在 A 列中具有相同值的所有记录, B 和 C。所以在这个例子中,对于第一条记录,我们取 index = 1 和 index = 2 的记录,计算金额的总和并将其返回到 TotalAmount 列中。

对此我有这样的想法:

foreach (record in List)
{
    var totalAmount = List
        .Where(e => 
            e.Path.Contains(record.No) && 
            e.A == record.A && 
            e.B == record.B && 
            e.C == record.C)
        .Sum(e => e.Amount)
}

但是,它没有返回我想要的,我不知道如何在这样的计算后将它保存回列表。

【问题讨论】:

  • 似乎“包含”可能是一个有问题的选项(取决于“否”字段)。如果您在第 1000 号记录中,它可能会匹配您不想要的具有该子字符串(例如 10000)的其他记录,如果路径包含 1000、10000 等,则第 100 号也将匹配。如果您知道您只是在搜索 Path 字段的开头,然后尝试使用 StartsWith 之类的方法(并附加一个句点,以免过度抓取)。例如e.Path.StartsWith(record.No + ".")
  • @BryanLewis:我也在想同样的事情,但是根据这条路的“深度”,可能还需要做类似e.Path.StartsWith(record.No + ".") || e.Path.Contains($".{record.No}.") 这样的事情。可能还应该指定一个 Ordinal StringComparison。
  • “它没有返回我想要的”是什么意思?您是说单步执行代码时totalAmount 不正确吗?或者只是它没有设置recordTotalAmount 属性?

标签: c# linq


【解决方案1】:

如果您没有从 Sum 操作中获得正确的结果,可能是由于 Contains 方法返回了您不想要的记录(例如,因为 10000 包含 1000)。一种处理方法是在检查值的开头、中间和结尾时添加句点。

另一个问题与之前的回答一样 - 您需要设置记录的 TotalAmount 属性(假设它有一个):

foreach (var record in list)
{
    record.TotalAmount = list
        .Where(e => 
            (e.Path.StartsWith($"{record.No}.") ||
            e.Path.Contains($".{record.No}.") ||
            e.Path.EndsWith($".{record.No}")) && 
            e.A == record.A && 
            e.B == record.B && 
            e.C == record.C)
        .Sum(e => e.Amount)
}

【讨论】:

    【解决方案2】:

    将结果更新为循环中运行的当前项目

    list.ForEach(record=>{
    {
     var totalAmount = list.Where(e => e.Path.Contains(record.No) && e.A == record.A && e.B 
     == record.B 
      && e.C == record.C).Sum(e => e.Amount);
    
     record.TotalAmount = totalAmount;
    });
    

    【讨论】:

      【解决方案3】:

      首先,我建议不要将 TotalAmount 放在您的记录类型上,而是创建一个新类型来表示您正在寻找的结果。

      public record Source(int Index, string No, string Path, string A, string B, string C, double Amount);
      public record Totals(int Index, string No, string Path, string A, string B, string C, double Amount, double TotalAmount);
      

      接下来,我将为您的数据创建一个更易于推理的中间表示。听起来(A, B, C) 的组合是有意义的,听起来你的Path 确实是当前记录的父元素的集合。

          var intermediates = sources.Select(
              source => new
              {
                  source,
                  pathComponents = source.Path.Split('.').ToHashSet(),
                  abc = (source.A, source.B, source.C)
              });
      

      然后让我们按组合的abc 值对这些值进行分组,这样我们就可以快速查找与给定条目属于同一组的项目。

          var byAbc = intermediates.ToLookup(e => e.abc);
      

      最后,计算总数:

          var totals = 
              from intermediate in intermediates
              let totalAmount = byAbc[intermediate.abc]
                  .Where(e => e.pathComponents.Contains(intermediate.source.No))
                  .Sum(e => e.source.Amount)
              let source = intermediate.source
              select new Totals(
                      source.Index,
                      source.No,
                      source.Path,
                      source.A,
                      source.B,
                      source.C,
                      source.Amount,
                      totalAmount);
      

      以下是这种方法的一些好处:

      1. 通过将问题分解为具有不可变行为的各个步骤:
        1. 可以单步执行代码并直观地检查(或记录)每行代码的结果。
        2. 可以在调试器中跳回上一步并再次遍历代码,而无需更改程序的行为。
        3. 可以将各个步骤的结果放入有助于读者理解意图的命名变量中。
        4. 可以轻松地将各个步骤重构为单独的方法或类。
      2. 通过将路径表示为可以使用相等检查进行比较的单个片段的集合,我们避免了包含逻辑中的错误。例如,如果一条记录有一个No100,那么如果您使用简单的string.Contains 检查,上述示例中的所有路径都将匹配它,即使没有一个路径实际上包含100 作为路径的组成部分。
      3. 通过使用 Lookup 和 HashSet 等数据结构,我们避免了高渐近复杂性,这意味着这可以很好地扩展到非常大的数据集。
      4. 通过对输入和输出使用不同的类型,您可以防止在应用程序中引入错误,即有人需要使用 TotalAmount 属性,但会给出尚未填充该属性的列表。

      Here's a LINQPad script 把整个事情放在一起。

      结果:

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-09-03
        • 2021-06-07
        • 1970-01-01
        • 2011-07-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-12-15
        相关资源
        最近更新 更多