user544111
user544111

Reputation: 33

transpose 2D array that exists as IEnumerable of IEnumerable

How do I do this in VB .NET? I tried using the linq Zip method on IEnumerable but it does not work for more than 2 arrays.

Here is an example in Python of what I am trying to do(I got p - nested IEnumerable - and need q - another nested IEnumerable):

>>> l=['a','b','c']
>>> m=[1,2,3]
>>> n=['x','y','z']
>>> p=[l,m,n]
>>> p
[['a', 'b', 'c'], [1, 2, 3], ['x', 'y', 'z']]
>>> q=zip(*p)
>>> q
[('a', 1, 'x'), ('b', 2, 'y'), ('c', 3, 'z')]

Upvotes: 2

Views: 1449

Answers (2)

Ahmad Mageed
Ahmad Mageed

Reputation: 96487

The .NET version of Zip won't handle an arbitrary number of arrays the way Python's appears to do. You'll need to call Zip twice:

Dim first As String() = { "a", "b", "c" }
Dim second As Integer() = { 1, 2, 3 }
Dim third As String() = { "x", "y", "z" }

Dim query = first.Zip(second, Function(f, s) New With { .First = f, .Second = s }) _
                 .Zip(third, Function(o, t) New With { o.First, o.Second, .Third = t })

For Each item in query
    Console.WriteLine("{0}, {1}, {2}", item.First, item.Second, item.Third)
Next

Another option would be to use the overloaded Enumerable.Select method that includes the index. This approach relies on the types you're working with allowing access by index. I wouldn't recommend substituting index access with the ElementAt method for performance purposes. Also, this approach assumes all collections have the same length, otherwise it will throw an exception. It would work as follows:

Dim query2 = first.Select(Function(f, i) New With { .First = f, .Second = second(i), .Third = third(i) })

EDIT: One thought is to leverage Python directly and call it from VB.NET. I'm not really sure how this would be handled, and there will be a learning curve to set it all up. Search for "call python from c#" or from "vb.net" for more on that topic.

The challenge is you can't dynamically create an anonymous type. The closest approach I came up with is to use .NET 4.0's ExpandoObject. To use C#'s dynamic keyword in VB.NET you should be able to initialize an object without specifying the type, such as Dim o = 5 since it's really an object underneath. You'll probably need to set Option Infer On and Option Strict Off to achieve that.

The following code expects arrays as input. Unfortunately mixing dynamic types and other IEnumerable<T>s becomes challenging when attempting to access the Count. Jon Skeet has a relevant article about it here: Gotchas in dynamic typing. For that reason I stuck with arrays; it can be changed to List<T> to use the Count property, but definitely not a mixture without a lot of work.

VB.NET

Dim first As String() = { "a", "b", "c" }
Dim second As Integer() = { 1, 2, 3 }
Dim third As String() = { "x", "y", "z" }
Dim fourth As Boolean() = { true, false, true }

Dim list As New List(Of Object) From { first, second, third, fourth }
' ensure the arrays all have the same length '
Dim isValidLength = list.All(Function(c) c.Length = list(0).Length)
If isValidLength
    Dim result As New List(Of ExpandoObject)()
    For i As Integer = 0 To list(i).Length - 1
        Dim temp As New ExpandoObject()
        For j As Integer = 0 To list.Count - 1
            CType(temp, IDictionary(Of string, Object)).Add("Property" + j.ToString(), list(j)(i))
        Next
        result.Add(temp)
    Next

    ' loop over as IDictionary '
    For Each o As ExpandoObject In result
        For Each p in CType(o, IDictionary(Of string, Object))
            Console.WriteLine("{0} : {1}", p.Key, p.Value)
        Next
        Console.WriteLine()
    Next    

    ' or access via property '
    For Each o As Object In result
        Console.WriteLine(o.Property0)
        Console.WriteLine(o.Property1)
        Console.WriteLine(o.Property2)
        Console.WriteLine(o.Property3)
        Console.WriteLine()
    Next
End If

C# equivalent (for anyone interested)

string[] first = { "a", "b", "c" };
int[] second = { 1, 2, 3 };
string[] third = { "x", "y", "z" };
bool[] fourth = { true, false, true };

