2
\$\begingroup\$

I am attempting to implement a converter which can convert two dimensional array (such as string[,]) into markdown table.

The experimental implementation

The experimental implementation of Converter class is as below.

public static class Converter
{
    /// <summary>
    /// Align formats
    /// </summary>
    public enum Align
    {
        Right,
        Center,
        Left,
        Default
    }

    private static TOutput[,] ConvertAll<TInput, TOutput>(TInput[,] array, Converter<TInput, TOutput> converter)
    {
        //    null check
        if (ReferenceEquals(array, null))
        {
            throw new ArgumentNullException($"{nameof(array)} is null");
        }

        if (ReferenceEquals(converter, null))
        {
            throw new ArgumentNullException($"{nameof(converter)} is null");
        }

        var output = new TOutput[array.GetLongLength(0), array.GetLongLength(1)];
        for (long row = 0; row < array.GetLongLength(0); row++)
        {
            for (long column = 0; column < array.GetLongLength(1); column++)
            {
                output[row, column] = converter(array[row, column]);
            }
        }
        return output;
    }

    /// <summary>
    /// ToMarkdownTable method with TInput type array
    /// </summary>
    /// <param name="input">Input array</param>
    /// <param name="align">Align format of all columns</param>
    /// <returns></returns>
    public static string[] ToMarkdownTable<TInput>(TInput[,] input, Align align = Align.Default)
    {
        return ToMarkdownTable(ConvertAll(input, element => element.ToString()), align);
    }

    /// <summary>
    /// ToMarkdownTable method with TInput type array and the parameter for assigning align format of each column
    /// </summary>
    /// <param name="input">Input array</param>
    /// <param name="aligns">Align format of each column</param>
    /// <returns></returns>
    public static string[] ToMarkdownTable<TInput>(TInput[,] input, Align[] aligns)
    {
        return ToMarkdownTable(ConvertAll(input, element => element.ToString()), aligns);
    }


    /// <summary>
    /// ToMarkdownTable method
    /// </summary>
    /// <param name="input">Input array</param>
    /// <param name="align">Align format of all columns</param>
    /// <returns></returns>
    public static string[] ToMarkdownTable(string[,] input, Align align = Align.Default)
    {
        Align[] format = new Align[input.GetLongLength(1)];
        Array.Fill(format, align);
        return ToMarkdownTable(input, format);
    }

    /// <summary>
    /// ToMarkdownTable method with assigning align format of each column
    /// </summary>
    /// <param name="input">Input array</param>
    /// <param name="aligns">Align format of each column</param>
    /// <returns></returns>
    public static string[] ToMarkdownTable(string[,] input, Align[] aligns)
    {
        //    Null check
        if (ReferenceEquals(input, null))
        {
            throw new ArgumentNullException($"{nameof(input)} is null");
        }

        if (ReferenceEquals(input, null))
        {
            throw new ArgumentNullException($"{nameof(aligns)} is null");
        }

        long width = input.GetLongLength(1);
        long height = input.GetLongLength(0);

        string[] output = new string[height + 1];

        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append('|');
        for (int x = 0; x < width; x++)
        {
            stringBuilder.Append(input[0, x]);
            stringBuilder.Append('|');
        }
        output[0] = stringBuilder.ToString();
        stringBuilder.Clear();

        stringBuilder.Append('|');
        for (int x = 0; x < width; x++)
        {
            switch (aligns[x])
            {
                case Align.Right:
                    stringBuilder.Append("-:|");
                    break;
                case Align.Center:
                    stringBuilder.Append(":-:|");
                    break;
                case Align.Left:
                    stringBuilder.Append(":-|");
                    break;
                case Align.Default:
                    stringBuilder.Append("-|");
                    break;
                default:
                    stringBuilder.Append("-|");
                    break;
            }
            
        }
        output[1] = stringBuilder.ToString();
        stringBuilder.Clear();

        for (int y = 1; y < height; y++)
        {
            stringBuilder.Append('|');
            for (int x = 0; x < width; x++)
            {
                stringBuilder.Append(input[y, x]);
                stringBuilder.Append('|');
            }
            output[y + 1] = stringBuilder.ToString();
            stringBuilder.Clear();
        }
        return output;
    }
}

