[c#] 방법 : 콘솔 앱에서 테이블을 그리는 가장 좋은 방법 (C #)

흥미로운 질문이 있습니다. 매우 빠른 간격으로 많은 데이터가 변경된다고 상상해보십시오. 해당 데이터를 콘솔 앱의 테이블로 표시하고 싶습니다. f.ex :

-------------------------------------------------------------------------
|    Column 1     |    Column 2     |    Column 3     |    Column 4     |
-------------------------------------------------------------------------
|                 |                 |                 |                 |
|                 |                 |                 |                 |
|                 |                 |                 |                 |
-------------------------------------------------------------------------

빠르게 유지하는 방법과 열 너비를 수정하는 방법? Java에서 수행하는 방법을 알고 있지만 C #에서는 수행하지 않습니다.



답변

다음과 같이 할 수 있습니다.

static int tableWidth = 73;

static void Main(string[] args)
{
    Console.Clear();
    PrintLine();
    PrintRow("Column 1", "Column 2", "Column 3", "Column 4");
    PrintLine();
    PrintRow("", "", "", "");
    PrintRow("", "", "", "");
    PrintLine();
    Console.ReadLine();
}

static void PrintLine()
{
    Console.WriteLine(new string('-', tableWidth));
}

static void PrintRow(params string[] columns)
{
    int width = (tableWidth - columns.Length) / columns.Length;
    string row = "|";

    foreach (string column in columns)
    {
        row += AlignCentre(column, width) + "|";
    }

    Console.WriteLine(row);
}

static string AlignCentre(string text, int width)
{
    text = text.Length > width ? text.Substring(0, width - 3) + "..." : text;

    if (string.IsNullOrEmpty(text))
    {
        return new string(' ', width);
    }
    else
    {
        return text.PadRight(width - (width - text.Length) / 2).PadLeft(width);
    }
}


답변

정렬 값과 함께 String.Format 을 사용 합니다.

예를 들면 :

String.Format("|{0,5}|{1,5}|{2,5}|{3,5}|", arg0, arg1, arg2, arg3);

서식이 지정된 하나의 행을 만듭니다.


답변

편집 : @superlogical 덕분에 이제 github 에서 다음 코드를 찾아 개선 할 수 있습니다 !


여기 몇 가지 아이디어를 바탕으로이 수업을 작성했습니다. 열 너비는 최적이며 다음과 같은 간단한 API로 객체 배열을 처리 할 수 ​​있습니다.

static void Main(string[] args)
{
  IEnumerable<Tuple<int, string, string>> authors =
    new[]
    {
      Tuple.Create(1, "Isaac", "Asimov"),
      Tuple.Create(2, "Robert", "Heinlein"),
      Tuple.Create(3, "Frank", "Herbert"),
      Tuple.Create(4, "Aldous", "Huxley"),
    };

  Console.WriteLine(authors.ToStringTable(
    new[] {"Id", "First Name", "Surname"},
    a => a.Item1, a => a.Item2, a => a.Item3));

  /* Result:
  | Id | First Name | Surname  |
  |----------------------------|
  | 1  | Isaac      | Asimov   |
  | 2  | Robert     | Heinlein |
  | 3  | Frank      | Herbert  |
  | 4  | Aldous     | Huxley   |
  */
}

수업은 다음과 같습니다.

public static class TableParser
{
  public static string ToStringTable<T>(
    this IEnumerable<T> values,
    string[] columnHeaders,
    params Func<T, object>[] valueSelectors)
  {
    return ToStringTable(values.ToArray(), columnHeaders, valueSelectors);
  }

  public static string ToStringTable<T>(
    this T[] values,
    string[] columnHeaders,
    params Func<T, object>[] valueSelectors)
  {
    Debug.Assert(columnHeaders.Length == valueSelectors.Length);

    var arrValues = new string[values.Length + 1, valueSelectors.Length];

    // Fill headers
    for (int colIndex = 0; colIndex < arrValues.GetLength(1); colIndex++)
    {
      arrValues[0, colIndex] = columnHeaders[colIndex];
    }

    // Fill table rows
    for (int rowIndex = 1; rowIndex < arrValues.GetLength(0); rowIndex++)
    {
      for (int colIndex = 0; colIndex < arrValues.GetLength(1); colIndex++)
      {
        arrValues[rowIndex, colIndex] = valueSelectors[colIndex]
          .Invoke(values[rowIndex - 1]).ToString();
      }
    }

    return ToStringTable(arrValues);
  }

  public static string ToStringTable(this string[,] arrValues)
  {
    int[] maxColumnsWidth = GetMaxColumnsWidth(arrValues);
    var headerSpliter = new string('-', maxColumnsWidth.Sum(i => i + 3) - 1);

    var sb = new StringBuilder();
    for (int rowIndex = 0; rowIndex < arrValues.GetLength(0); rowIndex++)
    {
      for (int colIndex = 0; colIndex < arrValues.GetLength(1); colIndex++)
      {
        // Print cell
        string cell = arrValues[rowIndex, colIndex];
        cell = cell.PadRight(maxColumnsWidth[colIndex]);
        sb.Append(" | ");
        sb.Append(cell);
      }

      // Print end of line
      sb.Append(" | ");
      sb.AppendLine();

      // Print splitter
      if (rowIndex == 0)
      {
        sb.AppendFormat(" |{0}| ", headerSpliter);
        sb.AppendLine();
      }
    }

    return sb.ToString();
  }

  private static int[] GetMaxColumnsWidth(string[,] arrValues)
  {
    var maxColumnsWidth = new int[arrValues.GetLength(1)];
    for (int colIndex = 0; colIndex < arrValues.GetLength(1); colIndex++)
    {
      for (int rowIndex = 0; rowIndex < arrValues.GetLength(0); rowIndex++)
      {
        int newLength = arrValues[rowIndex, colIndex].Length;
        int oldLength = maxColumnsWidth[colIndex];

        if (newLength > oldLength)
        {
          maxColumnsWidth[colIndex] = newLength;
        }
      }
    }

    return maxColumnsWidth;
  }
}

편집 : 약간의 개선 사항을 추가했습니다. 열 머리글이 속성 이름이되도록하려면 다음 메서드를 추가합니다 TableParser(반사로 인해 약간 느려질 수 있음).

public static string ToStringTable<T>(
    this IEnumerable<T> values,
    params Expression<Func<T, object>>[] valueSelectors)
{
  var headers = valueSelectors.Select(func => GetProperty(func).Name).ToArray();
  var selectors = valueSelectors.Select(exp => exp.Compile()).ToArray();
  return ToStringTable(values, headers, selectors);
}

private static PropertyInfo GetProperty<T>(Expression<Func<T, object>> expresstion)
{
  if (expresstion.Body is UnaryExpression)
  {
    if ((expresstion.Body as UnaryExpression).Operand is MemberExpression)
    {
      return ((expresstion.Body as UnaryExpression).Operand as MemberExpression).Member as PropertyInfo;
    }
  }

  if ((expresstion.Body is MemberExpression))
  {
    return (expresstion.Body as MemberExpression).Member as PropertyInfo;
  }
  return null;
}


답변

간단한 (여기 답변의 코드 샘플과 같은)부터 고급에 이르기까지 콘솔 테이블 형식화를 허용하는 여러 오픈 소스 라이브러리가 있습니다.

ConsoleTable

NuGet 통계로 판단하면 테이블 서식 지정에 가장 많이 사용되는 라이브러리는 ConsoleTable 입니다. 테이블은 다음과 같이 구성됩니다 (readme 파일에서).

var table = new ConsoleTable("one", "two", "three");
table.AddRow(1, 2, 3)
     .AddRow("this line should be longer", "yes it is", "oh");

미리 정의 된 스타일 중 하나를 사용하여 테이블 형식을 지정할 수 있습니다. 다음과 같이 표시됩니다.

--------------------------------------------------
| one                        | two       | three |
--------------------------------------------------
| 1                          | 2         | 3     |
--------------------------------------------------
| this line should be longer | yes it is | oh    |
--------------------------------------------------

이 라이브러리에는 서식이없는 단일 행 셀이 필요합니다.

더 많은 선 스타일과 같이 약간 확장 된 기능 세트가있는 ConsoleTable 기반 라이브러리가 두 개 있습니다.

CsConsoleFormat

더 복잡한 서식이 필요한 경우 CsConsoleFormat 을 사용할 수 있습니다 . † 다음은 프로세스 목록 (샘플 프로젝트에서)에서 생성 된 테이블입니다.

new Grid { Stroke = StrokeHeader, StrokeColor = DarkGray }
    .AddColumns(
        new Column { Width = GridLength.Auto },
        new Column { Width = GridLength.Auto, MaxWidth = 20 },
        new Column { Width = GridLength.Star(1) },
        new Column { Width = GridLength.Auto }
    )
    .AddChildren(
        new Cell { Stroke = StrokeHeader, Color = White }
            .AddChildren("Id"),
        new Cell { Stroke = StrokeHeader, Color = White }
            .AddChildren("Name"),
        new Cell { Stroke = StrokeHeader, Color = White }
            .AddChildren("Main Window Title"),
        new Cell { Stroke = StrokeHeader, Color = White }
            .AddChildren("Private Memory"),
        processes.Select(process => new[] {
            new Cell { Stroke = StrokeRight }
                .AddChildren(process.Id),
            new Cell { Stroke = StrokeRight, Color = Yellow, TextWrap = TextWrapping.NoWrap }
                .AddChildren(process.ProcessName),
            new Cell { Stroke = StrokeRight, Color = White, TextWrap = TextWrapping.NoWrap }
                .AddChildren(process.MainWindowTitle),
            new Cell { Stroke = LineThickness.None, Align = HorizontalAlignment.Right }
                .AddChildren(process.PrivateMemorySize64.ToString("n0")),
        })
    )

최종 결과는 다음과 같습니다.

모든 종류의 테이블 라인 (여러 개 포함 및 사용자 지정 가능), 단어 줄 바꿈, 색상, 내용 또는 백분율에 따라 증가하는 열, 텍스트 정렬 등이있는 여러 줄 셀을 지원합니다.

† CsConsoleFormat은 제가 개발했습니다.


답변

class ArrayPrinter
    {
    #region Declarations

    static bool isLeftAligned = false;
    const string cellLeftTop = "┌";
    const string cellRightTop = "┐";
    const string cellLeftBottom = "└";
    const string cellRightBottom = "┘";
    const string cellHorizontalJointTop = "┬";
    const string cellHorizontalJointbottom = "┴";
    const string cellVerticalJointLeft = "├";
    const string cellTJoint = "┼";
    const string cellVerticalJointRight = "┤";
    const string cellHorizontalLine = "─";
    const string cellVerticalLine = "│";

    #endregion

    #region Private Methods

    private static int GetMaxCellWidth(string[,] arrValues)
    {
        int maxWidth = 1;

        for (int i = 0; i < arrValues.GetLength(0); i++)
        {
            for (int j = 0; j < arrValues.GetLength(1); j++)
            {
                int length = arrValues[i, j].Length;
                if (length > maxWidth)
                {
                    maxWidth = length;
                }
            }
        }

        return maxWidth;
    }

    private static string GetDataInTableFormat(string[,] arrValues)
    {
        string formattedString = string.Empty;

        if (arrValues == null)
            return formattedString;

        int dimension1Length = arrValues.GetLength(0);
        int dimension2Length = arrValues.GetLength(1);

        int maxCellWidth = GetMaxCellWidth(arrValues);
        int indentLength = (dimension2Length * maxCellWidth) + (dimension2Length - 1);
        //printing top line;
        formattedString = string.Format("{0}{1}{2}{3}", cellLeftTop, Indent(indentLength), cellRightTop, System.Environment.NewLine);

        for (int i = 0; i < dimension1Length; i++)
        {
            string lineWithValues = cellVerticalLine;
            string line = cellVerticalJointLeft;
            for (int j = 0; j < dimension2Length; j++)
            {
                string value = (isLeftAligned) ? arrValues[i, j].PadRight(maxCellWidth, ' ') : arrValues[i, j].PadLeft(maxCellWidth, ' ');
                lineWithValues += string.Format("{0}{1}", value, cellVerticalLine);
                line += Indent(maxCellWidth);
                if (j < (dimension2Length - 1))
                {
                    line += cellTJoint;
                }
            }
            line += cellVerticalJointRight;
            formattedString += string.Format("{0}{1}", lineWithValues, System.Environment.NewLine);
            if (i < (dimension1Length - 1))
            {
                formattedString += string.Format("{0}{1}", line, System.Environment.NewLine);
            }
        }

        //printing bottom line
        formattedString += string.Format("{0}{1}{2}{3}", cellLeftBottom, Indent(indentLength), cellRightBottom, System.Environment.NewLine);
        return formattedString;
    }

    private static string Indent(int count)
    {
        return string.Empty.PadLeft(count, '─');
    }

    #endregion

    #region Public Methods

    public static void PrintToStream(string[,] arrValues, StreamWriter writer)
    {
        if (arrValues == null)
            return;

        if (writer == null)
            return;

        writer.Write(GetDataInTableFormat(arrValues));
    }

    public static void PrintToConsole(string[,] arrValues)
    {
        if (arrValues == null)
            return;

        Console.WriteLine(GetDataInTableFormat(arrValues));
    }

    #endregion

    static void Main(string[] args)
    {
        int value = 997;
        string[,] arrValues = new string[5, 5];
        for (int i = 0; i < arrValues.GetLength(0); i++)
        {
            for (int j = 0; j < arrValues.GetLength(1); j++)
            {
                value++;
                arrValues[i, j] = value.ToString();
            }
        }
        ArrayPrinter.PrintToConsole(arrValues);
        Console.ReadLine();
    }
}


답변

가변 너비 열을 원했고 상자 문자에 대해서는 특별히 신경 쓰지 않았습니다. 또한 각 행에 대한 추가 정보를 인쇄해야했습니다.

따라서 다른 사람이 필요로하는 경우 몇 분을 절약 할 수 있습니다.

public class TestTableBuilder
{

    public interface ITextRow
    {
        String Output();
        void Output(StringBuilder sb);
        Object Tag { get; set; }
    }

    public class TableBuilder : IEnumerable<ITextRow>
    {
        protected class TextRow : List<String>, ITextRow
        {
            protected TableBuilder owner = null;
            public TextRow(TableBuilder Owner)
            {
                owner = Owner;
                if (owner == null) throw new ArgumentException("Owner");
            }
            public String Output()
            {
                StringBuilder sb = new StringBuilder();
                Output(sb);
                return sb.ToString();
            }
            public void Output(StringBuilder sb)
            {
                sb.AppendFormat(owner.FormatString, this.ToArray());
            }
            public Object Tag { get; set; }
        }

        public String Separator { get; set; }

        protected List<ITextRow> rows = new List<ITextRow>();
        protected List<int> colLength = new List<int>();

        public TableBuilder()
        {
            Separator = "  ";
        }

        public TableBuilder(String separator)
            : this()
        {
            Separator = separator;
        }

        public ITextRow AddRow(params object[] cols)
        {
            TextRow row = new TextRow(this);
            foreach (object o in cols)
            {
                String str = o.ToString().Trim();
                row.Add(str);
                if (colLength.Count >= row.Count)
                {
                    int curLength = colLength[row.Count - 1];
                    if (str.Length > curLength) colLength[row.Count - 1] = str.Length;
                }
                else
                {
                    colLength.Add(str.Length);
                }
            }
            rows.Add(row);
            return row;
        }

        protected String _fmtString = null;
        public String FormatString
        {
            get
            {
                if (_fmtString == null)
                {
                    String format = "";
                    int i = 0;
                    foreach (int len in colLength)
                    {
                        format += String.Format("{{{0},-{1}}}{2}", i++, len, Separator);
                    }
                    format += "\r\n";
                    _fmtString = format;
                }
                return _fmtString;
            }
        }

        public String Output()
        {
            StringBuilder sb = new StringBuilder();
            foreach (TextRow row in rows)
            {
                row.Output(sb);
            }
            return sb.ToString();
        }

        #region IEnumerable Members

        public IEnumerator<ITextRow> GetEnumerator()
        {
            return rows.GetEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return rows.GetEnumerator();
        }

        #endregion
    }



    static void Main(String[] args)
    {
        TableBuilder tb = new TableBuilder();
        tb.AddRow("When", "ID", "Name");
        tb.AddRow("----", "--", "----");

        tb.AddRow(DateTime.Now, "1", "Name1");
        tb.AddRow(DateTime.Now, "1", "Name2");

        Console.Write(tb.Output());
        Console.WriteLine();

        // or

        StringBuilder sb = new StringBuilder();
        int i = 0;
        foreach (ITextRow tr in tb)
        {
            tr.Output(sb);
            if (i++ > 1) sb.AppendLine("more stuff per line");
        }
        Console.Write(sb.ToString());
    }
}

산출:

아이디 이름
---------
2013-02-04 오후 8:37:44 1 Name1
2013-02-04 오후 8:37:44 1 Name2

아이디 이름
---------
2013-02-04 오후 8:37:44 1 Name1
한 줄에 더 많은 항목
2013-02-04 오후 8:37:44 1 Name2
한 줄에 더 많은 항목


답변

이것은 이전 답변에 대한 개선 사항입니다. 다양한 길이의 값과 다양한 수의 셀이있는 행에 대한 지원을 추가합니다. 예를 들면 :

┌──────────┬─────────┬──────────────────────────┬────────────────┬─────┬───────┐
Identifier     Type               Description  CPU Credit UseHoursBalance
├──────────┼─────────┼──────────────────────────┼────────────────┼─────┼───────┘
 i-1234154 t2.small       This is an example.│         3263.75  360
├──────────┼─────────┼──────────────────────────┼────────────────┼─────┘
 i-1231412 t2.small  This is another example.│         3089.93
└──────────┴─────────┴──────────────────────────┴────────────────┘

다음은 코드입니다.

public class ArrayPrinter
{
    const string TOP_LEFT_JOINT = "┌";
    const string TOP_RIGHT_JOINT = "┐";
    const string BOTTOM_LEFT_JOINT = "└";
    const string BOTTOM_RIGHT_JOINT = "┘";
    const string TOP_JOINT = "┬";
    const string BOTTOM_JOINT = "┴";
    const string LEFT_JOINT = "├";
    const string JOINT = "┼";
    const string RIGHT_JOINT = "┤";
    const char HORIZONTAL_LINE = '─';
    const char PADDING = ' ';
    const string VERTICAL_LINE = "│";

    private static int[] GetMaxCellWidths(List<string[]> table)
    {
        int maximumCells = 0;
        foreach (Array row in table)
        {
            if (row.Length > maximumCells)
                maximumCells = row.Length;
        }

        int[] maximumCellWidths = new int[maximumCells];
        for (int i = 0; i < maximumCellWidths.Length; i++)
            maximumCellWidths[i] = 0;

        foreach (Array row in table)
        {
            for (int i = 0; i < row.Length; i++)
            {
                if (row.GetValue(i).ToString().Length > maximumCellWidths[i])
                    maximumCellWidths[i] = row.GetValue(i).ToString().Length;
            }
        }

        return maximumCellWidths;
    }

    public static string GetDataInTableFormat(List<string[]> table)
    {
        StringBuilder formattedTable = new StringBuilder();
        Array nextRow = table.FirstOrDefault();
        Array previousRow = table.FirstOrDefault();

        if (table == null || nextRow == null)
            return String.Empty;

        // FIRST LINE:
        int[] maximumCellWidths = GetMaxCellWidths(table);
        for (int i = 0; i < nextRow.Length; i++)
        {
            if (i == 0 && i == nextRow.Length - 1)
                formattedTable.Append(String.Format("{0}{1}{2}", TOP_LEFT_JOINT, String.Empty.PadLeft(maximumCellWidths[i], HORIZONTAL_LINE), TOP_RIGHT_JOINT));
            else if (i == 0)
                formattedTable.Append(String.Format("{0}{1}", TOP_LEFT_JOINT, String.Empty.PadLeft(maximumCellWidths[i], HORIZONTAL_LINE)));
            else if (i == nextRow.Length - 1)
                formattedTable.AppendLine(String.Format("{0}{1}{2}", TOP_JOINT, String.Empty.PadLeft(maximumCellWidths[i], HORIZONTAL_LINE), TOP_RIGHT_JOINT));
            else
                formattedTable.Append(String.Format("{0}{1}", TOP_JOINT, String.Empty.PadLeft(maximumCellWidths[i], HORIZONTAL_LINE)));
        }

        int rowIndex = 0;
        int lastRowIndex = table.Count - 1;
        foreach (Array thisRow in table)
        {
            // LINE WITH VALUES:
            int cellIndex = 0;
            int lastCellIndex = thisRow.Length - 1;
            foreach (object thisCell in thisRow)
            {
                string thisValue = thisCell.ToString().PadLeft(maximumCellWidths[cellIndex], PADDING);

                if (cellIndex == 0 && cellIndex == lastCellIndex)
                    formattedTable.AppendLine(String.Format("{0}{1}{2}", VERTICAL_LINE, thisValue, VERTICAL_LINE));
                else if (cellIndex == 0)
                    formattedTable.Append(String.Format("{0}{1}", VERTICAL_LINE, thisValue));
                else if (cellIndex == lastCellIndex)
                    formattedTable.AppendLine(String.Format("{0}{1}{2}", VERTICAL_LINE, thisValue, VERTICAL_LINE));
                else
                    formattedTable.Append(String.Format("{0}{1}", VERTICAL_LINE, thisValue));

                cellIndex++;
            }

            previousRow = thisRow;

            // SEPARATING LINE:
            if (rowIndex != lastRowIndex)
            {
                nextRow = table[rowIndex + 1];

                int maximumCells = Math.Max(previousRow.Length, nextRow.Length);
                for (int i = 0; i < maximumCells; i++)
                {
                    if (i == 0 && i == maximumCells - 1)
                    {
                        formattedTable.Append(String.Format("{0}{1}{2}", LEFT_JOINT, String.Empty.PadLeft(maximumCellWidths[i], HORIZONTAL_LINE), RIGHT_JOINT));
                    }
                    else if (i == 0)
                    {
                        formattedTable.Append(String.Format("{0}{1}", LEFT_JOINT, String.Empty.PadLeft(maximumCellWidths[i], HORIZONTAL_LINE)));
                    }
                    else if (i == maximumCells - 1)
                    {
                        if (i > previousRow.Length)
                            formattedTable.AppendLine(String.Format("{0}{1}{2}", TOP_JOINT, String.Empty.PadLeft(maximumCellWidths[i], HORIZONTAL_LINE), TOP_RIGHT_JOINT));
                        else if (i > nextRow.Length)
                            formattedTable.AppendLine(String.Format("{0}{1}{2}", BOTTOM_JOINT, String.Empty.PadLeft(maximumCellWidths[i], HORIZONTAL_LINE), BOTTOM_RIGHT_JOINT));
                        else if (i > previousRow.Length - 1)
                            formattedTable.AppendLine(String.Format("{0}{1}{2}", JOINT, String.Empty.PadLeft(maximumCellWidths[i], HORIZONTAL_LINE), TOP_RIGHT_JOINT));
                        else if (i > nextRow.Length - 1)
                            formattedTable.AppendLine(String.Format("{0}{1}{2}", JOINT, String.Empty.PadLeft(maximumCellWidths[i], HORIZONTAL_LINE), BOTTOM_RIGHT_JOINT));
                        else
                            formattedTable.AppendLine(String.Format("{0}{1}{2}", JOINT, String.Empty.PadLeft(maximumCellWidths[i], HORIZONTAL_LINE), RIGHT_JOINT));
                    }
                    else
                    {
                        if (i > previousRow.Length)
                            formattedTable.Append(String.Format("{0}{1}", TOP_JOINT, String.Empty.PadLeft(maximumCellWidths[i], HORIZONTAL_LINE)));
                        else if (i > nextRow.Length)
                            formattedTable.Append(String.Format("{0}{1}", BOTTOM_JOINT, String.Empty.PadLeft(maximumCellWidths[i], HORIZONTAL_LINE)));
                        else
                            formattedTable.Append(String.Format("{0}{1}", JOINT, String.Empty.PadLeft(maximumCellWidths[i], HORIZONTAL_LINE)));
                    }
                }
            }

            rowIndex++;
        }

        // LAST LINE:
        for (int i = 0; i < previousRow.Length; i++)
        {
            if (i == 0)
                formattedTable.Append(String.Format("{0}{1}", BOTTOM_LEFT_JOINT, String.Empty.PadLeft(maximumCellWidths[i], HORIZONTAL_LINE)));
            else if (i == previousRow.Length - 1)
                formattedTable.AppendLine(String.Format("{0}{1}{2}", BOTTOM_JOINT, String.Empty.PadLeft(maximumCellWidths[i], HORIZONTAL_LINE), BOTTOM_RIGHT_JOINT));
            else
                formattedTable.Append(String.Format("{0}{1}", BOTTOM_JOINT, String.Empty.PadLeft(maximumCellWidths[i], HORIZONTAL_LINE)));
        }

        return formattedTable.ToString();
    }
}