1
\$\begingroup\$

This is a follow-up question for ConvertAll Methods Implementation for Multidimensional Array in C# and ConvertAll Methods Implementation for Multidimensional Array in C# - follow-up. Besides the multidimensional array ([,], [,,], [,,,]...) case, I am trying to implement another series overloading methods to deal with jagged arrays in C#.

The experimental implementation

The experimental implementation is as below.

class Converters
{
    public static TOutput[][] ConvertAll<TInput, TOutput>(TInput[][] inputs, Converter<TInput, TOutput> converter)
    {
        if (inputs is null)
        {
            throw new ArgumentNullException(nameof(inputs));
        }

        if (converter is null)
        {
            throw new ArgumentNullException(nameof(converter));
        }

        TOutput[][] output = Array.ConvertAll(inputs, dim1 => Array.ConvertAll(dim1, dim2 => converter(dim2)));
        return output;
    }

    public static TOutput[][][] ConvertAll<TInput, TOutput>(TInput[][][] inputs, Converter<TInput, TOutput> converter)
    {
        if (inputs is null)
        {
            throw new ArgumentNullException(nameof(inputs));
        }

        if (converter is null)
        {
            throw new ArgumentNullException(nameof(converter));
        }

        TOutput[][][] output = Array.ConvertAll(inputs, dim1 => 
                               Array.ConvertAll(dim1, dim2 => 
                               Array.ConvertAll(dim2, dim3 => converter(dim3)
                               )));
        return output;
    }

    public static TOutput[][][][] ConvertAll<TInput, TOutput>(TInput[][][][] inputs, Converter<TInput, TOutput> converter)
    {
        if (inputs is null)
        {
            throw new ArgumentNullException(nameof(inputs));
        }

        if (converter is null)
        {
            throw new ArgumentNullException(nameof(converter));
        }

        TOutput[][][][] output = Array.ConvertAll(inputs, dim1 =>
                                 Array.ConvertAll(dim1, dim2 =>
                                 Array.ConvertAll(dim2, dim3 => 
                                 Array.ConvertAll(dim3, dim4 => converter(dim4)
                                 ))));
        return output;
    }

    public static TOutput[][][][][] ConvertAll<TInput, TOutput>(TInput[][][][][] inputs, Converter<TInput, TOutput> converter)
    {
        if (inputs is null)
        {
            throw new ArgumentNullException(nameof(inputs));
        }

        if (converter is null)
        {
            throw new ArgumentNullException(nameof(converter));
        }

        TOutput[][][][][] output = Array.ConvertAll(inputs, dim1 =>
                                   Array.ConvertAll(dim1, dim2 =>
                                   Array.ConvertAll(dim2, dim3 =>
                                   Array.ConvertAll(dim3, dim4 => 
                                   Array.ConvertAll(dim4, dim5 => converter(dim5)
                                   )))));
        return output;
    }

    public static TOutput[][][][][][] ConvertAll<TInput, TOutput>(TInput[][][][][][] inputs, Converter<TInput, TOutput> converter)
    {
        if (inputs is null)
        {
            throw new ArgumentNullException(nameof(inputs));
        }

        if (converter is null)
        {
            throw new ArgumentNullException(nameof(converter));
        }

        TOutput[][][][][][] output = Array.ConvertAll(inputs, dim1 =>
                                     Array.ConvertAll(dim1, dim2 =>
                                     Array.ConvertAll(dim2, dim3 =>
                                     Array.ConvertAll(dim3, dim4 =>
                                     Array.ConvertAll(dim4, dim5 => 
                                     Array.ConvertAll(dim5, dim6 => converter(dim6)
                                     ))))));
        return output;
    }
}

Test cases

The test cases listed here include array of arrays, array of arrays of arrays and array of arrays of arrays of arrays.

//    Reference: https://www.geeksforgeeks.org/c-sharp-jagged-arrays/

Console.WriteLine("Jagged Array [][]");
Console.WriteLine();

int[][] jagged_arr = new int[][] 
{
    new int[] {1, 2, 3, 4},
    new int[] {11, 34, 67},
    new int[] {89, 23},
    new int[] {0, 45, 78, 53, 99}
};

double[][] output1 = Converters.ConvertAll(jagged_arr, x => x + 0.1);

