How to create new list from list of list where elements are in new list are in alternative order?

Viewed 1323

Suppose I have list of list. I want to create new list from given list of list such that elements are in order of example given below.

Inputs:-

List<List<int>> l = new List<List<int>>();

List<int> a = new List<int>();
a.Add(1);
a.Add(2);
a.Add(3);
a.Add(4);
List<int> b = new List<int>();
b.Add(11);
b.Add(12);
b.Add(13);
b.Add(14);
b.Add(15);
b.Add(16);
b.Add(17);
b.Add(18);

l.Add(a);
l.Add(b);

Output(list):-

1
11
2
12
3
13
4
14
15
16 

And output list must not contain more than 10 elements.

I am currently doing this using foreach inside while but I want to know how can I do this using LINQ.

int loopCounter = 0,index=0;
List<int> o=new List<int>();
while(o.Count<10)
{
    foreach(List<int> x in l)
    {
        if(o.Count<10)
           o.Add(x[index]);
    }
    index++;
}

Thanks.

3 Answers

Use the SelectMany and Select overloads that receive the item's index. That will be used to apply the desired ordering. The use of the SelectMany is to flatten the nested collections level. Last, apply Take to retrieve only the desired number of items:

var result = l.SelectMany((nested, index) => 
                  nested.Select((item, nestedIndex) => (index, nestedIndex, item)))
              .OrderBy(i => i.nestedIndex)
              .ThenBy(i => i.index)
              .Select(i => i.item)
              .Take(10);

Or in query syntax:

var result = (from c in l.Select((nestedCollection, index) => (nestedCollection, index))
              from i in c.nestedCollection.Select((item, index) => (item, index))
              orderby i.index, c.index
              select i.item).Take(10);

If using a C# 6.0 and prior project an anonymous type instead:

var result = l.SelectMany((nested, index) => 
                  nested.Select((item, nestedIndex) => new {index, nestedIndex, item}))
              .OrderBy(i => i.nestedIndex)
              .ThenBy(i => i.index)
              .Select(i => i.item)
              .Take(10);

To explain why Zip alone is not enough: zip is equivalent to performing a join operation on the second collection to the first, where the attribute to join by is the index. Therefore Only items that exist in the first collection, if they have a match in the second, will appear in the result.

The next option is to think about left join which will return all items of the first collection with a match (if exists) in the second. In the case described OP is looking for the functionality of a full outer join - get all items of both collection and match when possible.

I know you asked for LINQ, but I do often feel that LINQ is a hammer and as soon as a developer finds it, every problem is a nail. I wouldn't have done this one with LINQ, for a readability/maintainability point of view because I think something like this is simpler and easier to understand/more self documenting:

List<int> r = new List<int>(10);
for(int i = 0; i < 10; i++){
  if(i < a.Count)
    r.Add(a[i]);
  if(i < b.Count)
    r.Add(b[i]);
}

You don't need to stop the loop early if a and b collectively only have eg 8 items, but you could by extending the test of the for loop

I also think this case may be more performant than LINQ because it's doing a lot less

If your mandate to use LINQ is academic (this is a homework that must use LINQ) then go ahead, but if it's a normal everyday system that some other poor sucker will have to maintain one day, I implore you to consider whether this is a good application for LINQ

This will handle 2 or more internal List<List<int>>'s - it returns an IEnumerable<int> via yield so you have to call .ToList() on it to make it a list. Linq.Any is used for the break criteria.

Will throw on any list being null. Add checks to your liking.

static IEnumerable<int> FlattenZip (List<List<int>> ienum, int maxLength = int.MaxValue)
{
  int done = 0;
  int index = 0;
  int yielded = 0;

  while (yielded <= maxLength && ienum.Any (list => index < list.Count))
    foreach (var l in ienum)
    {
      done++;

      if (index < l.Count)
      {
        // this list is big enough, we will take one out
        yielded++;
        yield return l[index];
      }

      if (yielded > maxLength)
        break; // we are done

      if (done % (ienum.Count) == 0)
        index += 1; // checked all lists, advancing index
    }
}

public static void Main ()
{
  // other testcases to consider: 
  //   in total too few elememts
  //   one list empty (but not null)
  //   too many lists (11 for 10 elements) 

  var l1 = new List<int> { 1, 2, 3, 4 };
  var l2 = new List<int> { 11, 12, 13, 14, 15, 16 };
  var l3 = new List<int> { 21, 22, 23, 24, 25, 26 };

  var l = new List<List<int>> { l1, l2, l3 };

  var zipped = FlattenZip (l, 10);

  Console.WriteLine (string.Join (", ", zipped));
  Console.ReadLine ();
}
Related