2

I have seen solutions like this to zip at compile time a known number of Lists greater than two Lists:

public static class MyFunkyExtensions
{
    public static IEnumerable<TResult> ZipThree<T1, T2, T3, TResult>(
        this IEnumerable<T1> source,
        IEnumerable<T2> second,
        IEnumerable<T3> third,
        Func<T1, T2, T3, TResult> func)
    {
        using (var e1 = source.GetEnumerator())
        using (var e2 = second.GetEnumerator())
        using (var e3 = third.GetEnumerator())
        {
            while (e1.MoveNext() && e2.MoveNext() && e3.MoveNext())
                yield return func(e1.Current, e2.Current, e3.Current);
        }
    }
}

What is the correct code if you have a List<List<>> and you want to dynamically zip them? NOTE that the number of Lists is unknown at compile time. I don't want to have to create a ZipFour, ZipFive etc...

Ry-
  • 218,210
  • 55
  • 464
  • 476
Ivan
  • 7,448
  • 14
  • 69
  • 134

2 Answers2

1

Something like this?

public static IEnumerable<TResult> ZipAll<T, TResult>(
    IEnumerable<IEnumerable<T>> lists,
    Func<IEnumerable<T>, TResult> func)
{
    var enumerators = lists.Select(l => l.GetEnumerator()).ToArray();
    while(enumerators.All(e => e.MoveNext()))
    {   
        yield return func(enumerators.Select(e => e.Current));
    }

    foreach (var enumerator in enumerators)
    {
        enumerator.Dispose();
    }
}

This assumes the type argument of each list/enumerable is the same (i.e. you want to call this on something like List<List<int>>. If that's not the case, you'd need to use the non-generic IEnumerable instead (and get rid of the foreach at the end, since the non-generic IEnumerable is not disposable).

I haven't tested this heavily; interested to see what kind of holes commenters may poke in it.

EDIT:

  • As MineR calls out, this implementation doesn't capture the effect of the using statements in your sample implementation. There are a few different ways you could modify this to use a try/finally (or multiple try/finallys) depending on exactly how you would want to handle exceptions that could occur within GetEnumerator, MoveNext, Current and Dispose.
  • Also, while you can zip enumerables of infinite length, Zip conceptually requires a finite number of those enumerables. It would probably be more correct if lists were of type ICollection<IEnumerable<T>>. This will throw an OutOfMemory exception if lists is infinite.
  • After some discussion: one specific requirement was to be able to use indexers in the selector function. This can be achieved by making the third parameter Func<IList<T>, TResult> instead of Func<IEnumerable<T>, TResult>, and adding a ToArray() to enumerators.Select(e => e.Current).
nlawalker
  • 6,364
  • 6
  • 29
  • 46
  • 1
    Maybe a try/finally, but yeah. – MineR Apr 16 '19 at 03:10
  • Definitely; I was basically aiming to extend the sample code and all of its behavior (for bad or good). – nlawalker Apr 16 '19 at 03:11
  • Ah, and now i see that a try/finally would be needed to get the effect of the `using`s. – nlawalker Apr 16 '19 at 14:11
  • nlawalker this is close however, Func needs to take the same number of parameters as there are lists. It has to be Func if there are three lists, four, etc. The lambda has to be created dynamically too. I guess it could be If T were a dynamically created Tuple<...> – Ivan Apr 16 '19 at 15:22
  • The lambda cannot take the same number of parameters as there are lists if the number of lists is unknown at compile time. – nlawalker Apr 16 '19 at 15:27
  • Then how does the lambda (func in your case) know how to operate on the IEnumerable> ? – Ivan Apr 16 '19 at 15:29
  • Say for example, the func was to take lists[0].Value / lists[1].Value * lists[2].Value. How does that get passed into ZipAll? And what is the general case when the number of lists is uknown (but the operations between each list __is__ known)? – Ivan Apr 16 '19 at 15:32
  • As I said the lambda is compiled dynamically from a string that I build by hand. That is not the hard part. – Ivan Apr 16 '19 at 15:36
  • I think func has to be a delegate like this: public delegate void func(params object[] arguments) – Ivan Apr 16 '19 at 15:39
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/191925/discussion-between-nlawalker-and-ivan). – nlawalker Apr 16 '19 at 15:40
0

Without knowing exactly how you want to Zip your Lists it's hard to write an exact solution but you could do something like the following:

List<int> ListOne = new List<int> { 1, 2, 3, 4, 5 };
List<int> ListTwo = new List<int> { 123, 12, 3243, 223 };
List<int> ListThree = new List<int> { 23, 23, 23, 23, 23, 23 };

var AllLists = new List<List<int>> { ListOne, ListTwo, ListThree };

var result = AllLists.Aggregate((acc, val) => acc.Zip(val, (init, each) => init + each).ToList());

The function you pass to the Zip function will be however you want to process the Zipping obviously, in this case the ints are simply being added together, but strings could be concatenated etc. The Aggregate function will serve to enumerate over all Lists though and combine the results into a single container, in this case a List.

Mike Weiss
  • 309
  • 3
  • 11