【问题标题】:Using Linq to sum up to a number (and skip the rest)使用 Linq 求和一个数字(并跳过其余部分)
【发布时间】:2017-01-28 07:18:22
【问题描述】:

如果我们有一个包含这样一个数字的类:

class Person 
{
  public string Name {get; set;}
  public int Amount {get; set;}
}

然后是一群人:

IList<Person> people;

其中包含 10 个随机姓名和数量的人 是否有一个 Linq 表达式会返回一个总和满足条件的 Person 对象的子集合?

例如,我想要金额总和低于 1000 的前 x 人。 我可以通过传统方式做到这一点

 var subgroup = new List<Person>();

 people.OrderByDescending(x => x.Amount);

 var count = 0;
 foreach (var person in people)
 {
    count += person.Amount;
    if (count < requestedAmount)
    {
        subgroup.Add(person);
    }
    else  
    {
        break;
    }
 }

但我一直想知道是否有一种优雅的 Linq 方法可以使用 Sum 和其他一些函数(如 Take)来执行类似的操作?

更新

这太棒了:

var count = 0;
var subgroup = people
                  .OrderByDescending(x => x.Amount)
                  .TakeWhile(x => (count += x.Amount) < requestedAmount)
                  .ToList();

但我想知道是否可以以某种方式进一步更改它,以便抓住人员列表中的下一个人并将剩余部分添加到总和中,以便总金额等于请求的金额。

【问题讨论】:

  • people.OrderByDescending(x =&gt; x.Amount); 这一行,什么都不做(usefull),因为返回的值会被排序,而不是列表本身...
  • 我明白,但我把它写成伪代码。抱歉,如果不清楚

标签: c# linq


【解决方案1】:

在将 Nicks 答案与 ORM 结合使用时要小心,因为在围绕它包装事务时,它也可以改变数据库中的对象值,无论是有意还是无意。至少我们公司是这样的。不过它对我们很有帮助。

