使用Linq来总结一个数字(并跳过其余的)

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

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

然后是一群人:

 IList<Person> people; 

那包含了,假设有10个随机名和数量的人是否有Linqexpression式,它会返回一个Person对象的子集合,其总和满足条件?

例如,我想要第一个x的人的金额总和是在1000以下。我可以做传统的

  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做这样的事情使用总结,然后像Take一样的其他function?

UPDATE

这是太棒了:

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

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

6 Solutions collect form web for “使用Linq来总结一个数字(并跳过其余的)”

你可以使用TakeWhile

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

注意:你在你的post里首先提到x个人。 人们可以把这个解释为最小的数量加起来直到1000 。 所以,我使用OrderBy 。 但是如果你想开始从具有最高金额的人那里获取,你可以用OrderByDescending来代替它。

编辑:

要从列表中select一个项目,您可以使用:

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

TakeWhile在这里检查了一次迭代的s值,所以需要再多一次,以确保已经超过了1000

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

编辑:我没有说,我以前的代码没有经过testing,有点伪y。 我也错过了Aggregate实际上一次吃完所有东西 – 正确地指出它不起作用。 这个想法是对的,但我们需要一个替代Aggreage。

LINQ没有正在运行的聚合是一件令人遗憾的事情。 我build议在这篇文章中user2088029的代码: 如何计算Linq查询中的一系列整数的运行总和? 。

然后使用这个(这是testing,是我的意图):

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

以前,错误的代码:

我有另外一个build议。 从一个列表开始

 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) 

我不喜欢这个问题的所有答案。 他们要么在查询中改变一个variables – 这是一个不好的做法,会导致意想不到的结果 – 或者在Niklas的(否则是好的)解决scheme的情况下,返回一个错误types的序列,或者,对于Jeroen的答案,代码是正确的,但可以解决一个更普遍的问题。

我将通过提供一个实际的通用解决scheme来改善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; } } 

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

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

尝试:

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

但这不是很有魅力,它的可读性会降低。

我采取了Eric Lippert的评论,并带来了更好的解决scheme。 我认为最好的方法是创build一个函数(在我的情况下,我写了一个扩展方法)

 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东西TimeSpan

这种方式每次subgroup迭代时,使用一个新的计数器。

吉尔戈斯指出我正确的方向,所以他的答案是接受的。

不过为了完整起见,我在这里写下了我最终解决的问题。

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

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

  • 使用Enumerable.Empty <T>()而不是新的List <T>()初始化IEnumerable <T>会更好吗?
  • 带输出参数的Func <T>
  • 从LINQ到SQL获取SQL查询?
  • LINQ:确定两个序列是否包含完全相同的元素
  • 如何使用一个string通过expression式创buildEF顺序?
  • 我可以在Linq查询中select多个对象吗?
  • 如何从XDocument获取XML节点
  • 为什么LINQ to Entities不能识别方法System.String ToString()?
  • 根据linq的ID列表select多个logging
  • 检查数组是否是另一个数组的子集
  • “Col1,Col2sorting”使用entity framework