Summing the previous values in an IEnumerable

2020-03-01 05:53发布

I have a sequence of numbers:

var seq = new List<int> { 1, 3, 12, 19, 33 };

and I want to transform that into a new sequence where the number is added to the preceding numbers to create a new sequence:

{ 1, 3, 12, 19, 33 } --> {1, 4, 16, 35, 68 }

I came up with the following, but I dislike the state variable 'count'. I also dislike the fact that I'm using the values Enumerable without acting on it.

int count = 1;
var summed = values.Select(_ => values.Take(count++).Sum());

How else could it be done?

7条回答
Ridiculous、
2楼-- · 2020-03-01 06:10

To use Linq and only iterate over the list once you could use a custom aggregator:

class Aggregator
{
    public List<int> List { get; set; }
    public int Sum { get; set; }
}

..

var seq = new List<int> { 1, 3, 12, 19, 33 };
var aggregator = new Aggregator{ List = new List<int>(), Sum = 0 };
var aggregatorResult = seq.Aggregate(aggregator, (a, number) => { a.Sum += number; a.List.Add(a.Sum); return a; });
var result = aggregatorResult.List;
查看更多
Summer. ? 凉城
3楼-- · 2020-03-01 06:10

Just to offer another alternative, albeit not really LINQ, you could write a yield-based function to do the aggregation:

public static IEnumerable<int> SumSoFar(this IEnumerable<int> values)
{
  int sumSoFar = 0;
  foreach (int value in values)
  {
    sumSoFar += value;
    yield return sumSoFar;
  }
}

Like BrokenGlass's this makes only a single pass over the data although unlike his returns an iterator not a list.

(Annoyingly you can't easily make this generic on the numeric type in the list.)

查看更多
Luminary・发光体
4楼-- · 2020-03-01 06:11

"Pure" LINQ:

var result = seq.Select((a, i) => seq.Take(i + 1).Sum());

One more "pure" LINQ O(n):

var res = Enumerable.Range(0, seq.Count)
    .Select(a => a == 0 ? seq[a] : seq[a] += seq[a - 1]);

One more LINQ, with state maintenance:

var tmp = 0;
var result = les.Select(a => { tmp += a; return tmp; });
查看更多
等我变得足够好
5楼-- · 2020-03-01 06:12

This is a common pattern in functional programming which in F# is called scan. It's like C#'s Enumerable.Aggregate and F#'s fold except that it yields the intermediate results of the accumulator along with the final result. We can implement scan in C# nicely with an extension method:

public static IEnumerable<U> Scan<T, U>(this IEnumerable<T> input, Func<U, T, U> next, U state) {
    yield return state;
    foreach(var item in input) {
        state = next(state, item);
        yield return state;
    }
}

And then use it as follows:

var seq = new List<int> { 1, 3, 12, 19, 33 };
var transformed = seq.Scan(((state, item) => state + item), 0).Skip(1);
查看更多
老娘就宠你
6楼-- · 2020-03-01 06:15
var seq = new List<int> { 1, 3, 12, 19, 33 };

var summed = new List<int>();

seq.ForEach(i => summed.Add(i + summed.LastOrDefault()));
查看更多
仙女界的扛把子
7楼-- · 2020-03-01 06:23

Stephen Swensen's answer is great, scan is exactly what you need. There is another version of scan though that doesn't require a seed, which would be slightly more appropriate for your exact problem.

This version requires that your output element type is the same as your input element type, which it is in your case, and gives the advantage of not needing you to pass in a 0 and then Skip the first (0) result.

You can implement this version of scan in C# as follows:

public static IEnumerable<T> Scan<T>(this IEnumerable<T> Input, Func<T, T, T> Accumulator)
{
    using (IEnumerator<T> enumerator = Input.GetEnumerator())
    {
        if (!enumerator.MoveNext())
            yield break;
        T state = enumerator.Current;
        yield return state;
        while (enumerator.MoveNext())
        {
            state = Accumulator(state, enumerator.Current);
            yield return state;
        }
    }
}

And then use it as follows:

IEnumerable<int> seq = new List<int> { 1, 3, 12, 19, 33 };
IEnumerable<int> transformed = seq.Scan((state, item) => state + item);
查看更多
登录 后发表回答