How to "zip" or "rotate" a variable number of lists?

If I have a list containing an arbitrary number of lists, like so:

var myList = new List<List<string>>()
{
    new List<string>() { "a", "b", "c", "d" },
    new List<string>() { "1", "2", "3", "4" },
    new List<string>() { "w", "x", "y", "z" },
    // ...etc...
};

...is there any way to somehow "zip" or "rotate" the lists into something like this?

{ 
    { "a", "1", "w", ... },
    { "b", "2", "x", ... },
    { "c", "3", "y", ... },
    { "d", "4", "z", ... }
}

The obvious solution would be to do something like this:

public static IEnumerable<IEnumerable<T>> Rotate<T>(this IEnumerable<IEnumerable<T>> list)
{
    for (int i = 0; i < list.Min(x => x.Count()); i++)
    {
        yield return list.Select(x => x.ElementAt(i));
    }
}

// snip

var newList = myList.Rotate();

...but I was wondering if there was a cleaner way of doing so, using linq or otherwise?


Solution 1:

You can roll your own ZipMany instance which manually iterates each of the enumerations. This will likely perform better on larger sequences than those using GroupBy after projecting each sequence:

public static IEnumerable<TResult> ZipMany<TSource, TResult>(
    IEnumerable<IEnumerable<TSource>> source,
    Func<IEnumerable<TSource>, TResult> selector)
{
   // ToList is necessary to avoid deferred execution
   var enumerators = source.Select(seq => seq.GetEnumerator()).ToList();
   try
   {
     while (true)
     {
       foreach (var e in enumerators)
       {
           bool b = e.MoveNext();
           if (!b) yield break;
       }
       // Again, ToList (or ToArray) is necessary to avoid deferred execution
       yield return selector(enumerators.Select(e => e.Current).ToList());
     }
   }
   finally
   {
       foreach (var e in enumerators) 
         e.Dispose();
   }
}

Solution 2:

You can do this by using the Select extension taking a Func<T, int, TOut>:

var rotatedList = myList.Select(inner => inner.Select((s, i) => new {s, i}))
                        .SelectMany(a => a)
                        .GroupBy(a => a.i, a => a.s)
                        .Select(a => a.ToList()).ToList();

This will give you another List<List<string>>.

Breakdown

.Select(inner => inner.Select((s, i) => new {s, i}))

For each inner list, we project the list's content to a new anonymous object with two properties: s, the string value, and i the index of that value in the original list.

.SelectMany(a => a)

We flatten the result to a single list

.GroupBy(a => a.i, a => a.s)

We group by the i property of our anonymous object (recall this is the index) and select the s property as our values (the string only).

.Select(a => a.ToList()).ToList();

For each groups, we changed the enumerable to a list and another list for all the groups.

Solution 3:

How about using SelectMany and GroupBy with some indexes?

// 1. Project inner lists to a single list (SelectMany)
// 2. Use "GroupBy" to aggregate the item's based on order in the lists
// 3. Strip away any ordering key in the final answer
var query = myList.SelectMany(
    xl => xl.Select((vv,ii) => new { Idx = ii, Value = vv }))
       .GroupBy(xx => xx.Idx)
       .OrderBy(gg => gg.Key)
       .Select(gg => gg.Select(xx => xx.Value));

From LinqPad:

we groupa da items