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

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

使用Linq求和得到一个数字(并跳过其余部分)

您可以使用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, floatTimeSpan创建。

这样,每次迭代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结合使用时要小心,因为当在其周围包装事务时,它也可以有意或无意地更改数据库中的对象值。至少在我们公司是这样。但它对我们很有好处。