Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Aggregate values until a limit is reached

I need something similar to an AggregateWhile method. The standard System.Linq.Enumerable class doesn't provide it. Until now I've always been able to leverage the standard LINQ methods to solve every problem I've encountered. So I'd like to know if that's still possible in this case, or if I really do need to extend LINQ with a non-standard method.

The hypothetical AggregateWhile method would iterate over a sequence and apply the accumulator. The aggregation would be complete once a predicate returns false. The result is the aggregration of elements up to but not including the element for which the predicate failed.

Here's an example. We have a List { 1, 2, 3, 4, 5 } with an accumulator that adds the two input numbers together, and a predicate that states the accumulation must be less than 12. AggregateWhile would return 10 since that's the result of 1 + 2 + 3 + 4 and adding the final 5 would push the total over the limit. In code:

var list = new List<int> { 1, 2, 3, 4, 5 };
int total = list.AggregateWhile( (x, y) => x + y, a => a < 12 ); // returns 10

I need a purely functional solution, so closing over a temporary variable is not an option.

like image 476
HappyNomad Avatar asked Feb 10 '15 08:02

HappyNomad


2 Answers

You could either write the function yourself, or carry a flag with your accumulator:

int total = list.Aggregate(new { value = 0, valid = true }, 
                          (acc, v) => acc.value + v < 12 && acc.valid ?
                                      new { value = acc.value + v, valid = true } :
                                      new { value = acc.value, valid = false },
                            acc => acc.value); 

It's quite ugly, so writting a new AggregateWhile would be nicer:

public static TSource AggregateWhile<TSource>(this IEnumerable<TSource> source, 
                                         Func<TSource, TSource, TSource> func,
                                         Func<TSource, bool> predicate)
{
   using (IEnumerator<TSource> e = source.GetEnumerator()) {
       TSource result = e.Current;
       TSource tmp = default(TSource);
       while (e.MoveNext() && predicate(tmp = func(result, e.Current))) 
            result = tmp;
       return result;
   }
}

(no error checking for brevity)

like image 95
sloth Avatar answered Oct 12 '22 12:10

sloth


You can write your own extension method. This is not as perfect as the normal Linq methods, I cheated because I already know your requirements to make it simpler. In reality you may want an optional starting value for a and maybe different In and output types for T or other stuff:

public static class Linq
{
  public static T AggregateWhile<T>(this IEnumerable<T> sequence, Func<T, T, T> aggregate, Func<T, bool> predicate)
  {
     T a;
     foreach(var value in sequence)
     {
        T temp = aggregate(a, value);
        if(!predicate(temp)) break;
        a = temp;
     }
     return a;
  }
}
like image 33
nvoigt Avatar answered Oct 12 '22 12:10

nvoigt