Test cases

The test cases of Converter.ToMarkdownTable methods include string, sbyte, byte, short, ushort, char and int type two dimensional arrays input.

//    string type two dimensional array case
Console.WriteLine("string type two dimensional array case");
string[,] test_string = { { "0", "1", "1", "1" },
                          { "2", "3", "1", "1" },
                          { "0", "1", "1", "1" }};

var format = new Converter.Align[test_string.GetLongLength(1)];
for (int i = 0; i < test_string.GetLongLength(1); i++)
{
    format[i] = Converter.Align.Center;
}
var MarkdownTable = Converter.ToMarkdownTable(test_string, Converter.Align.Center);
for (int i = 0; i < MarkdownTable.Length; i++)
{
    Console.WriteLine(MarkdownTable[i]);
}
Console.WriteLine();

//    sbyte type two dimensional array case
Console.WriteLine("sbyte type two dimensional array case");
sbyte[,] test_sbyte = { { 0, 1, 1, 1 },
                        { 2, 3, 1, 1 },
                        { 0, 1, 1, 1 }};

var MarkdownTable_sbyte = Converter.ToMarkdownTable(test_sbyte, Converter.Align.Center);
for (int i = 0; i < MarkdownTable.Length; i++)
{
    Console.WriteLine(MarkdownTable[i]);
}
Console.WriteLine();

//    byte type two dimensional array case
Console.WriteLine("byte type two dimensional array case");
byte[,] test_byte = { { 0, 1, 1, 1 },
                      { 2, 3, 1, 1 },
                      { 0, 1, 1, 1 }};

var MarkdownTable_byte = Converter.ToMarkdownTable(test_byte, Converter.Align.Center);
for (int i = 0; i < MarkdownTable.Length; i++)
{
    Console.WriteLine(MarkdownTable[i]);
}
Console.WriteLine();

//    short type two dimensional array case
Console.WriteLine("short type two dimensional array case");
short[,] test_short = { { 0, 1, 1, 1 },
                        { 2, 3, 1, 1 },
                        { 0, 1, 1, 1 }};

var MarkdownTable_short = Converter.ToMarkdownTable(test_short, Converter.Align.Center);
for (int i = 0; i < MarkdownTable.Length; i++)
{
    Console.WriteLine(MarkdownTable[i]);
}
Console.WriteLine();

//    ushort type two dimensional array case
Console.WriteLine("ushort type two dimensional array case");
ushort[,] test_ushort = { { 0, 1, 1, 1 },
                          { 2, 3, 1, 1 },
                          { 0, 1, 1, 1 }};

var MarkdownTable_ushort = Converter.ToMarkdownTable(test_ushort, Converter.Align.Center);
for (int i = 0; i < MarkdownTable.Length; i++)
{
    Console.WriteLine(MarkdownTable[i]);
}
Console.WriteLine();

//    char type two dimensional array case
Console.WriteLine("char type two dimensional array case");
char[,] test_char = { { '0', '1', '1', '1' },
                      { '2', '3', '1', '1' },
                      { '0', '1', '1', '1' }};

var MarkdownTable_char = Converter.ToMarkdownTable(test_char, Converter.Align.Center);
for (int i = 0; i < MarkdownTable.Length; i++)
{
    Console.WriteLine(MarkdownTable[i]);
}
Console.WriteLine();

//    int type two dimensional array case
Console.WriteLine("int type two dimensional array case");
int[,] test_int = { { 0, 1, 1, 1 },
                    { 2, 3, 1, 1 },
                    { 0, 1, 1, 1 }};

var MarkdownTable_int = Converter.ToMarkdownTable(test_int, Converter.Align.Center);
for (int i = 0; i < MarkdownTable.Length; i++)
{
    Console.WriteLine(MarkdownTable[i]);
}
Console.WriteLine();

The output of the above tests:

string type two dimensional array case
|0|1|1|1|
|:-:|:-:|:-:|:-:|
|2|3|1|1|
|0|1|1|1|

sbyte type two dimensional array case
|0|1|1|1|
|:-:|:-:|:-:|:-:|
|2|3|1|1|
|0|1|1|1|

