You have a dependency between separate elements of your collection, specifically, for each element you want to know "was the previous element a zero?". As soon as your query depends on the previous element (or, more generally, as soon as your query depends on other elements of the same sequence), you should reach for Aggregate
(or in more general functional programming terms, fold
). This is because Aggregate
, unlike other LINQ operators, allows you to carry state with you from one iteration to the next.
So, to answer your question, I'd write this query as follows in LINQ.
// assume our list of integers it called values
var splitByZero = values.Aggregate(new List<List<int>>{new List<int>()},
(list, value) => {
list.Last().Add(value);
if (value == 0) list.Add(new List<int>());
return list;
});
I'll break this down into parts so I can better explain my thinking.
values.Aggregate(new List<List<int>>{new List<int>()},
As I said before, reach for Aggregate because we need to carry state. Putting a new empty list into our List of Lists removes an edge case of the List<List<int>>
having no lists in it.
(list, value) => {...}
Again, looking at the signature of our lambda expression (which is Func<List<List<int>>, int, List<List<int>>
), we can see the state passing explicitly: we accept a List<List<int>>
and return the same.
list.Last().Add(value);
Since we always want to work on the most recent List<int>
, we get the Last()
element of our list of lists (which will never be null due to the section above).
if (value == 0) list.Add(new List<int>());
This is where we do the splitting - on the next iteration, the call to Last() will return this new list.
return list;
And we finally pass the state on to the next iteration.
This could be generalized with little difficulty, in a SplitOn
method, as follows:
public static IEnumerable<IEnumerable<T>> SplitOn<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
return source.Aggregate(new List<List<T>> {new List<T>()},
(list, value) =>
{
list.Last().Add(value);
if (predicate(value)) list.Add(new List<T>());
return list;
});
}
The version using IEnumerable
's instead of List
's is somewhat less clear because of the way Enumerables work, but again, isn't particularly hard to create from the above code, and looks like (simplified just a touch via a ternary operator):
public static IEnumerable<IEnumerable<T>> SplitOn<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
return source.Aggregate(Enumerable.Repeat(Enumerable.Empty<T>(), 1),
(list, value) =>
{
list.Last().Concat(Enumerable.Repeat(value, 1));
return predicate(value) ? list.Concat(Enumerable.Repeat(Enumerable.Empty<T>(), 1)) : list;
});
}
You also might find Haskell's implementation of splitOn interesting, as it does exactly what you want. I would call it nontrivial (to put it lightly).