16

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?

Michael0x2a
  • 58,192
  • 30
  • 175
  • 224
  • Why do you want to solve it specifically using LINQ? – Moo-Juice Jul 31 '13 at 17:21
  • @Moo-Juice -- good point, I edited my title. I think I just automatically assumed that the solution would use linq in some way, but I suppose that's not necessarily the case. – Michael0x2a Jul 31 '13 at 17:29
  • 3
    Your example code is very inefficient if the sequences are not random access. – Eric Lippert Jul 31 '13 at 17:35
  • 2
    I gave a general answer to another question which was more restricted which utilizes LINQ [here](http://stackoverflow.com/a/5039863/390278) and is a more direct answer to your question. – Jeff Mercado Jul 31 '13 at 17:44
  • Possible duplicate of [Create Items from 3 collections using Linq](http://stackoverflow.com/questions/5284315/create-items-from-3-collections-using-linq) – Robert Synoradzki Jul 11 '16 at 10:56
  • 1
    @ensisNoctis -- not quite. That post contains answers that can handle only exactly three collections, this post is asking for code that can handle an arbitrary number of collections. – Michael0x2a Jul 11 '16 at 14:14

7 Answers7

18

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();
   }
}
Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • That must have taken ages to type on a phone... :) – Moo-Juice Jul 31 '13 at 17:49
  • I went and cleaned it up for ya. I believe it captures your intent. – Jeff Mercado Jul 31 '13 at 17:55
  • @JeffMercado Your beat me to it by literally 5 seconds. I had pretty much the same code (which I tested and it worked). I think it's a good edit. – Simon Belanger Jul 31 '13 at 17:56
  • @JeffMercado: didn't realize you'd done it too...oh well we all did the same thing. @Eric, I added comments detailing why `ToList` (or `ToArray`) would be important. – user7116 Jul 31 '13 at 17:57
  • Would the enumerators be disposed of when this method hits the yield break? The question stems from this answer: http://stackoverflow.com/questions/47521/using-yield-to-iterate-over-a-datareader-might-not-close-the-connection/47799#47799 – Oliver Jan 24 '14 at 15:45
  • @Oliver: this is a question-and-answer site. Post a question if you have a question! – Eric Lippert Jan 24 '14 at 15:45
  • @EricLippert Noted! http://stackoverflow.com/questions/21337340/enumerator-disposal-when-not-using-using-or-manually-calling-dispose – Oliver Jan 24 '14 at 16:14
  • 1
    This method would leave un-disposed enumerators. – Oliver Jan 24 '14 at 16:59
  • 1
    @Oliver: Ah, now I understand the thrust of your question. You are absolutely right; I neglected to dispose the enumerators. I'll fix it. – Eric Lippert Jan 24 '14 at 17:31
  • Note here that I am making some assumptions in the fix. For example, if an exception happens while selecting out the `GetEnumerator()` calls, enumerators that have already been created are not disposed. If a `Dispose()` throws an exception then the remaining enumerators are not disposed. Enumerators are presumed to be non null. And so on. If these conditions are violated then the program has a more serious bug in it already than a few leaked resources! – Eric Lippert Jan 24 '14 at 17:37
  • One improvement I made to this: if enumerators is empty there should be a yield break; – Eric Roller Sep 21 '17 at 13:03
  • Otherwise you are invoking the selector to return a `TResult` from an empty `IEnumerable` which is not always possible (e.g. if `TResult` is not an `IEnumerable`) – Eric Roller Sep 21 '17 at 13:09
12

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.

Simon Belanger
  • 14,752
  • 3
  • 41
  • 35
5

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

user7116
  • 63,008
  • 17
  • 141
  • 172
3

Here's an inefficient variant based on Matrix Transposition:

public static class Ext
{
    public static IEnumerable<IEnumerable<T>> Rotate<T>(
        this IEnumerable<IEnumerable<T>> src)
    {
        var matrix = src.Select(subset => subset.ToArray()).ToArray();
        var height = matrix.Length;
        var width = matrix.Max(arr => arr.Length);

        T[][] transpose = Enumerable
            .Range(0, width)
            .Select(_ => new T[height]).ToArray();
        for(int i=0; i<height; i++)
        {        
            for(int j=0; j<width; j++)
            {            
                transpose[j][i] = matrix[i][j];            
            }
        }

        return transpose;
    }
}
JerKimball
  • 16,584
  • 3
  • 43
  • 55
2

Take a look at the linqlib project on codeplex, it has a rotate function that does exactly what you need.

1

You can condense for loops using Range:

var result = Enumerable.Range(0, myList.Min(l => l.Count))
    .Select(i => myList.Select(l => l[i]).ToList()).ToList();
nmclean
  • 7,564
  • 2
  • 28
  • 37
0
(from count in Range(myList[0].Count)
select new List<string>(
    from count2 in Range(myList.Count)
    select myList[count2][count])
    ).ToList();

It ain't pretty, but I think it'll work.

Nate Diamond
  • 5,525
  • 2
  • 31
  • 57
  • I think this solution works only if there are three lists inside `mylist`. I was hoping more for a solution which works no matter how many lists there are. – Michael0x2a Jul 31 '13 at 17:36
  • Okay, I *think* my edit makes that happen. Not 100% sure, but the basic idea is there. – Nate Diamond Jul 31 '13 at 17:39