SuperJMN
SuperJMN

Reputation: 13972

Is there a LINQ operator to do this?

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:

  1. Transpose the matrix (rotate it)
  2. It should push every "cell" in the matrix, returning the corresponding item, or default if the cell is empty.

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

NOTICE

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

Answers (3)

Flydog57
Flydog57

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

Gert Arnold
Gert Arnold

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

Serge
Serge

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

Related Questions