使用Linq求和得到一个数字(并跳过其余部分)
本文关键字:余部 数字 一个 Linq 使用 求和 | 更新日期: 2023-09-27 18:07:47
如果我们有一个包含如下数字的类:
class Person
{
public string Name {get; set;}
public int Amount {get; set;}
}
然后是一组人:
IList<Person> people;
包含,假设10个人的名字和数量是随机的是否有一个Linq表达式,将返回我的人和对象的总和满足条件的子集合?
例如,我想要前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方式做这样的事情使用Sum,然后一些其他的函数,如采取?
更新这太棒了:
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();
注意:你在你的文章第一个提到 x人。人们可以将其解释为具有最小的量,直到达到1000
。所以我用OrderBy
。但是,如果您想从拥有最高金额的人那里开始获取,则可以将其替换为OrderByDescending
。
编辑:
要从列表中选择多一个项,可以使用:
.TakeWhile(x => {
bool bExceeds = s > 1000;
s += x.Amount;
return !bExceeds;
})
这里的TakeWhile
检查来自上一个迭代的s
值,因此它将再执行一次,以确保1000
已被超过。
我不喜欢这些在linq查询中改变状态的方法。
编辑:我没有说我以前的代码是未经测试的,有点伪。我还忽略了一点,即Aggregate实际上是一次吃掉了整个东西——正如我正确指出的那样,它没有工作。虽然这个想法是对的,但我们需要一个替代Aggreage的方法。
很遗憾LINQ没有运行聚合。我建议在这篇文章中使用user2088029的代码:如何在Linq查询中计算一系列int的运行和?
然后使用这个(这是经过测试的,也是我想要的):
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)
我不喜欢这个问题的所有答案。它们要么在查询中改变变量——这是一种导致意外结果的糟糕做法——要么在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();
尝试:
int sumCount = 0;
var subgroup = people
.OrderByDescending(item => item.Amount) // <-- you wanted to sort them?
.Where(item => (sumCount += item.Amount) < requestedAmount)
.ToList();
但这并不迷人…
我接受了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);
也可以为double
, float
或TimeSpan
创建。
这样,每次迭代subgroup
时,都会使用一个新的计数器。
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();
返回一个总金额等于请求金额的子组。非常感谢!
在将Nicks answer与ORM结合使用时要小心,因为当在其周围包装事务时,它也可以有意或无意地更改数据库中的对象值。至少在我们公司是这样。但它对我们很有好处。