Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I flatten an array of arrays?

Tags:

arrays

c#

linq

I have an array consisting of following elements:

var schools = new [] {
    new object[]{ new[]{ "1","2" }, "3","4" },
    new object[]{ new[]{ "5","6" }, "7","8" },
    new object[]{ new[]{ "9","10","11" }, "12","13" }
};

The real object that i try to flatten is from importing data into array of arrays from CSV and then joining it on values of fields:

    var q =
        from c in list
        join p in vocatives on c.Line[name1].ToUpper() equals p.first_name.ToUpper() into ps
        from p in ps.DefaultIfEmpty()
        select new object[] { c.Line,  p == null ? "(No vocative)" : p.vocative, p == null ? "(No sex)" : p.sex }; 

I want to flatten that array of strings to get:

string[] {
    new string[]{ "1","2","3","4" },
    new string[]{ "5","6","7","8" },
    new string[]{ "9","10","11","12","13" }
}

I already have an solution that does that in loop, its not so performance-wise, but it seems to work ok.

I've tried to use SelectMany but cannot make up a solution.

Thank you very much for feedback ;) I've tried answer from npo:

var result = schools.Select(z => z.SelectMany(y=> y.GetType().IsArray 
           ? (object[])y : new object[] { y })
);

But CSVwriter class method accepts only explicitly typed:

IEnumerable<string[]>

So how to do it in linq, I've tried to:

List<string[]> listOflists = (List<string[]>)result;

But no go, InvalidCastException arrises, unfortunately.

like image 884
user2829330 Avatar asked Dec 13 '18 12:12

user2829330


2 Answers

In a first step, you have to normalize the data to one kind of type. Then you can iterate over them as you like. So at first create a method to flatten the values from a specific point to an arbitrary depth:

public static class Extensions
{
    public static IEnumerable<object> FlattenArrays(this IEnumerable source)
    {
        foreach (var item in source)
        {
            if (item is IEnumerable inner
                && !(item is string))
            {
                foreach (var innerItem in inner.FlattenArrays())
                {
                    yield return innerItem;
                }
            }

            yield return item;
        }
    }
}

Now you can either iterate on the top level to get a single array of all values:

// Produces one array => ["1", "2", "3", "4", ...]
var allFlat = schools.FlattenArrays().OfType<string>().ToArray();

Or you can create individual array one depth deeper:

foreach (var item in schools)
{
    // Produces an array for each top level e.g. ["5", "6", "7", "8"]
    var flat = item.FlattenArrays().OfType<string>().ToArray();
}
like image 116
Oliver Avatar answered Sep 28 '22 19:09

Oliver


As per the comments, because your inner array mixes elements of string[] and string, it likely won't be trivial to do this directly in Linq.

However, with the assistance of a helper function (I've called Flattener) you can branch the handling of both of the inner types manually to either return the elements in the array (if it's string[]), or to return the single element as an enumerable, if it's not. SelectMany can then be used to flatten the inner level, but the outer level seemingly you want to leave unflattened:

i.e.

var schools = new [] {
    new object[]{new[]{"1","2"}, "3","4"}, 
    new object[]{new[]{"5","6"}, "7","8"},
    new object[]{new[]{"9","10","11"}, "12","13"}
};

var result = schools
    .Select(s => s.SelectMany(o => Flattener(o)));

Which returns a type of IEnumerable<IEnumerable<string>>

Where the messy unpacking bit done by:

public IEnumerable<string> Flattener(object o)
{
    if (o is IEnumerable<string> strings)
    {
        return strings;
    }
    if (o is string s)
    {
       return new[]{s};
    }
    return new[]{"?"};
}

Note the above uses the pattern matching capabilities of C#7.

Result screenshot courtesy of LinqPad:

enter image description here

like image 37
StuartLC Avatar answered Sep 28 '22 19:09

StuartLC