for (int dim1 = 0; dim1 < output1.Length; dim1++)
{
    for (int dim2 = 0; dim2 < output1[dim1].GetLength(0); dim2++)
    {
        Console.Write($"[{dim1}][{dim2}]: {output1[dim1][dim2]}\t");
    }
    Console.WriteLine();
}

Console.WriteLine();
Console.WriteLine("Jagged Array [][][]");
Console.WriteLine();

int[][][] jagged_arr2 = new int[][][]
{
    jagged_arr,
    jagged_arr,
    jagged_arr,
    jagged_arr
};

double[][][] output2 = Converters.ConvertAll(jagged_arr2, x => x + 0.1);

for (int dim1 = 0; dim1 < output2.Length; dim1++)
{
    Console.WriteLine($"dim1 = {dim1}");
    for (int dim2 = 0; dim2 < output2[dim1].GetLength(0); dim2++)
    {
        for (int dim3 = 0; dim3 < output2[dim1][dim2].GetLength(0); dim3++)
        {
            Console.Write($"[{dim2}][{dim3}]: {output2[dim1][dim2][dim3]}\t");
        }
        Console.WriteLine();
    }
    Console.WriteLine();
}

Console.WriteLine();
Console.WriteLine("Jagged Array [][][][]");
Console.WriteLine();

int[][][][] jagged_arr3 = new int[][][][]
{
    jagged_arr2,
    jagged_arr2,
    jagged_arr2,
    jagged_arr2
};

double[][][][] output3 = Converters.ConvertAll(jagged_arr3, x => x + 0.1);

for (int dim1 = 0; dim1 < output3.Length; dim1++)
{
    for (int dim2 = 0; dim2 < output3[dim1].GetLength(0); dim2++)
    {
        Console.WriteLine($"dim1 = {dim1}, dim2 = {dim2}");
        for (int dim3 = 0; dim3 < output3[dim1][dim2].GetLength(0); dim3++)
        {
            for (int dim4 = 0; dim4 < output3[dim1][dim2][dim3].GetLength(0); dim4++)
            {
                Console.Write($"[{dim3}][{dim4}]: {output3[dim1][dim2][dim3][dim4]}\t");
            }
            Console.WriteLine();
        }
        Console.WriteLine();
    }
}

The output of the test code above:

Jagged Array [][]

