Reputation: 13972
I would like to know if there's a LINQ operator to do this:
var one = new[] { "A", "B", "C" };
var two = new[] { "A", "B", "C", "D" };
var combined = new [] { one, two };
var result = Operator(combined);
Console.WriteLine(result.Should().BeEquivalentTo(new [] { "A", "A", "B", "B", "C", "C", null, "D" }));
If short, it should act like if every sequence was a row in a matrix. And after that, it should:
I mean, graphically:
A, B, C
A, B, C, D
<Transpose>
A, A
B, B
C, C
D
result => A, A, B, B, C, C, D, null
Operator
should work on IEnumerable<IEnumerable<T>>
As you can see, the Operator
I'm interested in, uses combined
, so it accepts should IEnumerable<IEnumerable<T>>
(like SelectMany
).
Upvotes: 0
Views: 176
Reputation: 7111
It's a bit hard keeping up with your changing specs. Originally, it was a pair of string arrays. I changed that to be a pair of arrays of T
in my answer.
Then you wrote in a comment that "oh, no, I meant N sequences". Finally, after reading that, I noticed that you'd updated your question to ask about N collections expressed as IEnumerable<T>
.
In the mean time, I pointed out that my original answer would work well for N arrays with minimal change. So, here goes:
For N Arrays
I use the params
keyword to remove the need for your combined variable. The params
keyword will compose some or all of the parameters of a method into an array.
Here's a method that can take N arrays:
public static IEnumerable<T> KnitArrays<T>(params T[][] arrays)
{
var maxLen = (from array in arrays select array.Length).Max();
for (var i = 0; i < maxLen; i++)
{
foreach( var array in arrays)
{
yield return array.Length > i ? array[i] : default(T);
}
}
}
It's pretty much the same logic as the original answer. The test code looks the same as well:
var one = new[] { "A1", "B1", "C1" };
var two = new[] { "A2", "B2", "C2", "D2" };
var three = new[] { "A3", "B3" };
var knittedArray = KnitArrays(one, two, three);
List<string> result = knittedArray.ToList();
WriteCollectionContents(result);
Where WriteCollectionContents
spits out the contents of the collection. In this case:
"A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", null, null, "D2", null,
For N Lists and/or Arrays
It turns out that the same basic code can work with IList<T>
, i.e., for both List<T>
and T[]
:
public static IEnumerable<T> KnitILists<T>(params IList<T>[] ilists)
{
var maxLen = (from ilist in ilists select ilist.Count).Max();
for (var i = 0; i < maxLen; i++)
{
foreach (var ilist in ilists)
{
yield return ilist.Count > i ? ilist[i] : default(T);
}
}
}
The test code for this also looks pretty similar - though note the mix or arrays and lists:
var one = new[] { "A1", "B1", "C1" };
var two = new[] { "A2", "B2", "C2", "D2" };
var list3 = new List<string> { "A3", "B3" };
var knittedLists = KnitILists(one, two, list3);
result = knittedLists.ToList();
WriteCollectionContents(result);
With exactly the same result:
"A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", null, null, "D2", null,
The reason it works with IList<T>
is that that interface has a Count
property and an Item
indexer. If you go to ICollection<T>
the Count
property stays, but you lose the Item
indexer.
Once you get to IEnumerable<T>
, both the Count
property and the Item
indexer are gone. The only thing you can do with an IEnumerable
is to iterate through it. As a result, the logic needs to be very different.
I might get around to coming up with a solution. However, it will likely look very similar to @gertarnold's answer.
I'm looking foreword to your upcoming comment about how you really meant for this to work with multi-dimensional arrays as well.
Original answer follows
How about something like this:
public static IEnumerable<T> KnitArrays<T>(T[] first, T[] second)
{
var maxLen = Math.Max(first.Length, second.Length);
for (var i = 0; i < maxLen; i++)
{
yield return first.Length > i ? first[i] : default(T);
yield return second.Length > i ? second[i] : default(T);
}
}
Testing this with:
var one = new[] { "A", "B", "C" };
var two = new[] { "A", "B", "C", "D" };
var knittedArray = KnitArrays(one, two);
List<string> result = knittedArray.ToList();
yields a list that looks like what you are asking. Note that I just return a non-materialized IEnumerable since you were asking about LINQ.
Upvotes: 3
Reputation: 109099
To make the result independent of the number of arrays the function should loop trough all arrays and keep returning until all enumerations are exhausted:
public static IEnumerable<IEnumerable<T>> Transpose<T>(this IEnumerable<IEnumerable<T>> source)
{
var enumerators = source.Select(e => e.GetEnumerator()).ToArray();
try
{
var next = false;
do
{
var results = enumerators.Select(enr =>
{
if (enr.MoveNext())
{
return enr.Current;
}
return default;
}).ToList();
next = results.Any(e => !Equals(default, e));
if (next)
{
yield return results;
}
}
while (next);
}
finally
{
Array.ForEach(enumerators, e => e.Dispose());
}
}
Now you can use any number of arrays:
var one = new[] { "A", "B", "C" };
var two = new[] { "A", "B", "C", "D" };
var three = new[] { "U", "V","W", "X", "Y", "Z" };
var combined = new[] { one, three, two };
var result = combined.Transpose().SelectMany(e => e).ToList();
This will result in
"A","U","A","B","V","B","C","W","C",null,"X","D",null,"Y",null,null,"Z",null
Filtering the null values is trivial.
(Courtesy this answer for the basic idea, but only working for arrays of equal length).
Upvotes: 0
Reputation: 43870
try this
var result = Enumerable.Range(0, Math.Max(one.Count(), two.Count()))
.SelectMany(n => new[] { one.ElementAtOrDefault(n), two.ElementAtOrDefault(n) });
result
["A","A","B","B","C","C",null,"D"]
Upvotes: 0