Split list by element

int c = 0;
var list = new List<int>{1,1,1,0,1,1,0,1,1,1,1,0,1,1,1,1,1,1,0,1,1,1,0,1};

var res = list
    // split in groups and set their numbers
    // c is a captured variable
    .Select(x=>new {Item = x, Subgroup = x==1 ? c : c++})
    // remove zeros
    .Where(x=>x.Item!=0)
    // create groups
    .GroupBy(x=>x.Subgroup)
    // convert to format List<List<int>>
    .Select(gr=>gr.Select(w=>w.Item).ToList())
    .ToList();

You can solve your problem by transforming the input sequence into a sequence of sequences just like the LINQ GroupBy does. However, in your case you are grouping on a change in the input sequence. There is perhaps the possibility of combining existing LINQ operators like GroupBy, Zip and Skip into something that does what you want but I think it is easier (and performs better) to create an iterator block that looks at pairs of items in the input sequence:

static class EnumerableExtensions {

  public static IEnumerable<IEnumerable<T>> GroupOnChange<T>(
    this IEnumerable<T> source,
    Func<T, T, Boolean> changePredicate
  ) {
    if (source == null)
      throw new ArgumentNullException("source");
    if (changePredicate == null)
      throw new ArgumentNullException("changePredicate");

    using (var enumerator = source.GetEnumerator()) {
      if (!enumerator.MoveNext())
        yield break;
      var firstValue = enumerator.Current;
      var currentGroup = new List<T>();
      currentGroup.Add(firstValue);
      while (enumerator.MoveNext()) {
        var secondValue = enumerator.Current;
        var change = changePredicate(firstValue, secondValue);
        if (change) {
          yield return currentGroup;
          currentGroup = new List<T>();
        }
        currentGroup.Add(secondValue);
        firstValue = secondValue;
      }
      yield return currentGroup;
    }
  }

}

GroupOnChange will take the items in the input sequence and group them into a sequence of sequences. A new group is started when changePredicate is true.

You can use GroupOnChange to split your input sequence exactly as you want to. You then have to remove the groups that have zero as a value by using Where.

var groups = items
  .GroupOnChange((first, second) => first != second)
  .Where(group => group.First() != 0);

You can also use this approach if the input are class instances and you want to group by a property of that class. You then have to modify the predicate accordingly to compare the properties. (I know you need this because you asked a now deleted question that was slightly more complicated where the input sequence was not simply numbers but classes with a number property.)


You could write an extension method like this:

public static class Extensions
{
    public static IEnumerable<IEnumerable<TSource>> Split<TSource>(this IEnumerable<TSource> source, TSource splitOn, IEqualityComparer<TSource> comparer = null)
    {
        if (source == null)
            throw new ArgumentNullException("source");
        return SplitIterator(source, splitOn, comparer);
    }

    private static IEnumerable<IEnumerable<TSource>> SplitIterator<TSource>(this IEnumerable<TSource> source, TSource splitOn, IEqualityComparer<TSource> comparer)
    {
        comparer = comparer ?? EqualityComparer<TSource>.Default;
        var current = new List<TSource>();
        foreach (var item in source)
        {
            if (comparer.Equals(item, splitOn))
            {
                if (current.Count > 0)
                {
                    yield return current;
                    current = new List<TSource>();
                }
            }
            else
            {
                current.Add(item);
            }
        }

        if (current.Count > 0)
            yield return current;
    }
}

And use it like this:

var list = new List<int>{1,1,1,0,1,1,0,1,1,1,1,0,1,1,1,1,1,1,0,1,1,1,0,1};
var result = list.Split(0);

Tags:

C#

Linq