[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

Jagged Array [][][]

dim1 = 0
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 1
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 2
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 3
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1


Jagged Array [][][][]

dim1 = 0, dim2 = 0
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 0, dim2 = 1
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 0, dim2 = 2
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 0, dim2 = 3
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 1, dim2 = 0
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 1, dim2 = 1
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 1, dim2 = 2
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 1, dim2 = 3
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 2, dim2 = 0
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 2, dim2 = 1
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 2, dim2 = 2
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 2, dim2 = 3
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 3, dim2 = 0
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 3, dim2 = 1
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 3, dim2 = 2
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

dim1 = 3, dim2 = 3
[0][0]: 1.1     [0][1]: 2.1     [0][2]: 3.1     [0][3]: 4.1
[1][0]: 11.1    [1][1]: 34.1    [1][2]: 67.1
[2][0]: 89.1    [2][1]: 23.1
[3][0]: 0.1     [3][1]: 45.1    [3][2]: 78.1    [3][3]: 53.1    [3][4]: 99.1

All suggestions are welcome.

The summary information:

\$\endgroup\$
3
  • \$\begingroup\$ Have you considered to use meta programming to generate the different overloads? Just like I suggested in your previous follow-up thread. \$\endgroup\$ Feb 2, 2021 at 8:16
  • 1
    \$\begingroup\$ @PeterCsala Thank you for the comments. I know that there is a way to use meta programming to generate these overloads. Besides, I am wondering the feedback of the proposed non-metaprogramming version first. \$\endgroup\$
    – JimmyHu
    Feb 2, 2021 at 8:26
  • 1
    \$\begingroup\$ The parameters' null checks can be shortened. Unfortunately Simplified parameter null validation code is not yet part of the C# language. But with code weaving you can use NullGuards. \$\endgroup\$ Feb 2, 2021 at 8:48

1 Answer 1

3
\$\begingroup\$

It seems like your end goal is to apply ConvertAll to all of the different types of C# arrays, nested or not, and with with lower bounds not equal to zero.

All arrays can be manipulated through the Array base class and any level of nesting can be handled by using recursion.

First some plumbing is needed to enumerate all possible indices of an array of arbitrary dimensions:

private static bool IncrementIndices(Array array, int[] indices)
{
    int rank = array.Rank;
    indices[rank - 1]++;
    for (int dim = rank - 1; dim >= 0; dim--)
    {
        if (indices[dim] > array.GetUpperBound(dim))
        {
            if (dim == 0)
            {
                return false;
            }
            for (int j = dim; j < rank; j++)
                indices[j] = array.GetLowerBound(j);
            indices[dim - 1]++;
        }
    }
    return true;
}

With this method we can then convert each value in a non-nested array:

public static Array ConvertAll<TInput, TOutput>(this Array source, Converter<TInput, TOutput> converter)
{
    if (ReferenceEquals(source, null))
    {
        throw new ArgumentNullException(nameof(source));
    }
    if (ReferenceEquals(converter, null))
    {
        throw new ArgumentNullException(nameof(converter));
    }
    if (!typeof(TInput).IsAssignableFrom(source.GetType().GetElementType()))
    {
        throw new ArgumentException("Type of " + nameof(TInput) + " (" + typeof(TInput).Name + ") is not assignable from the element type of " + nameof(source) + " (" + source.GetType().GetElementType().Name + ")");
    }

    var dimensions = new int[source.Rank];
    var indices = new int[source.Rank];
    var anyDimensionZero = false;
    for (int dimension = 0; dimension < dimensions.Length; dimension++)
    {
        dimensions[dimension] = source.GetLength(dimension);
        indices[dimension] = source.GetLowerBound(dimension);
        if (dimensions[dimension] == 0)
        {
            anyDimensionZero = true;
        }
    }

    var destination = Array.CreateInstance(typeof(TOutput), dimensions, indices);
    if (anyDimensionZero)
    {
        return destination;
    }

    do
    {
        var currentValue = source.GetValue(indices);
        var convertedValue = converter((TInput)currentValue);
        destination.SetValue(convertedValue, indices);
    }
    while (IncrementIndices(source, indices));

    return destination;
}

We can then add recursion to handle nesting:

public static Array ConvertAll<TInput, TOutput>(this Array source, Converter<TInput, TOutput> converter)
{
    if (ReferenceEquals(source, null))
    {
        throw new ArgumentNullException(nameof(source));
    }
    if (ReferenceEquals(converter, null))
    {
        throw new ArgumentNullException(nameof(converter));
    }
    var elementType = source.GetType().GetElementType();
    if (!typeof(TInput).IsAssignableFrom(source.GetType().GetElementType()) && !elementType.IsArray)
    {
        throw new ArgumentException("Type of " + nameof(TInput) + " (" + typeof(TInput).Name + ") is not assignable from the element type of " + nameof(source) + " (" + source.GetType().GetElementType().Name + ")");
    }

    var recursivelyApplyConverter = elementType != typeof(TInput);

    var dimensions = new int[source.Rank];
    var indices = new int[source.Rank];
    var anyDimensionZero = false;
    for (int dimension = 0; dimension < dimensions.Length; dimension++)
    {
        dimensions[dimension] = source.GetLength(dimension);
        indices[dimension] = source.GetLowerBound(dimension);
        if (dimensions[dimension] == 0)
        {
            anyDimensionZero = true;
        }
    }

    var destinationElementType = recursivelyApplyConverter ? GetTargetArrayType(elementType, typeof(TOutput)) : typeof(TOutput);
    var destination = Array.CreateInstance(destinationElementType, dimensions, indices);
    if (anyDimensionZero)
    {
        return destination;
    }

    do
    {
        var currentValue = source.GetValue(indices);
        if (ReferenceEquals(currentValue, null) && recursivelyApplyConverter)
            continue;
        var convertedValue = recursivelyApplyConverter
            ? (object)ConvertAll<TInput, TOutput>((Array)currentValue, converter)
            : converter((TInput)currentValue);
        destination.SetValue(convertedValue, indices);
    }
    while (IncrementIndices(source, indices));

    return destination;
}

Here the tricky bit is to construct the correct target type:

private static Type GetTargetArrayType(Type sourceType, Type targetType)
{
    var types = new Stack<Type>();
    var type = sourceType;
    while (type.IsArray && type != targetType)
    {
        types.Push(type);
        type = type.GetElementType();
    }
    var resultType = targetType;
    while (types.Count > 0)
    {
        var arrayType = types.Pop();
        var rank = arrayType.GetArrayRank();
        //MakeArrayType() != MakeArrayType(1)
        //See: https://docs.microsoft.com/en-us/dotnet/api/system.type.makearraytype
        resultType = rank == 1 ? resultType.MakeArrayType() : resultType.MakeArrayType(rank);
    }
    return resultType;
}

The same basic construct can be used to implement other primitives such as foreach:

public static void ForEach<TSource>(this Array source, Func<TSource, int[], TSource> updater)
{
    if (ReferenceEquals(source, null))
    {
        throw new ArgumentNullException(nameof(source));
    }
    if (ReferenceEquals(updater, null))
    {
        throw new ArgumentNullException(nameof(updater));
    }
    if (!typeof(TSource).IsAssignableFrom(source.GetType().GetElementType()))
    {
        throw new ArgumentException("Type of " + nameof(TSource) + " (" + typeof(TSource).Name + ") is not assignable from the element type of " + nameof(source) + " (" + source.GetType().GetElementType().Name + ")");
    }

    var indices = new int[source.Rank];
    for (int dimension = 0; dimension < indices.Length; dimension++)
    {
        if (source.GetLength(dimension) == 0)
        {
            return;
        }
        indices[dimension] = source.GetLowerBound(dimension);
    }

    var tempIndices = new int[indices.Length];
    do
    {
        Array.Copy(indices, tempIndices, indices.Length);
        var currentValue = source.GetValue(indices);
        var updatedValue = updater((TSource)currentValue, tempIndices);
        source.SetValue(updatedValue, indices);
    }
    while (IncrementIndices(source, indices));
}

public static void ForEach<TSource>(this Array source, Action<TSource, int[]> action)
{
    if (ReferenceEquals(source, null))
    {
        throw new ArgumentNullException(nameof(source));
    }
    if (ReferenceEquals(action, null))
    {
        throw new ArgumentNullException(nameof(action));
    }
    if (!typeof(TSource).IsAssignableFrom(source.GetType().GetElementType()))
    {
        throw new ArgumentException("Type of " + nameof(TSource) + " (" + typeof(TSource).Name + ") is not assignable from the element type of " + nameof(source) + " (" + source.GetType().GetElementType().Name + ")");
    }

    var indices = new int[source.Rank];
    for (int dimension = 0; dimension < indices.Length; dimension++)
    {
        if (source.GetLength(dimension) == 0)
        {
            return;
        }
        indices[dimension] = source.GetLowerBound(dimension);
    }

    var tempIndices = new int[indices.Length];
    do
    {
        Array.Copy(indices, tempIndices, indices.Length);
        var currentValue = source.GetValue(indices);
        action((TSource)currentValue, tempIndices);
    }
    while (IncrementIndices(source, indices));
}

Now all that's left is to wrap the core ConvertAll in strongly typed wrappers, preferably in an automated way.

int[,][] test = (int[,][])Array.CreateInstance(typeof(int[]), lengths: new int[] { 2, 2 }, lowerBounds: new int[] { -11, 10 });
int arrayLength = 1;
int valueCounter = 1;
test.ForEach(_ =>
{
    var array = new int[arrayLength++];
    array.ForEach(__ => valueCounter++);
    return array;
});

test.ConvertAll(x => x + 0.1f)
    .ForEach((l2, l1Indices) => 
        l2.ForEach((x, l2Indices) => 
            Console.WriteLine("array[{1}][{2}] = {0}", x, string.Join(", ", l1Indices), string.Join(", ", l2Indices))));

public static class ArrayExtensions
{
    //All of the above
    
    public static void ForEach<TSource>(this TSource[] source, Func<TSource, TSource> updater)
    {
        ForEach<TSource>((Array)source, (x, _) => updater(x));
    }
    
    public static void ForEach<TSource>(this TSource[,] source, Func<TSource, TSource> updater)
    {
        ForEach<TSource>((Array)source, (x, _) => updater(x));
    }
    
    public static void ForEach<TSource>(this TSource[] source, Action<TSource, int[]> action)
    {
        ForEach<TSource>((Array)source, action);
    }
    
    public static void ForEach<TSource>(this TSource[,] source, Action<TSource, int[]> action)
    {
        ForEach<TSource>((Array)source, action);
    }
    
    public static TOutput[,][] ConvertAll<TInput, TOutput>(this TInput[,][] source, Converter<TInput, TOutput> converter)
    {
        return (TOutput[,][])ConvertAll<TInput, TOutput>((Array)source, converter);
    }
}
\$\endgroup\$

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.