Using Linq to summarize a number (and skip the rest)

If we have a class that contains a number like this:

class Person { public string Name {get; set;} public int Amount {get; set;} } 

and then a set of people:

 IList<Person> people; 

This contains, say, 10 people of random names and sums. Is there a Linq expression that will return to me a subcollection of Person objects whose sum satisfies the condition?

For example, I want the first x people whose sum was less than 1000. I can do this traditionally

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

But I was wondering if there is an elegant way for Linq to do something like this using Sum, and then some other features like Take?

UPDATE

It is fantastic:

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

But I wonder if I can somehow change it further to capture the next person in the list of people and add a balance to the amount so that the total amount equals the requested amount.

+42
c # linq
Sep 20 '16 at 11:26
source share
6 answers

You can use TakeWhile :

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

Note. You mention the first x people in your post. One could interpret this as those that have the smallest amount that adds up to reaching 1000 . So, I used OrderBy . But you can replace this with OrderByDescending if you want to start receiving from the person with the highest amount.




Edit:

To do this, select another item from the list that you can use:

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

TakeWhile considers the value of s from the previous iteration here, so one more is needed to make sure that 1000 exceeded.

+43
Sep 20 '16 at 11:40
source share
β€” -

I don't like these mutation approaches inside linq queries.

EDIT: I did not claim that my previous code was untested and was somewhat pseudo-y. I also missed the fact that the Unit actually eats the whole thing at once - as correctly pointed out that this did not work. However, the idea was right, but we need an alternative to Aggreage.

It is a shame that LINQ does not have a working unit. I suggest code from user2088029 in this post: How to calculate the current amount of an int series in a Linq query? .

And then use this (which is verified and 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 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; start with a list

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

Calculate current 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 the elements;

 people .Aggregate((sofar, next) => new {item = next, total = sofar.total + next.value}) .TakeWhile(x=>x.total<1000) .Select(x=>x.Item) 
+24
Sep 20 '16 at 14:39
source share

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

I would improve the efforts of Niklas and Jerun by actually making a general solution that will return the correct 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; } } 

What we can now use to implement a solution to a specific problem:

 var subgroup = people .OrderByDescending(x => x.Amount) .AggregatingTakeWhile( 0, (item, count) => count + item.Amount, (item, count) => count < requestedAmount) .ToList(); 
+14
Sep 21 '16 at 17:46
source share

Try:

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

But this is not charming ... It will be less readable.

+7
Sep 20 '16 at 11:36
source share

I accepted Eric Lippert comment and came up with this best solution. I think the best way is to create a function (in my case I wrote an extension method)

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

Using:

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

It can also be created for double , float or something like TimeSpan .

Thus, every time a subgroup repeated, a new counter is used.

+3
Sep 21 '16 at 9:34
source share

Giorgos pointed me in the right direction so that his answer would be accepted.

However, for completeness, I am writing here the solution I ended up with.

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

Returns a subgroup whose total amount is equal to the requested amount. Thank you very much!

+1
Sep 20 '16 at 14:55
source share



All Articles