byte type two dimensional array case
|0|1|1|1|
|:-:|:-:|:-:|:-:|
|2|3|1|1|
|0|1|1|1|

short type two dimensional array case
|0|1|1|1|
|:-:|:-:|:-:|:-:|
|2|3|1|1|
|0|1|1|1|

ushort type two dimensional array case
|0|1|1|1|
|:-:|:-:|:-:|:-:|
|2|3|1|1|
|0|1|1|1|

char type two dimensional array case
|0|1|1|1|
|:-:|:-:|:-:|:-:|
|2|3|1|1|
|0|1|1|1|

int type two dimensional array case
|0|1|1|1|
|:-:|:-:|:-:|:-:|
|2|3|1|1|
|0|1|1|1|

If there is any possible improvement, please let me know.

\$\endgroup\$
5
\$\begingroup\$

Here are my observations:

Align

  • I think Alignment would be a better name. (Align is a verb where Alignment is a noun)
  • Most of the time the Default, Unknown or Invalid is the first element in the enum
    • If you define an enum variable without specifying its value then it will be declared as 0
public enum Alignment { Default, Left, Center, Right }

ConvertAll

  • ReferenceEquals(array, null): This can be simplified like this: array is null
  • ArgumentNullException($"{nameof(array)} is null"): Here the constructor anticipates the name of the parameter, not the message. So the correct way would look like this:
if (array is null) throw new ArgumentNullException(nameof(array));
  • array.GetLongLength(0): As you have done it in the ToMarkdownTable you can declare a helper variable to store this value and reuse it multiple times:
var rowCount = array.GetLongLength(0);
var columnCount = array.GetLongLength(1);
var output = new TOutput[rowCount, columnCount];
  • Nested for loops: It is minor, but you can get rid of the block operators:
for (var row = 0; row < rowCount; row++)
    for (var column = 0; column < columnCount; column++)
        output[row, column] = converter(array[row, column]);

So the whole ConvertAll can be refactored like this:

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

    var rowCount = array.GetLongLength(0);
    var columnCount = array.GetLongLength(1);
    var output = new TOutput[rowCount, columnCount];

    for (var row = 0; row < rowCount; row++)
        for (var column = 0; column < columnCount; column++)
            output[row, column] = converter(array[row, column]);

    return output;
}

ToMarkDownTable where the core logic resides

  • Yet again the null checks can be simplified
    • The second null check is done against input, please fix it to use alings there
  • width and height: I would suggest to stick with the terms row and column because we are talking about table.
  • stringBuilder.Clear(): Even though it does work, it indicates that your separation of concerns is not good.
    • I do suggest to introduce several helper method and use them like this:
public static string[] ToMarkDownTable(string[,] input, Alignment[] aligns)
{
    if (input is null) throw new ArgumentNullException(nameof(input));
    if (aligns is null) throw new ArgumentNullException(nameof(aligns));

    var rowCount = input.GetLongLength(0);
    var output = new string[rowCount + 1];

    output[0] = ToTableHeader(input);
    output[1] = ToTableCellAlignments(input, aligns);

    for (var row = 1; row < rowCount; row++)
        output[row + 1] = ToTableRow(input, row);

    return output;
}

Header

  • stringBuilder.Append('|');: I suggest to introduce a class level constant to hold the cell separator:
private const char CellSeparator = '|';
  • With this the header generation would be string literal free:
private static string ToTableHeader(string[,] input)
{
    var tableHeaderBuilder = new StringBuilder();
    tableHeaderBuilder.Append(CellSeparator);

    for (var column = 0; column < input.GetLongLength(1); column++)
    {
        tableHeaderBuilder.Append(input[0, column]);
        tableHeaderBuilder.Append(CellSeparator);
    }

    return tableHeaderBuilder.ToString();
}
  • If your input would be a multi-dimensional array, not a jagged one then the input could be a single dimensional array. That would increase the separation.

Cell Alignments

  • stringBuilder.Append: This has been repeated way too many times.
    • Your branching should be scoped to the data (should NOT include the operation).
    • With that you would be able to use switch expression:
