Using Linq to sum up to a number (and skip the rest)

I don't like these approaches of mutating state inside linq queries.

EDIT: I did not state that the my previous code was untested and was somewhat pseudo-y. I also missed the point that Aggregate actually eats the entire thing at once - as correctly pointed out it didn't work. The idea was right though, but we need an alternative to Aggreage.

It's a shame that LINQ don't have a running aggregate. I suggest the code from user2088029 in this post: How to compute a running sum of a series of ints in a Linq query?.

And then use this (which is tested and is what I intended):

var y = people.Scanl(new { item = (Person) null, Amount = 0 },
    (sofar, next) => new { 
        item = next, 
        Amount = sofar.Amount + next.Amount 
    } 
);       

Stolen code here for longevity:

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

Previous, wrong code:

I have another suggestion; begin with a list

people

[{"a", 100}, 
 {"b", 200}, 
 ... ]

Calculate the running totals:

people.Aggregate((sofar, next) => new {item = next, total = sofar.total + next.value})


[{item: {"a", 100}, total: 100}, 
 {item: {"b", 200}, total: 300},
 ... ]

Then use TakeWhile and Select to return to just items;

people
 .Aggregate((sofar, next) => new {item = next, total = sofar.total + next.value})
 .TakeWhile(x=>x.total<1000)
 .Select(x=>x.Item)

You can use TakeWhile:

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

Note: You mention in your post first x people. One could interpret this as the ones having the smallest amount that adds up until 1000 is reached. So, I used OrderBy. But you can substitute this with OrderByDescending if you want to start fetching from the person having the highest amount.


Edit:

To make it select one more item from the list you can use:

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

The TakeWhile here examines the s value from the previous iteration, so it will take one more, just to be sure 1000 has been exceeded.


I dislike all answers to this question. They either mutate a variable in a query -- a bad practice that leads to unexpected results -- or in the case of Niklas's (otherwise good) solution, returns a sequence that is of the wrong type, or, in the case of Jeroen's answer, the code is correct but could be made to solve a more general problem.

I would improve the efforts of Niklas and Jeroen by making an actually generic solution that returns the right type:

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

Which we can now use to implement a solution to the specific problem:

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

Tags:

C#

Linq