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