使用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();
但是,我想知道是否可以通过某种方式进一步改变它,以便抓取人员列表中的下一个人,并将余额添加到总和中,以使总金额等于请求金额。
你可以使用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);
这也可以为double
, float
或类似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();
这将返回一个小组,其总金额等于请求金额。 非常感谢!