Reputation: 33
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
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
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