How to get subset of List<T> items based on their Sum of an object property

Viewed 478

I want to get a subset of objects from a List based on the value of one of the object properties, specifically I want to get the first few objects based on the sum of their aggregated value of that property.

I could manually iterate through the list, add/Sum the value of the property and compare the result to my desired value, but is there a better way?

For example, imagine I have this list:

List<MyObj> MyObjList;

Where MyObj looks like this:

public class MyObj
{
  public int MyValue { get; set; }
}

And MyObjList has the following objects and values, in this order:

MyObjList[0].MyValue = 1;
MyObjList[1].MyValue = 3;
MyObjList[2].MyValue = 2;
MyObjList[3].MyValue = 3;
MyObjList[4].MyValue = 2;

For example I might want to get the first few items where their collective sum of MyValue <= 5, which would return only the first 2 objects.

How would you do that?

2 Answers

What you want is a combination of Aggregate and TakeWhile, so let's write that.

public static IEnumerable<S> AggregatingTakeWhile<S, A>(
  this IEnumerable<S> items,
  A initial,
  Func<A, S, A> accumulator,
  Func<A, S, bool> predicate) 
{
  A current = initial;
  foreach(S item in items)
  {
    current = accumulator(current, item);
    if (!predicate(current, item))
      break;
    yield return item;
  }
}

And so now you can say

var items = myObjList.AggregatingTakeWhile(
  0,
  (a, s) => a + s.MyValue,
  (a, s) => a <= 5);

Note that I have made the decision here to consult the predicate after the accumulator has been updated; depending on your application, you might want to tweak that slightly.

Another solution would be to combine aggregation with enumeration:

public static IEnumerable<(A, S)> RunningAggregate<S, A>(
  this IEnumerable<S> items,
  A initial,
  Func<A, S, A> accumulator) 
{
  A current = initial;
  foreach(S item in items)
  {
    current = accumulator(current, item);
    yield return (current, item);
  }
}

And now your desired operation is

var result = myObjList
  .RunningAggregate(0, (a, s) => a + s.MyValue)
  .TakeWhile( ((a, s)) => a <= 5)
  .Select(((a, s)) => s);

I might have gotten the tuple syntax wrong there; I haven't got Visual Studio handy right now. But you get the idea. The aggregation produces a sequence of (sum, item) tuples, and now we can use normal sequence operators on that thing.

For an old-school, non-Linq approach, you could write a simple method for this:

static List<MyObj> GetItemsUntilSumEquals(List<MyObj> items, int maxSum)
{    
    if (items == null) return null;

    var result = new List<MyObj>();
    var sum = 0;

    foreach (var item in items)
    {
        if (sum + item.MyValue > maxSum) break;
        sum += item.MyValue;
        result.Add(item);
    }

    return result;
}
Related