1

I am wondering if two int arrays can theoretically be merged into a single array of structures using only LINQ, where each array going into its respective field?

So let's say we have 2 int arrays called numbers_raw and categories_raw, and class Record with 2 fields.

Using procedural code it looks like this:

class Record {
    public int number;
    public int category;
}
int[] numbers_raw =    { 0, 1, 2, 3, 4, 5, 6, 7, 8, 14, -4 };
int[] categories_raw = { 0, 0, 0, 0, 1, 1, 1, 1, 2,  2,  2 };
var records = new List<Record>();
for (int ii = 0; ii < numbers_raw.Length; ii++) {
    records.Add(new Record { number = numbers_raw[ii], category = categories_raw[ii] });
}

And if we had only 1 array and 1 field we could do it like this:

var records = numbers_raw.Select(x => new Record { number = x });

But I am not sure how to do it in such a way that both arrays are used, categories_raw going into the category field and numbers_raw goes into number field.

I am not sure if it is at all possible using LINQ.

2
  • Did you attempt to use Enumerable.Zip() ? Commented Jun 1, 2021 at 12:33
  • I didn't know about it. Thank you. Commented Jun 1, 2021 at 12:37

5 Answers 5

3

Enumerable.Zip is what you need:

List<Record> records = numbers_raw
    .Zip(categories_raw, (n, c) => new Record { number = n, category = c})
    .ToList();

If one of both collections is larger, Zip takes all items until the common maximum count. For example, if one sequence has three elements and the other one has four, the result sequence will have only three elements.

This makes me wonder if there is a generic solution for any number of arrays but I assume there isn't.

You're right, there is no ZipAll and it's not easy to build one. If you have 3 or 4 you could still use Zip, just concatenate them:

List<Record> records = numbers_raw
    .Zip(categories_raw, (first, second) => (first, second))
    .Zip(thirdSeq, (both, third) => new Record { number = both.first, category = both.second, thirdProperty = third})
    .ToList();

or provide an extension method (this exists already with C#6 but only for 3):

public static class EnumerableExtensions
{
    public static IEnumerable<TResult> Zip<TFirst, TSecond, TThird, TResult>(
        this IEnumerable<TFirst> first, 
        IEnumerable<TSecond> second, 
        IEnumerable<TThird> third, 
        Func<TFirst, TSecond, 
        TThird, TResult> resultSelector)
    {
        using (IEnumerator<TFirst> e1 = first.GetEnumerator())
        using (IEnumerator<TSecond> e2 = second.GetEnumerator())
        using (IEnumerator<TThird> e3 = third.GetEnumerator())
            while (e1.MoveNext() && e2.MoveNext() && e3.MoveNext())
                yield return resultSelector(e1.Current, e2.Current, e3.Current);
    }
}

But these approches work just for a couple of sequences.

Sign up to request clarification or add additional context in comments.

3 Comments

Thank you. This makes me wonder if there is a generic solution for any number of arrays but I assume there isn't. Also this action of unifying separate "channels" of information into fields of a single array must have a name but I couldn't figure out what it is so I used the word "merge".
@sashoalm: No, there is no easy or available generic solution to make Zip work with any number of collections. But look at my answer, i have edited it to provide a different way.
@sashoalm Side note: With .NET 6 will be possible to do Zip with 3 collections. For more, you still would have to create your own method.
2

Yes, you can use Zip:

var records = numbers_raw.Zip(
        categories_raw, 
        (x, y) => new Record {number = x, category = y})
    .ToList();

Comments

1

You can use another version of .Select(element, index)

var records = numbers_raw.Select((x, index) => 
           new Record { 
              number = numbers_raw[index], 
              category= categories_raw[index]
           }).ToList();

Note: Here .Zip() is appropriate than .Select() version, I just shared alternate way to .Zip()


Why I said .Zip() is better than Select()here?

  • As @TimSchmelter said in his answer, Zip() takes all items until the common maximum count, but Select(element, index) will work only in case of numbers_raw.Count() > categories_raw.Count(). It will throw an Array out of bound exception if this condition failed.
  • If you desperate to use this solution, then always apply .Select() on the list which contains maximum elements.

Comments

1

Update:

I didn't know Zip actually comes with the same implementation, so I reworked the extension method slightly. As I got the problem with regular Select that it knows nothing about the index, so we can actually workaround this with the following extension:

public static IEnumerable<TResult> SelectByIndex<T1, TResult>(this IEnumerable<T1> sequence, Func<int, TResult> selector)
{
    var baseArray = sequence.ToArray();
    for (int i = 0; i < baseArray.Length; i++)
    {
        TResult result;
        try
        {
            result = selector(i);
        }
        catch (IndexOutOfRangeException)
        {
            yield break;
        }

        yield return result;
    }
}

And then use it like this:

var result = numbers_int.SelectByIndex(i => new Record {number = numbers_int[i], category = categories_raw[i]});

Old answer:

While Zip is good for the simple solution, you can write your own with the LINQ extension methods.

Let's assume your record class has different types of fields:

class Record
{
    public int number;
    public string category;
}

Then you can define your own LINQ expression for such case:

public static class LinqExtensions
{
    public static IEnumerable<TResult> Merge<T1, T2, TResult>(this IEnumerable<T1> sequence, IEnumerable<T2> other, Func<T1, T2, TResult> selector)
    {
        using var sequenceEnumerator = sequence.GetEnumerator();
        using var otherEnumerator = other.GetEnumerator();
        while (sequenceEnumerator.MoveNext() && otherEnumerator.MoveNext())
        {
            yield return selector(sequenceEnumerator.Current, otherEnumerator.Current);
        }
    }
}

And use it like this:

int[] numbers_int = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 14, -4 };
string[] categories_str = { "0", "0", "0", "0", "1", "1", "1", "1", "2", "2", "2"};

var result = numbers_int.Merge(categories_str, (i, s) => new Record {number = i, category = s});

This will give you the flexibility of defining your own generic methods for any situtation.

2 Comments

Which is basically the implementation of Enumerable.Zip. Of course you can extend it to take three sequences or four, but not any number of collections.
Thanks, @TimSchmelter didn't know that, updated the answer. Hopefully, someone might find it useful.
0
int[] numbers_raw =    { 0, 1, 2, 3, 4, 5, 6, 7, 8, 14, -4 };
int[] categories_raw = { 0, 0, 0, 0, 1, 1, 1, 1, 2,  2,  2 };

var records = numbers_raw.Select(delegate(int value, int index)
{
      return new Record() {category = categories_raw[index], number = numbers_raw[index]};
});

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.