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

时间:2016-09-20 11:26:15

标签: c# linq

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

IList<Person> people;

然后是一群人:

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

包含,让我们说10个随机名称和数量的人 是否有一个Linq表达式将返回一个Person对象的子集合,其总和满足条件?

例如,我想要金额总和低于1000的前x个人。 我可以通过

传统地做到这一点
var count = 0;
var subgroup = people
                  .OrderByDescending(x => x.Amount)
                  .TakeWhile(x => (count += x.Amount) < requestedAmount)
                  .ToList();

但是我一直想知道是否有一种优雅的Linq方式使用Sum做一些这样的事情然后像Take这样的其他功能?

更新

这太棒了:

boost::lexical_cast<std::string>(5)

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

7 个答案:

答案 0 :(得分:42)

您可以使用TakeWhile

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

注意:您在帖子中首先提到 x x人。人们可以将其解释为具有最小量的那个,直到达到1000为止。所以,我使用了OrderBy。但是,如果您想从具有最高金额的人开始提取,您可以用OrderByDescending替换它。

修改

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

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

TakeWhile此处检查之前次迭代中的s值,因此需要多一次,以确保已超出1000

答案 1 :(得分:24)

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

编辑: 我没有声明我之前的代码是未经测试的,并且有些伪y。我也错过了Aggregate实际上同时吃掉整个东西的观点 - 正确指出它没有用。这个想法是正确的,但我们需要一个替代Aggreage。

令人遗憾的是,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)

答案 2 :(得分:16)

我不喜欢这个问题的所有答案。他们要么在查询中改变一个变量 - 一个导致意外结果的坏习惯 - 或者在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();

答案 3 :(得分:7)

尝试:

int sumCount = 0;

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

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

答案 4 :(得分:3)

我接受了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 :(得分:1)

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

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

答案 6 :(得分:0)

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