Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Split list by element

Tags:

c#

linq

I have list of 1 and 0 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}

between two items, can be only one zero. How to split that list into sublists by 0?

Other words: if I have string like this: string myString = "111011011110111111011101" then it is easy to split it by 0 into few strings. But how to do it with list? This example shoudl produce these sublists:

1,1,1
1,1
1,1,1,1
1,1,1,1,1,1
1,1,1
1

so is there better way then casting each element into string, joining them and doing what I show what can be done with string ?

like image 380
kosnkov Avatar asked Apr 25 '15 16:04

kosnkov


5 Answers

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.)

like image 149
Martin Liversage Avatar answered Oct 21 '22 00:10

Martin Liversage


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);
like image 39
Thomas Levesque Avatar answered Oct 21 '22 00:10

Thomas Levesque


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();
like image 32
ASh Avatar answered Oct 21 '22 01:10

ASh


You can just group by the index of the next zero:

    static void Main(string[] args)
    {
        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.Select((e, i) => new { Element = e, Index = i })
            .Where(e => e.Element == 1)
            .GroupBy(e => list.IndexOf(0, e.Index));
    }   
like image 35
Alex Sikilinda Avatar answered Oct 21 '22 00:10

Alex Sikilinda


Maybe something simpler:

IEnumerable<IEnumerable<int>> Split(IEnumerable<int> items)
{
    List<int> result = new List<int>();
    foreach (int item in items)
        if (item == 0 && result.Any())
        {
            yield return result;
            result = new List<int>();
        }
        else
            result.Add(item);
    if (result.Any())
        yield return result;
}
like image 4
Michael Sander Avatar answered Oct 21 '22 02:10

Michael Sander