【讨论】:

    【解决方案2】:

    你可以使用TakeWhile:

    int s = 0;
    var subgroup  = people.OrderBy(x => x.Amount)
                          .TakeWhile(x => (s += x.Amount) < 1000)
                          .ToList();
    

    注意:您在帖子中提到首先 x 人。可以将其解释为在达到1000 之前加起来的最小数量。所以,我使用了OrderBy。但是,如果您想从拥有最高金额的人那里开始获取,您可以将其替换为 OrderByDescending


    编辑:

    要让它从列表中选择一个您可以使用的项目:

    .TakeWhile(x => {
                       bool bExceeds = s > 1000;
                       s += x.Amount;                                 
                       return !bExceeds;
                    })
    

    这里的TakeWhile 会检查上一次 迭代中的s 值,因此需要再进行一次,以确保已超过1000

    【讨论】:

    • 不错的答案,我认为TakeWhileWhere 更好,因为它会停止迭代。结果相同,但更适合迭代。你得到了我的投票。
    • 嗯,这太棒了......但只是为了增加一点趣味性,有没有办法从下一个人那里获取剩余值,以便平均总和等于“requestedAmount”?跨度>
    • @Nick 有点丑但是; x =&gt; (s += x.Amount) &lt; 1000||(done^=true)
    • 请不要修改LINQ查询中的变量;这是一个非常糟糕的编程习惯。它可能会导致一些真正奇怪的场景。
    • @EricLippert 我同意,当子组迭代不止一个时,它会给出奇怪的结果,因为计数器没有重置。我用一个可以解决问题的扩展方法添加了另一个答案。
    【解决方案3】:

    我不喜欢这个问题的所有答案。他们要么在查询中改变一个变量 - 一种导致意外结果的坏习惯 - 或者在 Niklas 的(否则很好)解决方案的情况下,返回一个错误类型的序列,或者在 Jeroen 的答案的情况下,代码是正确的,但可以用来解决更普遍的问题。

    我会改进 Niklas 和 Jeroen 的努力,方法是制作一个返回正确类型的实际通用解决方案:

    public static IEnumerable<T> AggregatingTakeWhile<T, U>(
      this IEnumerable<T> items, 
      U first,
      Func<T, U, U> aggregator,
      Func<T, U, bool> predicate)
    {
      U aggregate = first;
      foreach (var item in items)
      {
        aggregate = aggregator(item, aggregate);
        if (!predicate(item, aggregate))
          yield break;
        yield return item; 
      }
    }
    

    我们现在可以使用它来实现特定问题的解决方案:

    var subgroup = people
      .OrderByDescending(x => x.Amount)
      .AggregatingTakeWhile(
        0, 
        (item, count) => count + item.Amount, 
        (item, count) => count < requestedAmount)
      .ToList();
    

    【讨论】:

      【解决方案4】:

      我接受了Eric Lippert 的评论并提出了这个更好的 解决方案。我认为最好的方法是创建一个函数(在我的例子中我写了一个扩展方法)

      public static IEnumerable<T> TakeWhileAdding<T>(
          this IEnumerable<T> source, 
          Func<T, int> selector, 
          Func<int, bool> comparer)
      {
          int total = 0;
      
          foreach (var item in source)
          {
              total += selector(item);
      
              if (!comparer(total))
                  yield break;
      
              yield return item;
          }
      }
      

      用法:

      var values = new Person[]
      {
          new Person { Name = "Name1", Amount = 300 },
          new Person { Name = "Name2", Amount = 500 },
          new Person { Name = "Name3", Amount = 300 },
          new Person { Name = "Name4", Amount = 300 }
      };
      
      var subgroup = values.TakeWhileAdding(
          person => person.Amount, 
          total => total < requestedAmount);
      
      foreach (var v in subgroup)
          Trace.WriteLine(v);
      

      这也可以为doublefloat 或类似TimeSpan 的东西创建。

      这样每次subgroup 被迭代时,都会使用一个新的计数器。

      【讨论】:

        【解决方案5】:

        我不喜欢这些在 linq 查询中改变状态的方法。

        编辑: 我没有说明我之前的代码未经测试并且有点伪。我也错过了 Aggregate 实际上一次吃掉整个东西的观点——正如正确指出的那样,它不起作用。虽然这个想法是正确的,但我们需要一个 Aggregage 的替代方案。

        很遗憾 LINQ 没有运行聚合。我在这篇文章中建议来自 user2088029 的代码:How to compute a running sum of a series of ints in a Linq query?

        然后使用它(这是经过测试的,是我想要的):

        var y = people.Scanl(new { item = (Person) null, Amount = 0 },
            (sofar, next) => new { 
                item = next, 
                Amount = sofar.Amount + next.Amount 
            } 
        );       
        

        为了长寿,这里被盗代码:

        public static IEnumerable<TResult> Scanl<T, TResult>(
            this IEnumerable<T> source,
            TResult first,
            Func<TResult, T, TResult> combine)
            {
                using (IEnumerator<T> data = source.GetEnumerator())
                {
                    yield return first;
        
                    while (data.MoveNext())
                    {
                        first = combine(first, data.Current);
                        yield return first;
                    }
                }
            }
        

        以前的错误代码:

        我还有一个建议;以列表开头

        people
        
        [{"a", 100}, 
         {"b", 200}, 
         ... ]
        

        计算运行总数:

        people.Aggregate((sofar, next) => new {item = next, total = sofar.total + next.value})
        
        
        [{item: {"a", 100}, total: 100}, 
         {item: {"b", 200}, total: 300},
         ... ]
        

        然后使用 TakeWhile 和 Select 只返回项目;

        people
         .Aggregate((sofar, next) => new {item = next, total = sofar.total + next.value})
         .TakeWhile(x=>x.total<1000)
         .Select(x=>x.Item)
        

        【讨论】:

        • @JeroenvanLangen:你完全正确。我做了一些错误的假设 - 请检查更新的代码。
        • @NiklasJ 太好了,你更新了你的答案,有趣的是看到所有的羊都点击而不检查.... (基于你/eric 的想法,我写了一个扩展方法(见我的第二个答案))
        • @JeroenvanLangen Scanl 的优势在于它是通用的并且可重复用于任何甚至模糊相似的情况。基本上,这是 LINQ 中缺少的功能。虽然您的代码没有任何问题,但在我看来,这是解决问题的程序解决方案。
        • 它认为,您提供的代码的可读性不如我的TakeWhileAdding 扩展方法。如您所见,该方法是如何被调用的。通过为每种使用的类型创建一些重载方法,我没有看到问题。 (int、float、double、TimeSpan 等)
        • 这变成了聊天。我深受 Clojure 的影响,这对函数被称为 reduce/reductions。 Linq 可能有(而且 imo 应该有)聚合/聚合。假设确实存在,我认为内联程序解决方案比标准工具的组合更糟糕。这里唯一添加的是缺少的功能。我就这样吧。
        【解决方案6】:

        Giorgos 为我指出了正确的方向,所以他的答案是公认的。

        但是为了完整起见,我在这里写下我最终得到的解决方案。

        var count = 0;
        var exceeds = false;
        
        var subgroup  = people.OrderBy(x => x.Amount).TakeWhile(x =>
        {
            if (exceeds)
            {
                return false;
            }
        
            count += x.Amount;
            if (count >= requestedAmount)
            {
                x.Amount = requestedAmount - (count - x.Amount);
                exceeds = true;
                return true;
            }
        
            return !exceeds;
        }).ToList();
        

        这将返回一个总金额等于请求金额的子组。 非常感谢!

        【讨论】:

        • return !exceeds 应该是 return true,因为此时 exceeds 只能是 false
        【解决方案7】:

        试试:

        int sumCount = 0;
        
        var subgroup = people
            .OrderByDescending(item => item.Amount)           // <-- you wanted to sort them?
            .Where(item => (sumCount += item.Amount) < requestedAmount)
            .ToList();
        

        但它并不迷人......它的可读性会降低。

        【讨论】:

          猜你喜欢
          • 2011-01-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-03-20
          • 2019-06-17
          • 2016-03-16
          相关资源
          最近更新 更多