0

I'm trying to use Linq to convert IEnumerable<int> to IEnumerable<List<int>> - the input stream will be separated by special value 0.

IEnumerable<List<int>> Parse(IEnumerable<int> l) 
{
    l.Select(x => {
      .....; //?
      return new List<int>();
    });
}
var l = new List<int> {0,1,3,5,0,3,4,0,1,4,0};
Parse(l) // returns {{1,3,5}, {3, 4}, {1,4}}

How to implement it using Linq instead of imperative looping? Or is Linq not good for this requirement because the logic depends on the order of the input stream?

Ross Presser
  • 6,027
  • 1
  • 34
  • 66
ca9163d9
  • 27,283
  • 64
  • 210
  • 413
  • `Or is Linq not good for this requirement because the logic depends on the order of the input stream` **yes.** – Eser Feb 17 '16 at 23:13
  • There are several solutions to this problem but linq is not one of them. – omni Feb 17 '16 at 23:20
  • 1
    possibly helpful, the .GroupAdjacentBy() extension shown [here](http://stackoverflow.com/a/4682163/864696) – Ross Presser Feb 17 '16 at 23:21
  • Not sure why people are saying Linq isn't a solution to this problem, because it is just fine–`IEnumerables` are inherently ordered and, depending on the Linq method used (for example, GroupBy and OrderBy are going to change the order), it's safe to rely on ordering not changing. – ErikE Feb 17 '16 at 23:31

5 Answers5

4

Simple loop would be good option.

Alternatives:

Aggregate sample

var result = list.Aggregate(new List<List<int>>(),
    (sum,current) => { 
       if(current == 0) 
            sum.Add(new List<int>());
       else 
            sum.Last().Add(current);
    return sum;
});

Note: this is only sample of the approach working for given very friendly input like {0,1,2,0,3,4}.

One can even make aggregation into immutable lists but that will look insane with basic .Net types.

Community
  • 1
  • 1
Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
  • I think your Aggregate implementation results in an empty and likely unwanted extra list at the end. – ErikE Feb 17 '16 at 23:32
  • And note that this assumes that `list` will always have to start with a `0` – Markus Weninger Feb 17 '16 at 23:36
  • @ErikE yes. And it does assume it starts from 0. If one *really* want to create generic splitter with `Aggregate` there need to be more logic (i.e. carry pair of {ListOfLists, CurrentList} and check for all sort of empty conditions. (I've added note to the post too) – Alexei Levenkov Feb 17 '16 at 23:38
  • Do you think my answer handles all of those concerns? – ErikE Feb 17 '16 at 23:45
  • @ErikE *looks* reasonable to me (not verified, but I bet you've tried enough cases). I've already voted on that when you've posted it so. – Alexei Levenkov Feb 17 '16 at 23:53
2

Here's an answer that lazily enumerates the source enumerable, but eagerly enumerates the contents of each returned list between zeroes. It properly throws upon null input or upon being given a list that does not start with a zero (though allowing an empty list through--that's really an implementation detail you have to decide on). It does not return an extra and empty list at the end like at least one other answer's possible suggestions does.

public static IEnumerable<List<int>> Parse(this IEnumerable<int> source, int splitValue = 0) {
   if (source == null) {
      throw new ArgumentNullException(nameof (source));
   }
   using (var enumerator = source.GetEnumerator()) {
      if (!enumerator.MoveNext()) {
         return Enumerable.Empty<List<int>>();
      }
      if (enumerator.Current != splitValue) {
         throw new ArgumentException(nameof (source), $"Source enumerable must begin with a {splitValue}.");
      }
      return ParseImpl(enumerator, splitValue);
   }
}

private static IEnumerable<List<int>> ParseImpl(IEnumerator<int> enumerator, int splitValue) {
   var list = new List<int>();
   while (enumerator.MoveNext()) {
      if (enumerator.Current == splitValue) {
         yield return list;
         list = new List<int>(); 
      }
      else {
         list.Add(enumerator.Current);
      }
   }
   if (list.Any()) {
      yield return list;
   }
}

This could easily be adapted to be generic instead of int, just change Parse to Parse<T>, change int to T everywhere, and use a.Equals(b) or !a.Equals(b) instead of a == b or a != b.

ErikE
  • 48,881
  • 23
  • 151
  • 196
  • It seems the code will be much more concise if using foeeach. – ca9163d9 Feb 18 '16 at 00:35
  • @dc7a9163d9 Given that my code eagerly checks for an initial `0` value (so that the moment the `Parse` method is called, it throws, even before normal enumeration), I can't use a simple `foreach`. In any case, true, `foreach` is a little simpler than using `GetEnumerator()`, though like I said, it didn't meet my design goals. – ErikE Feb 18 '16 at 00:38
1

You could create an extension method like this:

    public static IEnumerable<IEnumerable<T>> SplitBy<T>(this IEnumerable<T> source, T value)
    {
        using (var e = source.GetEnumerator())
        {
            if (e.MoveNext())
            {
                var list = new List<T> { };
                //In case the source doesn't start with 0
                if (!e.Current.Equals(value))
                {
                    list.Add(e.Current);
                }
                while (e.MoveNext())
                {
                    if ( !e.Current.Equals(value))
                    {
                        list.Add(e.Current);
                    }
                    else
                    {
                        yield return list;
                        list = new List<T> { };
                    }

                }
                //In case the source doesn't end with 0
                if (list.Count>0)
                {
                    yield return list;
                }

            }
        }
    }

Then, you can do the following:

var l = new List<int> { 0, 1, 3, 5, 0, 3, 4, 0, 1, 4, 0 };
var result = l.SplitBy(0);
ocuenca
  • 38,548
  • 11
  • 89
  • 102
  • Your code is almost identical to mine, though I appreciate using a generic with `.Equals` and parameterizing the sentinel value for splitting. I did consider parameterizing, but decided in the moment it wasn't worth it, though have since changed my mind. :) – ErikE Feb 17 '16 at 23:56
  • Hi ErikE, sorry if my solution has similarities to yours, I was writing it at the moment you posted your solution. I thought in publishing it anyways because (as you said) it's a generic solution, but I'm totally agree you was the first in came out with the same idea – ocuenca Feb 18 '16 at 00:07
  • No problem, I figured that was the case! You made me realize that `IEnumerator` must be disposed of, and reminded me of how `.Equals` is important for generics as it's required for structs, for example. Remember any time you create a deferred enumerable that you need to check parameters for null eagerly in a public method that calls the private implementation. – ErikE Feb 18 '16 at 00:09
0

You could use GroupBy with a counter.

var list = new List<int> {0,1,3,5,0,3,4,0,1,4,0};

int counter = 0;
var result = list.GroupBy(x => x==0 ? counter++ : counter)
                 .Select(g => g.TakeWhile(x => x!=0).ToList())
                 .Where(l => l.Any());
juharr
  • 31,741
  • 4
  • 58
  • 93
  • Clever, though you might mention that this does fully enumerate the input list in the `GroupBy`. – ErikE Feb 18 '16 at 00:06
-2

Edited to fix possibility of zeroes within numbers

Here is a semi-LINQ solution:

var l = new List<int> {0,1,3,5,0,3,4,0,1,4,0};
string
    .Join(",", l.Select(x => x == 0 ? "|" : x.ToString()))
    .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
    .Select(x => x.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries));

This is probably not preferable to using a loop due to performance and other reasons, but it should work.

devuxer
  • 41,681
  • 47
  • 180
  • 292
  • 2
    1) I guess you assign the result of `string.Join` to somewhere 2) what about for `new List {1020304,5}` ? – Eser Feb 17 '16 at 23:26
  • @Eser, Okay, I admit this code is not best practice, but I think I've at least fixed it so that it's not wrong. – devuxer Feb 17 '16 at 23:39
  • @Eser, Not sure what you meant about assigning `string.Join()` to somewhere. I tested the code in my answer and it does work. – devuxer Feb 17 '16 at 23:45
  • I'm all for avoiding premature optimization, but I have to say that converting numeric data types to string is one of the worst habits a developer can ever get into. In 99% of the cases it is very slow. There are almost always better solutions. Strings are also rife with other issues that you just may not have thought of–best to avoid them. (For example, what if someone changed the code to work with `string` instead of `int`, and then later someone started passing in your delimiter in the strings?) This is just not a good way to do it. – ErikE Feb 17 '16 at 23:49
  • @ErikE, I agree...and said so in my answer. – devuxer Feb 17 '16 at 23:50
  • @devuxer Fair enough. :) I just felt it hadn't been said clearly enough! – ErikE Feb 17 '16 at 23:50
  • @ErikE, I'll try to clarify that a bit better. – devuxer Feb 17 '16 at 23:51