private static string ToTableCellAlignments(string[,] input, Alignment[] aligns)
{
    var tableCellAlignmentBuilder = new StringBuilder();
    tableCellAlignmentBuilder.Append(CellSeparator);

    for (var column = 0; column < input.GetLongLength(1); column++)
    {
        var separator = aligns[column] switch
        {
            Alignment.Right => $"-:{CellSeparator}",
            Alignment.Center => $":-:{CellSeparator}",
            Alignment.Left => $":-{CellSeparator}",
            Alignment.Default => $"-{CellSeparator}",
            _ => $"-{CellSeparator}"
        };

        tableCellAlignmentBuilder.Append(separator);
    }

    return tableCellAlignmentBuilder.ToString();
}

Row

  • I've decided to extract away only a single row generation instead of all rows
    • This simplifies the row generation logic
    • and can be easily altered to utilize multi dimensional array
private static string ToTableRow(string[,] input, int row)
{
    var tableRowBuilder = new StringBuilder();
    tableRowBuilder.Append(CellSeparator);

    for (var column = 0; column < input.GetLongLength(1); column++)
    {
        tableRowBuilder.Append(input[row, column]);
        tableRowBuilder.Append(CellSeparator);
    }
    return tableRowBuilder.ToString();
}

In case of multi-dimensional arrays the core logic would look like this:

  • So, in my opinion it might make sense to convert your jagged array to multi-dimensional
private static string[] ToMarkDownTable(string[][] input, Alignment[] aligns)
{
    if (input is null) throw new ArgumentNullException(nameof(input));
    if (aligns is null) throw new ArgumentNullException(nameof(aligns));

    var rowCount = input.GetLongLength(0);
    var columnCount = input.GetLongLength(1);
    var output = new string[rowCount + 1];

    output[0] = ToTableHeader(input[0]);
    output[1] = ToTableCellAlignments(columnCount, aligns);

    for (var row = 1; row < rowCount; row++)
        output[row + 1] = ToTableRow(input[row]);

    return output;
}

private const char CellSeparator = '|';
private static string ToTableHeader(string[] headers)
{
    var tableHeaderBuilder = new StringBuilder();
    tableHeaderBuilder.Append(CellSeparator);

    foreach (var header in headers)
    {
        tableHeaderBuilder.Append(header);
        tableHeaderBuilder.Append(CellSeparator);
    }

    return tableHeaderBuilder.ToString();
}

private static string ToTableCellAlignments(long columns, Alignment[] aligns)
{
    var tableCellAlignmentBuilder = new StringBuilder();
    tableCellAlignmentBuilder.Append(CellSeparator);

    for (var column = 0; column < columns; column++)
    {
        var separator = aligns[column] switch
        {
            Alignment.Right => $"-:{CellSeparator}",
            Alignment.Center => $":-:{CellSeparator}",
            Alignment.Left => $":-{CellSeparator}",
            _ => $"-{CellSeparator}"
        };

        tableCellAlignmentBuilder.Append(separator);
    }

    return tableCellAlignmentBuilder.ToString();
}

private static string ToTableRow(string[] cells)
{
    var tableRowBuilder = new StringBuilder();
    tableRowBuilder.Append(CellSeparator);

    foreach (var cell in cells)
    {
        tableRowBuilder.Append(cell);
        tableRowBuilder.Append(CellSeparator);
    }
    return tableRowBuilder.ToString();
}

UPDATE Further simplification of ToTableCellAlignments:

private static string ToTableCellAlignments(Alignment[] alignments)
{
    var tableCellAlignmentBuilder = new StringBuilder();
    tableCellAlignmentBuilder.Append(CellSeparator);

    for (var alignment in alignments)
    {
        var separator = alignment switch
        {
            Alignment.Right => $"-:{CellSeparator}",
            Alignment.Center => $":-:{CellSeparator}",
            Alignment.Left => $":-{CellSeparator}",
            _ => $"-{CellSeparator}"
        };

        tableCellAlignmentBuilder.Append(separator);
    }

    return tableCellAlignmentBuilder.ToString();
}

Prerequisite: ToMarkDownTable has to check aligns.Length against array.GetLongLength(1) if they aren't matching then it could throw an ArgumentException.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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