var list = new List<dynamic> { first, second, third, fourth };
bool isValidLength = list.All(l => l.Length == list[0].Length);
if (isValidLength)
{
    var result = new List<ExpandoObject>();
    for (int i = 0; i < list[i].Length; i++)
    {
        dynamic temp = new ExpandoObject();
        for (int j = 0; j < list.Count; j++)
        {
            ((IDictionary<string, object>)temp).Add("Property" + j, list[j][i]);
        }
        result.Add(temp);
    }

    // loop over as IDictionary
    foreach (ExpandoObject o in result)
    {
        foreach (var p in (IDictionary<string, object>)o)
            Console.WriteLine("{0} : {1}", p.Key, p.Value);

        Console.WriteLine();
    }

    // or access property via dynamic
    foreach (dynamic o in result)
    {
        Console.WriteLine(o.Property0);
        Console.WriteLine(o.Property1);
        Console.WriteLine(o.Property2);
        Console.WriteLine(o.Property3);
        Console.WriteLine();
    }
}

Upvotes: 2

Gideon Engelberth
Gideon Engelberth

Reputation: 6155

If you have a specific number of IEnumerables you want to support, you could return some sort of Tuple or similar structure (like with Ahmad Mageeds answer). For the general case, you will have to do some sort of caching and you will end up with only one type of item in all enumerables. Something like this:

Public Function Transpose(Of T)(ByVal source As IEnumerable(Of IEnumerable(Of T))) As IEnumerable(Of IEnumerable(Of T))
    If source is Nothing then Throw New ArgumentNullException("source")
    Return New TransposeEnumerable(Of T)(source)
End Function

Friend NotInheritable Class TransposeEnumerable(Of T)
    Implements IEnumerable(Of IEnumerable(Of T))

    Public Sub New(ByVal base As IEnumerable(Of IEnumerable(Of T)))
        _base = base
    End Sub

    Private ReadOnly _base As IEnumerable(Of IEnumerable(Of T))

    Public Function GetEnumerator() As System.Collections.Generic.IEnumerator(Of IEnumerable(Of T)) Implements System.Collections.Generic.IEnumerable(Of IEnumerable(Of T)).GetEnumerator
        Return New TransposeEnumerator(Me)
    End Function

    Private Function GetObjectEnumerator() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
        Return Me.GetEnumerator()
    End Function

    Private NotInheritable Class TransposeEnumerator
        Implements IEnumerator(Of IEnumerable(Of T))

        Public Sub New(ByVal owner As TransposeEnumerable(Of T))
            _owner = owner
            _sources = owner.Select(Function(e) e.GetEnumerator()).ToList()
        End Sub

        Private disposedValue As Boolean
        Public Sub Dispose() Implements IDisposable.Dispose
            If Not Me.disposedValue Then
                If _sources IsNot Nothing Then
                    For Each e In _sources
                        If e IsNot Nothing Then e.Dispose()
                    Next
                End If
            End If
            Me.disposedValue = True
        End Sub

        Private ReadOnly _owner As TransposeEnumerable(Of T)
        Private _sources As New List(Of IEnumerator(Of T))
        Private _current As IEnumerable(Of T)

        Public ReadOnly Property Current() As IEnumerable(Of T) Implements System.Collections.Generic.IEnumerator(Of IEnumerable(Of T)).Current
            Get
                Return _current
            End Get
        End Property

        Private ReadOnly Property CurrentObject() As Object Implements System.Collections.IEnumerator.Current
            Get
                Return Me.Current
            End Get
        End Property

        Public Function MoveNext() As Boolean Implements System.Collections.IEnumerator.MoveNext
            Dim success As Boolean = _sources.All(Function(s) s.MoveNext())
            If success Then
                _current = _sources.Select(Function(s) s.Current).ToList().AsEnumerable()
            End If
            Return success
        End Function

        Public Sub Reset() Implements System.Collections.IEnumerator.Reset
            Throw New InvalidOperationException("This enumerator does not support resetting.")
        End Sub

    End Class
End Class

Upvotes: 0

Related Questions