diff --git a/ChangeLogs/2.0.0-ChangeLog.md b/ChangeLogs/2.0.0-ChangeLog.md new file mode 100644 index 0000000..5e29589 --- /dev/null +++ b/ChangeLogs/2.0.0-ChangeLog.md @@ -0,0 +1,61 @@ +# V2.0.0 + +## New Properties + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `Footers` | `string[]` | `Array.Empty()` | The table footers. Footers are not required. | +| `FooterTextAlignmentRight` | `bool` | `false` | When `true`, footer text is right-aligned otherwise left aligned | + +## New Method + +| Method | Description | +|--------|-------------| +| `SetFooters(params string[] footers)` | Sets the table footers. Calling this again will overwrite previous footers. Footers are not required. | + +## New Styling +- Headers now have a double bottom line to seperate them from the rows. +- Now an option to add footers to the table (optional). These are drawn without borders at the bottom. +- Footer text can be left or right aligned. + +```csharp +using ConsoleTable.Text; + +// Setup the table +var table = new Table(); + +// Set headers +table.SetHeaders("Name", "Age", "City"); + +// Add rows +table.AddRow("Alice Cooper", "30", "New York"); + +table.AddRows(new string[][] +{ + new string[] { "Bob", "25", "Los Angeles" }, + new string[] { "Charlie Brown", "47", "Chicago" } +}); + +//Set footers +table.SetFooters("Total: 3", "Total Age: 102"); + +// Display the table +Console.WriteLine(table.ToTable()); +``` + +Output: +``` +┌───────────────┬────────────────┬─────────────┐ +│ Name │ Age │ City │ +├═══════════════┼════════════════┼═════════════┤ +│ Alice Cooper │ 30 │ New York │ +├───────────────┼────────────────┼─────────────┤ +│ Bob │ 25 │ Los Angeles │ +├───────────────┼────────────────┼─────────────┤ +│ Charlie Brown │ 47 │ Chicago │ +└───────────────┴────────────────┴─────────────┘ + Total: 3 Total Age: 102 +``` + +## Performance Improvements +- Slightly improved performance when rendering large tables with many rows and columns. diff --git a/ConsoleTable.Text.Examples/Program.cs b/ConsoleTable.Text.Examples/Program.cs index b57ad4d..2d18ed7 100644 --- a/ConsoleTable.Text.Examples/Program.cs +++ b/ConsoleTable.Text.Examples/Program.cs @@ -8,15 +8,21 @@ static void Main(string[] args) WriteDefaultTableWithProperties(); - WriteTableWithStyling(true, true, 10); + WriteTableWithStyling(true, true, true, 10); - WriteTableWithStyling(false, true, 10); + WriteTableWithStyling(false, true, true, 10); - WriteTableWithStyling(true, false, 10); + WriteTableWithStyling(true, false, true, 10); - WriteTableWithStyling(false, false, 10); + WriteTableWithStyling(true, true, false, 10); - WriteTableWithoutHeaders(); + WriteTableWithStyling(false, false, false, 10); + + WriteTableOnlyHeaders(); + + WriteTableOnlyRows(); + + WriteTableOnlyFooters(); WriteTableMoreHeaders(); @@ -36,8 +42,13 @@ private static void WriteDefaultTable() Console.WriteLine(); Console.WriteLine("Default table:"); + // Setup the table var table = new Table(); + + // Set headers table.SetHeaders("Name", "Age", "City"); + + // Add rows table.AddRow("Alice Cooper", "30", "New York"); table.AddRows(new string[][] { @@ -45,6 +56,10 @@ private static void WriteDefaultTable() new string[] { "Charlie Brown", "47", "Chicago" } }); + // Set footers + table.SetFooters("Total: 3", "Total Age: 102"); + + // Display the table Console.WriteLine(table.ToTable()); Console.WriteLine(); } @@ -70,15 +85,41 @@ private static void WriteDefaultTableWithProperties() Console.WriteLine(); } - private static void WriteTableWithoutHeaders() + private static void WriteTableOnlyRows() { Console.WriteLine(); - Console.WriteLine("Table without headers:"); + Console.WriteLine("Table only rows:"); var table = new Table(); for (int i = 1; i <= 5; i++) + { table.AddRow($"name {i}", (i * 15).ToString()); + } + + Console.WriteLine(table.ToString()); + Console.WriteLine(); + } + + private static void WriteTableOnlyHeaders() + { + Console.WriteLine(); + Console.WriteLine("Table only headers:"); + + var table = new Table(); + table.SetHeaders("Name", "Age", "City"); + + Console.WriteLine(table.ToString()); + Console.WriteLine(); + } + + private static void WriteTableOnlyFooters() + { + Console.WriteLine(); + Console.WriteLine("Table only footers:"); + + var table = new Table(); + table.SetFooters("Total: 3", "Total Age: 102"); Console.WriteLine(table.ToString()); Console.WriteLine(); @@ -145,16 +186,19 @@ private static void WriteTableEachRowRandom() table.AddRow("Mathias", "37", "Oslo", "Norway", "Europe", "Earth", "Solar System"); table.AddRow("Kenny", "55", "Tokyo"); + table.SetFooters("Footer 1", "Footer 2"); + Console.WriteLine(table.ToString()); Console.WriteLine(); } - private static void WriteTableWithStyling(bool headerTextAlignRight, bool rowTextAlignRight, int padding) + private static void WriteTableWithStyling(bool headerTextAlignRight, bool rowTextAlignRight, bool footerTextAlignRight, int padding) { Console.WriteLine(); Console.WriteLine($"Table with following styling:"); Console.WriteLine($"Header text alignment: {(headerTextAlignRight ? "right" : "left")}"); Console.WriteLine($"Row text alignment: {(rowTextAlignRight ? "right" : "left")}"); + Console.WriteLine($"Footer text alignment: {(footerTextAlignRight ? "right" : "left")}"); Console.WriteLine($"Padding: {padding}"); var table = new Table @@ -162,7 +206,8 @@ private static void WriteTableWithStyling(bool headerTextAlignRight, bool rowTex CachingEnabled = true, Padding = padding, HeaderTextAlignmentRight = headerTextAlignRight, - RowTextAlignmentRight = rowTextAlignRight + RowTextAlignmentRight = rowTextAlignRight, + FooterTextAlignmentRight = footerTextAlignRight }; table.SetHeaders("Name", "Age", "City"); @@ -175,6 +220,8 @@ private static void WriteTableWithStyling(bool headerTextAlignRight, bool rowTex table.AddRow($"Very Long Name {i}", (i * 8).ToString(), $"City {i}"); } + table.SetFooters("Footer 1", "Footer 2", "Footer 3"); + Console.WriteLine(table.ToString()); Console.WriteLine(); } @@ -191,6 +238,7 @@ private static void WriteTableFluent() new string[] { "Bob", "25", "Los Angeles" }, new string[] { "Charlie Brown", "47", "Chicago" } ) + .SetFooters("Total: 3", "Total Age: 102") .ToTable(); Console.WriteLine(tableString); @@ -230,6 +278,13 @@ private static void WriteBigTable() } table.Rows = rows; + var footers = new List(); + for (var columnPos = 1; columnPos <= columnCount; columnPos++) + { + footers.Add($"Footer {columnPos}"); + } + table.Footers = footers.ToArray(); + var tableString = table.ToTable(); Console.WriteLine(tableString); diff --git a/ConsoleTable.Text/ConsoleTable.Text.csproj b/ConsoleTable.Text/ConsoleTable.Text.csproj index d0b75ad..bb38db6 100644 --- a/ConsoleTable.Text/ConsoleTable.Text.csproj +++ b/ConsoleTable.Text/ConsoleTable.Text.csproj @@ -5,13 +5,13 @@ ConsoleTable.Text - 1.0.3 + 2.0.0 Bruno Van Thournout - A library for creating a formatted string tables with customizable headers, rows, and alignment options. + A library for creating a formatted string table with customizable headers, footers, rows and easy to use styling options. https://github.com/BrunoVT1992/ConsoleTable https://github.com/BrunoVT1992/ConsoleTable git - console;table;formatting;cli + console;table;tables;consoletable;consoletables;console-table;texttable;text-table;output;format;formatting;cli;string;text;ct;fluent;grid MPL-2.0 README.md diff --git a/ConsoleTable.Text/Table.cs b/ConsoleTable.Text/Table.cs index 6592221..84a111c 100644 --- a/ConsoleTable.Text/Table.cs +++ b/ConsoleTable.Text/Table.cs @@ -54,6 +54,20 @@ public List Rows } } + private string[] _footers = Array.Empty(); + /// + /// Gets or sets the footers of the table. This is a single optional bottom row without a border. + /// + public string[] Footers + { + get => _footers; + set + { + _footers = value ?? Array.Empty(); + ClearCache(); + } + } + private int _padding = 1; /// /// Gets or sets the amount of padding in spaces left and right of the rows cell content. Default is 1 @@ -71,17 +85,17 @@ public int Padding } } - private bool headerTextAlignmentRight; + private bool _headerTextAlignmentRight; /// /// Gets or sets a value indicating whether the header text is aligned to the right or left /// public bool HeaderTextAlignmentRight { get => - headerTextAlignmentRight; + _headerTextAlignmentRight; set { - headerTextAlignmentRight = value; + _headerTextAlignmentRight = value; ClearCache(); } } @@ -89,14 +103,27 @@ public bool HeaderTextAlignmentRight /// /// Gets or sets a value indicating whether the row text is aligned to the right or left /// - private bool rowTextAlignmentRight; + private bool _rowTextAlignmentRight; public bool RowTextAlignmentRight { - get => - rowTextAlignmentRight; + get => _rowTextAlignmentRight; set { - rowTextAlignmentRight = value; + _rowTextAlignmentRight = value; + ClearCache(); + } + } + + /// + /// Gets or sets a value indicating whether the footer text is aligned to the right or left + /// + private bool _footerTextAlignmentRight; + public bool FooterTextAlignmentRight + { + get => _footerTextAlignmentRight; + set + { + _footerTextAlignmentRight = value; ClearCache(); } } @@ -110,6 +137,15 @@ public Table SetHeaders(params string[] headers) return this; } + /// + /// Sets the footers of the table. Overwrites them each time. + /// + public Table SetFooters(params string[] footers) + { + Footers = footers; + return this; + } + /// /// Adds a new row to the table /// @@ -158,18 +194,155 @@ public Table ClearRows() return this; } - private int[] GetMaxCellWidths(List table) + + + /// + /// Clears the cached generated table string + /// + public Table ClearCache() { + _tableCache = null; + return this; + } + + /// + /// Clear all the headers and rows from the table + /// + public Table Clear() + { + SetHeaders(null); + ClearRows(); + SetFooters(null); + ClearCache(); + return this; + } + + /// + /// Generates the formatted table to a string + /// + public string ToTable() + { + if (CachingEnabled && !string.IsNullOrEmpty(_tableCache)) + return _tableCache; + + if (Headers?.Any() != true && Rows?.Any() != true && Footers?.Any() != true) + return string.Empty; + + var formattedTable = new StringBuilder(); + + int[] maximumCellWidths = GetMaxCellWidths(); + + var topLineCreated = false; + var previousRow = Array.Empty(); + var nextRow = Array.Empty(); + + if (Headers?.Any() == true) + { + formattedTable = CreateTopLine(maximumCellWidths, Headers.Count(), formattedTable); + topLineCreated = true; + + formattedTable = CreateValueLine(maximumCellWidths, Headers, HeaderTextAlignmentRight, TableDrawing.VerticalLine, formattedTable); + + previousRow = Headers; + + //When there are no rows immediatly draw the bottom line after the header + if (Rows?.Any() == true) + { + nextRow = Rows.First(); + formattedTable = CreateSeperatorLine(maximumCellWidths, previousRow.Count(), nextRow.Count(), TableDrawing.HorizontalHeaderLine, formattedTable); + } + else + { + formattedTable = CreateBottomLine(maximumCellWidths, Headers.Count(), TableDrawing.HorizontalHeaderLine, formattedTable); + } + } + + if (Rows?.Any() == true) + { + if (!topLineCreated) + { + formattedTable = CreateTopLine(maximumCellWidths, Rows.First().Count(), formattedTable); + topLineCreated = true; + } + + int rowIndex = 0; + int lastRowIndex = Rows.Count - 1; + + for (int i = 0; i < Rows.Count; i++) + { + var row = CleanupRow(Rows[i]); + + formattedTable = CreateValueLine(maximumCellWidths, row, RowTextAlignmentRight, TableDrawing.VerticalLine, formattedTable); + + previousRow = row; + + if (rowIndex != lastRowIndex) + { + nextRow = CleanupRow(Rows[rowIndex + 1]); + + formattedTable = CreateSeperatorLine(maximumCellWidths, previousRow.Count(), nextRow.Count(), TableDrawing.HorizontalLine, formattedTable); + } + + rowIndex++; + } + + formattedTable = CreateBottomLine(maximumCellWidths, previousRow.Count(), TableDrawing.HorizontalLine, formattedTable); + } + + if (Footers?.Any() == true) + { + formattedTable = CreateValueLine(maximumCellWidths, Footers, FooterTextAlignmentRight, TableDrawing.EmptySpace, formattedTable); + } + + var generatedTable = formattedTable.ToString(); + + if (CachingEnabled) + _tableCache = generatedTable; + + return generatedTable; + } + + public override string ToString() + { + return ToTable(); + } + + private static string[] CleanupRow(string[] row) + { + //Weird behaviour with empty rows. So we create 1 column with a space inside. + if (row == null || row.Length <= 0) + return new string[] { " " }; + + return row; + } + + private int[] GetMaxCellWidths() + { + var table = new List(); + + if (Headers?.Length > 0) + { + table.Add(Headers); + } + + if (Rows?.Any() == true) + { + table.AddRange(Rows); + } + + if (Footers?.Length > 0) + { + table.Add(Footers); + } + var maximumColumns = 0; foreach (var row in table) { - if (row.Length > maximumColumns) + if (row != null && row.Length > maximumColumns) maximumColumns = row.Length; } var maximumCellWidths = new int[maximumColumns]; - for (int i = 0; i < maximumCellWidths.Count(); i++) - maximumCellWidths[i] = 0; var paddingCount = 0; if (Padding > 0) @@ -180,6 +353,9 @@ private int[] GetMaxCellWidths(List table) foreach (var row in table) { + if (row == null || row.Length <= 0) + continue; + for (int i = 0; i < row.Length; i++) { var maxWidth = row[i].Length + paddingCount; @@ -209,24 +385,24 @@ private StringBuilder CreateTopLine(int[] maximumCellWidths, int rowColumnCount, return formattedTable; } - private StringBuilder CreateBottomLine(int[] maximumCellWidths, int rowColumnCount, StringBuilder formattedTable) + private StringBuilder CreateBottomLine(int[] maximumCellWidths, int rowColumnCount, char horizontalLine, StringBuilder formattedTable) { for (int i = 0; i < rowColumnCount; i++) { if (i == 0 && i == rowColumnCount - 1) - formattedTable.AppendLine(string.Format("{0}{1}{2}", TableDrawing.BottomLeftJoint, string.Empty.PadLeft(maximumCellWidths[i], TableDrawing.HorizontalLine), TableDrawing.BottomRightJoint)); + formattedTable.AppendLine(string.Format("{0}{1}{2}", TableDrawing.BottomLeftJoint, string.Empty.PadLeft(maximumCellWidths[i], horizontalLine), TableDrawing.BottomRightJoint)); else if (i == 0) - formattedTable.Append(string.Format("{0}{1}", TableDrawing.BottomLeftJoint, string.Empty.PadLeft(maximumCellWidths[i], TableDrawing.HorizontalLine))); + formattedTable.Append(string.Format("{0}{1}", TableDrawing.BottomLeftJoint, string.Empty.PadLeft(maximumCellWidths[i], horizontalLine))); else if (i == rowColumnCount - 1) - formattedTable.AppendLine(string.Format("{0}{1}{2}", TableDrawing.BottomJoint, string.Empty.PadLeft(maximumCellWidths[i], TableDrawing.HorizontalLine), TableDrawing.BottomRightJoint)); + formattedTable.AppendLine(string.Format("{0}{1}{2}", TableDrawing.BottomJoint, string.Empty.PadLeft(maximumCellWidths[i], horizontalLine), TableDrawing.BottomRightJoint)); else - formattedTable.Append(string.Format("{0}{1}", TableDrawing.BottomJoint, string.Empty.PadLeft(maximumCellWidths[i], TableDrawing.HorizontalLine))); + formattedTable.Append(string.Format("{0}{1}", TableDrawing.BottomJoint, string.Empty.PadLeft(maximumCellWidths[i], horizontalLine))); } return formattedTable; } - private StringBuilder CreateValueLine(int[] maximumCellWidths, string[] row, bool alignRight, StringBuilder formattedTable) + private StringBuilder CreateValueLine(int[] maximumCellWidths, string[] row, bool alignRight, string verticalLine, StringBuilder formattedTable) { int cellIndex = 0; int lastCellIndex = row.Length - 1; @@ -244,13 +420,13 @@ private StringBuilder CreateValueLine(int[] maximumCellWidths, string[] row, boo var cellValue = alignRight ? column.PadLeft(restWidth, ' ') : column.PadRight(restWidth, ' '); if (cellIndex == 0 && cellIndex == lastCellIndex) - formattedTable.AppendLine(string.Format("{0}{1}{2}{3}{4}", TableDrawing.VerticalLine, paddingString, cellValue, paddingString, TableDrawing.VerticalLine)); + formattedTable.AppendLine(string.Format("{0}{1}{2}{3}{4}", verticalLine, paddingString, cellValue, paddingString, verticalLine)); else if (cellIndex == 0) - formattedTable.Append(string.Format("{0}{1}{2}{3}", TableDrawing.VerticalLine, paddingString, cellValue, paddingString)); + formattedTable.Append(string.Format("{0}{1}{2}{3}", verticalLine, paddingString, cellValue, paddingString)); else if (cellIndex == lastCellIndex) - formattedTable.AppendLine(string.Format("{0}{1}{2}{3}{4}", TableDrawing.VerticalLine, paddingString, cellValue, paddingString, TableDrawing.VerticalLine)); + formattedTable.AppendLine(string.Format("{0}{1}{2}{3}{4}", verticalLine, paddingString, cellValue, paddingString, verticalLine)); else - formattedTable.Append(string.Format("{0}{1}{2}{3}", TableDrawing.VerticalLine, paddingString, cellValue, paddingString)); + formattedTable.Append(string.Format("{0}{1}{2}{3}", verticalLine, paddingString, cellValue, paddingString)); cellIndex++; } @@ -258,7 +434,7 @@ private StringBuilder CreateValueLine(int[] maximumCellWidths, string[] row, boo return formattedTable; } - private StringBuilder CreateSeperatorLine(int[] maximumCellWidths, int previousRowColumnCount, int rowColumnCount, StringBuilder formattedTable) + private StringBuilder CreateSeperatorLine(int[] maximumCellWidths, int previousRowColumnCount, int rowColumnCount, char horizontalLine, StringBuilder formattedTable) { var maximumCells = Math.Max(previousRowColumnCount, rowColumnCount); @@ -266,137 +442,37 @@ private StringBuilder CreateSeperatorLine(int[] maximumCellWidths, int previousR { if (i == 0 && i == maximumCells - 1) { - formattedTable.AppendLine(string.Format("{0}{1}{2}", TableDrawing.LeftJoint, string.Empty.PadLeft(maximumCellWidths[i], TableDrawing.HorizontalLine), TableDrawing.RightJoint)); + formattedTable.AppendLine(string.Format("{0}{1}{2}", TableDrawing.LeftJoint, string.Empty.PadLeft(maximumCellWidths[i], horizontalLine), TableDrawing.RightJoint)); } else if (i == 0) { - formattedTable.Append(string.Format("{0}{1}", TableDrawing.LeftJoint, string.Empty.PadLeft(maximumCellWidths[i], TableDrawing.HorizontalLine))); + formattedTable.Append(string.Format("{0}{1}", TableDrawing.LeftJoint, string.Empty.PadLeft(maximumCellWidths[i], horizontalLine))); } else if (i == maximumCells - 1) { if (i > previousRowColumnCount) - formattedTable.AppendLine(string.Format("{0}{1}{2}", TableDrawing.TopJoint, string.Empty.PadLeft(maximumCellWidths[i], TableDrawing.HorizontalLine), TableDrawing.TopRightJoint)); + formattedTable.AppendLine(string.Format("{0}{1}{2}", TableDrawing.TopJoint, string.Empty.PadLeft(maximumCellWidths[i], horizontalLine), TableDrawing.TopRightJoint)); else if (i > rowColumnCount) - formattedTable.AppendLine(string.Format("{0}{1}{2}", TableDrawing.BottomJoint, string.Empty.PadLeft(maximumCellWidths[i], TableDrawing.HorizontalLine), TableDrawing.BottomRightJoint)); + formattedTable.AppendLine(string.Format("{0}{1}{2}", TableDrawing.BottomJoint, string.Empty.PadLeft(maximumCellWidths[i], horizontalLine), TableDrawing.BottomRightJoint)); else if (i > previousRowColumnCount - 1) - formattedTable.AppendLine(string.Format("{0}{1}{2}", TableDrawing.MiddleJoint, string.Empty.PadLeft(maximumCellWidths[i], TableDrawing.HorizontalLine), TableDrawing.TopRightJoint)); + formattedTable.AppendLine(string.Format("{0}{1}{2}", TableDrawing.MiddleJoint, string.Empty.PadLeft(maximumCellWidths[i], horizontalLine), TableDrawing.TopRightJoint)); else if (i > rowColumnCount - 1) - formattedTable.AppendLine(string.Format("{0}{1}{2}", TableDrawing.MiddleJoint, string.Empty.PadLeft(maximumCellWidths[i], TableDrawing.HorizontalLine), TableDrawing.BottomRightJoint)); + formattedTable.AppendLine(string.Format("{0}{1}{2}", TableDrawing.MiddleJoint, string.Empty.PadLeft(maximumCellWidths[i], horizontalLine), TableDrawing.BottomRightJoint)); else - formattedTable.AppendLine(string.Format("{0}{1}{2}", TableDrawing.MiddleJoint, string.Empty.PadLeft(maximumCellWidths[i], TableDrawing.HorizontalLine), TableDrawing.RightJoint)); + formattedTable.AppendLine(string.Format("{0}{1}{2}", TableDrawing.MiddleJoint, string.Empty.PadLeft(maximumCellWidths[i], horizontalLine), TableDrawing.RightJoint)); } else { if (i > previousRowColumnCount) - formattedTable.Append(string.Format("{0}{1}", TableDrawing.TopJoint, string.Empty.PadLeft(maximumCellWidths[i], TableDrawing.HorizontalLine))); + formattedTable.Append(string.Format("{0}{1}", TableDrawing.TopJoint, string.Empty.PadLeft(maximumCellWidths[i], horizontalLine))); else if (i > rowColumnCount) - formattedTable.Append(string.Format("{0}{1}", TableDrawing.BottomJoint, string.Empty.PadLeft(maximumCellWidths[i], TableDrawing.HorizontalLine))); + formattedTable.Append(string.Format("{0}{1}", TableDrawing.BottomJoint, string.Empty.PadLeft(maximumCellWidths[i], horizontalLine))); else - formattedTable.Append(string.Format("{0}{1}", TableDrawing.MiddleJoint, string.Empty.PadLeft(maximumCellWidths[i], TableDrawing.HorizontalLine))); + formattedTable.Append(string.Format("{0}{1}", TableDrawing.MiddleJoint, string.Empty.PadLeft(maximumCellWidths[i], horizontalLine))); } } return formattedTable; } - - /// - /// Clears the cached generated table string - /// - public Table ClearCache() - { - _tableCache = null; - return this; - } - - /// - /// Clear all the headers and rows from the table - /// - public Table Clear() - { - SetHeaders(null); - ClearRows(); - ClearCache(); - return this; - } - - /// - /// Generates the formatted table to a string - /// - public string ToTable() - { - if (CachingEnabled && !string.IsNullOrEmpty(_tableCache)) - return _tableCache; - - var table = new List(); - - var firstRowIsHeader = false; - if (Headers?.Any() == true) - { - table.Add(Headers); - firstRowIsHeader = true; - } - - if (Rows?.Any() == true) - { - foreach (var row in Rows) - { - //Weird behaviour with empty rows - if ((row == null || row.Length <= 0) && Headers?.Length > 0) - table.AddRange(new string[][] { new string[] { " " } }); - else - table.Add(row); - } - } - - if (!table.Any()) - return string.Empty; - - var formattedTable = new StringBuilder(); - - var previousRow = table.First(); - var nextRow = table.First(); - - int[] maximumCellWidths = GetMaxCellWidths(table); - - formattedTable = CreateTopLine(maximumCellWidths, nextRow.Count(), formattedTable); - - int rowIndex = 0; - int lastRowIndex = table.Count - 1; - - for (int i = 0; i < table.Count; i++) - { - var row = table[i]; - - var align = RowTextAlignmentRight; - if (i == 0 && firstRowIsHeader) - align = HeaderTextAlignmentRight; - - formattedTable = CreateValueLine(maximumCellWidths, row, align, formattedTable); - - previousRow = row; - - if (rowIndex != lastRowIndex) - { - nextRow = table[rowIndex + 1]; - formattedTable = CreateSeperatorLine(maximumCellWidths, previousRow.Count(), nextRow.Count(), formattedTable); - } - - rowIndex++; - } - - formattedTable = CreateBottomLine(maximumCellWidths, previousRow.Count(), formattedTable); - - var generatedTable = formattedTable.ToString(); - - if (CachingEnabled) - _tableCache = generatedTable; - - return generatedTable; - } - - public override string ToString() - { - return ToTable(); - } } } diff --git a/ConsoleTable.Text/TableDrawing.cs b/ConsoleTable.Text/TableDrawing.cs index 1474b44..d729daf 100644 --- a/ConsoleTable.Text/TableDrawing.cs +++ b/ConsoleTable.Text/TableDrawing.cs @@ -12,6 +12,8 @@ internal static class TableDrawing public const string MiddleJoint = "┼"; public const string RightJoint = "┤"; public const char HorizontalLine = '─'; + public const char HorizontalHeaderLine = '═'; public const string VerticalLine = "│"; + public const string EmptySpace = " "; } } \ No newline at end of file diff --git a/ConsoleTable.slnx b/ConsoleTable.slnx index 717850d..b907c3e 100644 --- a/ConsoleTable.slnx +++ b/ConsoleTable.slnx @@ -8,6 +8,7 @@ + diff --git a/README.md b/README.md index 1420407..87903ad 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ A lightweight .NET library for creating beautifully formatted console tables wit ## Features -- Create formatted tables as a string with headers and data rows +- Create formatted tables as a string with styled headers, footers and rows - Unicode box-drawing characters for clean borders - Automatic column width calculation - Configurable cell padding -- Text alignment options (left/right) for headers and rows +- Text alignment options (left/right) for headers, footers and rows - Easy clearing and reusing of tables - Simple and intuitive API - Optimized for performance @@ -31,7 +31,7 @@ dotnet add package ConsoleTable.Text ### PackageReference ```xml - + ``` ### nuget.org @@ -42,6 +42,7 @@ Download this nuget package from [https://www.nuget.org/packages/ConsoleTable.Te ```csharp using ConsoleTable.Text; +// Setup the table var table = new Table(); // Set headers @@ -56,21 +57,25 @@ table.AddRows(new string[][] new string[] { "Charlie Brown", "47", "Chicago" } }); +//Set footers +table.SetFooters("Total: 3", "Total Age: 102"); + // Display the table Console.WriteLine(table.ToTable()); ``` Output: ``` -┌─────────┬─────┬─────────────┐ -│ Name │ Age │ City │ -├─────────┼─────┼─────────────┤ -│ Alice │ 30 │ New York │ -├─────────┼─────┼─────────────┤ -│ Bob │ 25 │ Los Angeles │ -├─────────┼─────┼─────────────┤ -│ Charlie │ 47 │ Chicago │ -└─────────┴─────┴─────────────┘ +┌───────────────┬────────────────┬─────────────┐ +│ Name │ Age │ City │ +├═══════════════┼════════════════┼═════════════┤ +│ Alice Cooper │ 30 │ New York │ +├───────────────┼────────────────┼─────────────┤ +│ Bob │ 25 │ Los Angeles │ +├───────────────┼────────────────┼─────────────┤ +│ Charlie Brown │ 47 │ Chicago │ +└───────────────┴────────────────┴─────────────┘ + Total: 3 Total Age: 102 ``` ## API Reference @@ -80,10 +85,12 @@ Output: | Property | Type | Default | Description | |----------|------|---------|-------------| | `Headers` | `string[]` | `Array.Empty()` | The table headers. Headers are not required. | -| `Rows` | `List` | `new List()` | All the data rows for the table. Rows are not required. | +| `Rows` | `List` | `new List()` | All the rows for the table. Rows are not required. | +| `Footers` | `string[]` | `Array.Empty()` | The table footers. Footers are not required. | | `Padding` | `int` | `1` | The number of spaces on each side of cell content | | `HeaderTextAlignmentRight` | `bool` | `false` | When `true`, header text is right-aligned otherwise left aligned | | `RowTextAlignmentRight` | `bool` | `false` | When `true`, row text is right-aligned otherwise left aligned | +| `FooterTextAlignmentRight` | `bool` | `false` | When `true`, footer text is right-aligned otherwise left aligned | | `CachingEnabled` | `bool` | `true` | When `true`, the generated table string is cached when the ToTable method is called. Cache will be cleared on any property change or method call. | ### Methods @@ -91,10 +98,11 @@ Output: | Method | Description | |--------|-------------| | `SetHeaders(params string[] headers)` | Sets the table headers. Calling this again will overwrite previous headers. Headers are not required. | -| `AddRow(params string[] row)` | Adds a data row to the table. Rows are not required. | -| `AddRows(params string[][] rows)` | Adds multiple data rows to the table. Rows are not required. | -| `ClearRows()` | Removes all data rows from the table (headers are preserved). | -| `Clear()` | Clear all the headers and rows from the table. | +| `AddRow(params string[] row)` | Adds a row to the table. Rows are not required. | +| `AddRows(params string[][] rows)` | Adds multiple rows to the table. Rows are not required. | +| `ClearRows()` | Removes all rows from the table (headers are preserved). | +| `SetFooters(params string[] footers)` | Sets the table footers. Calling this again will overwrite previous footers. Footers are not required. | +| `Clear()` | Clear all the headers, footers and rows from the table. | | `ClearCache()` | Clears the generated table string cache. This can be done to save memory. | | `ToTable() / ToString()` | Returns the formatted table as a string. | @@ -105,30 +113,39 @@ Output: ```csharp using ConsoleTable.Text; +// Setup the table var table = new Table { Padding = 10 }; +// Set headers table.SetHeaders("Name", "Age", "City"); -table.AddRows( - new string[] { "Alice", "30", "New York" }, +// Add rows +table.AddRow("Alice Cooper", "30", "New York"); +table.AddRows(new string[][] +{ new string[] { "Bob", "25", "Los Angeles" }, - new string[] { "Charlie", "47", "Chicago" } -); + new string[] { "Charlie Brown", "47", "Chicago" } +}); +// Set footers +table.SetFooters("Total: 3", "Total Age: 102"); + +// Display the table Console.WriteLine(table.ToTable()); ``` Output: ``` -┌───────────────────────────┬───────────────────────┬───────────────────────────────┐ -│ Name │ Age │ City │ -├───────────────────────────┼───────────────────────┼───────────────────────────────┤ -│ Alice │ 30 │ New York │ -├───────────────────────────┼───────────────────────┼───────────────────────────────┤ -│ Bob │ 25 │ Los Angeles │ -├───────────────────────────┼───────────────────────┼───────────────────────────────┤ -│ Charlie │ 47 │ Chicago │ -└───────────────────────────┴───────────────────────┴───────────────────────────────┘ +┌─────────────────────────────────┬──────────────────────────────────┬───────────────────────────────┐ +│ Name │ Age │ City │ +├═════════════════════════════════┼══════════════════════════════════┼═══════════════════════════════┤ +│ Alice Cooper │ 30 │ New York │ +├─────────────────────────────────┼──────────────────────────────────┼───────────────────────────────┤ +│ Bob │ 25 │ Los Angeles │ +├─────────────────────────────────┼──────────────────────────────────┼───────────────────────────────┤ +│ Charlie Brown │ 47 │ Chicago │ +└─────────────────────────────────┴──────────────────────────────────┴───────────────────────────────┘ + Total: 3 Total Age: 102 ``` ### Text Alignment Right @@ -136,30 +153,39 @@ Output: ```csharp using ConsoleTable.Text; -var table = new Table { HeaderTextAlignmentRight = true, RowTextAlignmentRight = true }; +// Setup the table +var table = new Table { HeaderTextAlignmentRight = true, RowTextAlignmentRight = true, FooterTextAlignmentRight = true }; +// Set headers table.SetHeaders("Name", "Age", "City"); -table.AddRows( - new string[] { "Alice Cooper", "30", "New York" }, +// Add rows +table.AddRow("Alice Cooper", "30", "New York"); +table.AddRows(new string[][] +{ new string[] { "Bob", "25", "Los Angeles" }, new string[] { "Charlie Brown", "47", "Chicago" } -); +}); + +// Set footers +table.SetFooters("Total: 3", "Total Age: 102"); +// Display the table Console.WriteLine(table.ToTable()); ``` Output: ``` -┌───────────────┬─────┬─────────────┐ -│ Name │ Age │ City │ -├───────────────┼─────┼─────────────┤ -│ Alice Cooper │ 30 │ New York │ -├───────────────┼─────┼─────────────┤ -│ Bob │ 25 │ Los Angeles │ -├───────────────┼─────┼─────────────┤ -│ Charlie Brown │ 47 │ Chicago │ -└───────────────┴─────┴─────────────┘ +┌───────────────┬────────────────┬─────────────┐ +│ Name │ Age │ City │ +├═══════════════┼════════════════┼═════════════┤ +│ Alice Cooper │ 30 │ New York │ +├───────────────┼────────────────┼─────────────┤ +│ Bob │ 25 │ Los Angeles │ +├───────────────┼────────────────┼─────────────┤ +│ Charlie Brown │ 47 │ Chicago │ +└───────────────┴────────────────┴─────────────┘ + Total: 3 Total Age: 102 ``` @@ -191,19 +217,19 @@ Console.WriteLine(table.ToTable()); Output: ``` -┌──────────┬─────┬──────────┐ -│ Name │ Age │ City │ -├──────────┼─────┴──────────┘ +┌──────────┬──────────┬──────────┐ +│ Name │ Age │ City │ +├══════════┼══════════┴══════════┘ │ Alice │ -├──────────┼─────┬──────────┬─────────┐ -│ Bob │ 25 │ Antwerp │ Belgium │ -├──────────┼─────┼──────────┼─────────┘ -│ Charlie │ 47 │ Chicago │ -├──────────┼─────┼──────────┼─────────┬───────────────┐ -│ Karina │ 33 │ Lima │ Peru │ South-America │ -├──────────┼─────┼──────────┴─────────┴───────────────┘ -│ Jenny │ 43 │ -├──────────┼─────┘ +├──────────┼──────────┬──────────┬─────────┐ +│ Bob │ 25 │ Antwerp │ Belgium │ +├──────────┼──────────┼──────────┼─────────┘ +│ Charlie │ 47 │ Chicago │ +├──────────┼──────────┼──────────┼─────────┬───────────────┐ +│ Karina │ 33 │ Lima │ Peru │ South-America │ +├──────────┼──────────┼──────────┴─────────┴───────────────┘ +│ Jenny │ 43 │ +├──────────┼──────────┘ │ John │ ├──────────┤ │ Johny │ @@ -211,15 +237,16 @@ Output: │ │ ├──────────┤ │ │ -├──────────┼─────┬──────────┬─────────┬───────────────┬───────┬──────────────┐ -│ Thomas │ 33 │ Brussels │ Belgium │ Europe │ Earth │ Solar System │ -├──────────┼─────┼──────────┼─────────┼───────────────┼───────┼──────────────┤ -│ Nathalie │ 29 │ Paris │ France │ Europe │ Earth │ Solar System │ -├──────────┼─────┼──────────┼─────────┼───────────────┼───────┼──────────────┤ -│ Mathias │ 37 │ Oslo │ Norway │ Europe │ Earth │ Solar System │ -├──────────┼─────┼──────────┼─────────┴───────────────┴───────┴──────────────┘ -│ Kenny │ 55 │ Tokyo │ -└──────────┴─────┴──────────┘ +├──────────┼──────────┬──────────┬─────────┬───────────────┬───────┬──────────────┐ +│ Thomas │ 33 │ Brussels │ Belgium │ Europe │ Earth │ Solar System │ +├──────────┼──────────┼──────────┼─────────┼───────────────┼───────┼──────────────┤ +│ Nathalie │ 29 │ Paris │ France │ Europe │ Earth │ Solar System │ +├──────────┼──────────┼──────────┼─────────┼───────────────┼───────┼──────────────┤ +│ Mathias │ 37 │ Oslo │ Norway │ Europe │ Earth │ Solar System │ +├──────────┼──────────┼──────────┼─────────┴───────────────┴───────┴──────────────┘ +│ Kenny │ 55 │ Tokyo │ +└──────────┴──────────┴──────────┘ + Footer 1 Footer 2 ``` @@ -228,13 +255,14 @@ Output: ```csharp using ConsoleTable.Text; - var tableString = new Table() + var tableString = new Table() .SetHeaders("Name", "Age", "City") .AddRow("Alice Cooper", "30", "New York") .AddRows( new string[] { "Bob", "25", "Los Angeles" }, new string[] { "Charlie Brown", "47", "Chicago" } ) + .SetFooters("Total: 3", "Total Age: 102") .ToTable(); Console.WriteLine(tableString); @@ -242,18 +270,89 @@ Console.WriteLine(tableString); Output: ``` -┌───────────────┬─────┬─────────────┐ -│ Name │ Age │ City │ -├───────────────┼─────┼─────────────┤ -│ Alice Cooper │ 30 │ New York │ -├───────────────┼─────┼─────────────┤ -│ Bob │ 25 │ Los Angeles │ -├───────────────┼─────┼─────────────┤ -│ Charlie Brown │ 47 │ Chicago │ -└───────────────┴─────┴─────────────┘ +┌───────────────┬────────────────┬─────────────┐ +│ Name │ Age │ City │ +├═══════════════┼════════════════┼═════════════┤ +│ Alice Cooper │ 30 │ New York │ +├───────────────┼────────────────┼─────────────┤ +│ Bob │ 25 │ Los Angeles │ +├───────────────┼────────────────┼─────────────┤ +│ Charlie Brown │ 47 │ Chicago │ +└───────────────┴────────────────┴─────────────┘ + Total: 3 Total Age: 102 ``` +### Header only + +```csharp +using ConsoleTable.Text; + +var table = new Table(); +table.SetHeaders("Name", "Age", "City"); + +Console.WriteLine(table.ToString()); +``` + +Output: +``` +┌──────┬─────┬──────┐ +│ Name │ Age │ City │ +└══════┴═════┴══════┘ +``` + +### Rows only + +```csharp +using ConsoleTable.Text; + + var table = new Table(); + +for (int i = 1; i <= 5; i++) +{ + table.AddRow($"name {i}", (i * 15).ToString()); +} + +Console.WriteLine(table.ToString()); +``` + +Output: +``` +┌────────┬────┐ +│ name 1 │ 15 │ +├────────┼────┤ +│ name 2 │ 30 │ +├────────┼────┤ +│ name 3 │ 45 │ +├────────┼────┤ +│ name 4 │ 60 │ +├────────┼────┤ +│ name 5 │ 75 │ +└────────┴────┘ +``` + +### Footers only + +```csharp +using ConsoleTable.Text; + +var table = new Table(); +table.SetFooters("Total: 3", "Total Age: 102"); + +Console.WriteLine(table.ToString()); +``` + +Output: +``` + Total: 3 Total Age: 102 +``` + +## Feature Requests & Bug Reports +If you want to log a bug or request a new feature, please do so by creating an issue on GitHub: [https://github.com/BrunoVT1992/ConsoleTable/issues](https://github.com/BrunoVT1992/ConsoleTable/issues) ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. \ No newline at end of file +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Find me on +- GitHub: [https://github.com/BrunoVT1992](https://github.com/BrunoVT1992) +- [https://brunovt.be/](https://brunovt.be/) \ No newline at end of file diff --git a/Tests/ConsoleTable.Text.Tests/TableTests.cs b/Tests/ConsoleTable.Text.Tests/TableTests.cs index 1ad5f3e..25499be 100644 --- a/Tests/ConsoleTable.Text.Tests/TableTests.cs +++ b/Tests/ConsoleTable.Text.Tests/TableTests.cs @@ -4,61 +4,61 @@ namespace ConsoleTable.Text.Tests; public class TableTests { - [Theory] - [InlineData(true)] - [InlineData(false)] - public void PerformanceCheck(bool cacheEnabled) + #region Headers + [Fact] + public void SetHeaders_Success() { - var table = new Table - { - CachingEnabled = cacheEnabled, - HeaderTextAlignmentRight = true, - RowTextAlignmentRight = false, - Padding = 5 - }; + var table = new Table(); + table.SetHeaders("Name", "Age"); - var columnCount = 100; - var headers = new List(); - for (var columnPos = 1; columnPos <= columnCount; columnPos++) - { - headers.Add($"Header {columnPos}"); - } - table.Headers = headers.ToArray(); + var result = table.ToTable(); - var rows = new List(); - for (var rowPos = 1; rowPos <= 100000; rowPos++) - { - var row = new string[columnCount]; - for (var columnPos = 1; columnPos <= columnCount; columnPos++) - { - row[columnPos - 1] = $"Row {rowPos} -> Column {columnPos}"; - } - rows.Add(row); - } - table.Rows = rows; + Assert.Contains("Name", result); + Assert.Contains("Age", result); + } - var tableResult1 = table.ToTable(); - Assert.NotEmpty(tableResult1); + [Fact] + public void SetHeaders_OverwritesPreviousHeaders() + { + var table = new Table(); + table.SetHeaders("Old1", "Old2"); + table.SetHeaders("New1", "New2"); - var tableResult2 = table.ToTable(); - Assert.NotEmpty(tableResult2); + var result = table.ToTable(); + + Assert.DoesNotContain("Old1", result); + Assert.DoesNotContain("Old2", result); + Assert.Contains("New1", result); + Assert.Contains("New2", result); } [Fact] - public void ToTable_EmptyTable_ReturnsEmptyString() + public void SetHeaders_Null() { var table = new Table(); + table.SetHeaders(null); var result = table.ToTable(); - Assert.Equal(string.Empty, result); + Assert.Empty(result); } [Fact] - public void ToTable_WithHeadersOnly_ReturnsFormattedTable() + public void SetHeaders_Empty() { var table = new Table(); - table.SetHeaders("Name", "Age"); + table.SetHeaders([]); + + var result = table.ToTable(); + + Assert.Empty(result); + } + + [Fact] + public void Headers_Success() + { + var table = new Table(); + table.Headers = new[] { "Name", "Age" }; var result = table.ToTable(); @@ -67,7 +67,39 @@ public void ToTable_WithHeadersOnly_ReturnsFormattedTable() } [Fact] - public void ToTable_WithRowsOnly_ReturnsFormattedTable() + public void Headers_SetProperty_OverwritesPreviousHeaders() + { + var table = new Table(); + table.Headers = new[] { "Old1", "Old2" }; + table.Headers = new[] { "New1", "New2" }; + + var result = table.ToTable(); + + Assert.DoesNotContain("Old1", result); + Assert.DoesNotContain("Old2", result); + Assert.Contains("New1", result); + Assert.Contains("New2", result); + } + + [Fact] + public void Headers_SetHeadersProperty_ClearsCache() + { + var table = new Table(); + table.Headers = new[] { "Header1" }; + var firstResult = table.ToTable(); + + table.Headers = new[] { "Header2" }; + var secondResult = table.ToTable(); + + Assert.Contains("Header1", firstResult); + Assert.DoesNotContain("Header1", secondResult); + Assert.Contains("Header2", secondResult); + } + #endregion + + #region Rows + [Fact] + public void AddRow_ReturnsFormattedTable() { var table = new Table(); table.AddRow("John", "30"); @@ -98,12 +130,13 @@ public void ToTable_WithHeadersAndRows_ReturnsFormattedTable() } [Fact] - public void ClearRows_RemovesAllRows() + public void ClearRows_RemovesAllRows_KeepsHeadersAndFooters() { var table = new Table(); table.SetHeaders("Name", "Age"); table.AddRow("John", "30"); table.AddRow("Jane", "25"); + table.SetFooters("Footer1", "Footer2"); table.ClearRows(); @@ -113,25 +146,12 @@ public void ClearRows_RemovesAllRows() Assert.Contains("Age", result); Assert.DoesNotContain("John", result); Assert.DoesNotContain("Jane", result); + Assert.Contains("Footer1", result); + Assert.Contains("Footer2", result); } [Fact] - public void Clear_IsEmpty() - { - var table = new Table(); - table.SetHeaders("Name", "Age"); - table.AddRow("John", "30"); - table.AddRow("Jane", "25"); - - table.Clear(); - - var result = table.ToTable(); - - Assert.Empty(result); - } - - [Fact] - public void ClearRows_ThenAddNewRows_Works() + public void ClearRows_ThenAddNewRows() { var table = new Table(); table.SetHeaders("Name"); @@ -146,172 +166,196 @@ public void ClearRows_ThenAddNewRows_Works() Assert.Contains("Jane", result); } - [Theory] - [InlineData(true, true, 10)] - [InlineData(false, false, 0)] - [InlineData(true, true, 0)] - [InlineData(true, false, 10)] - [InlineData(false, true, 10)] - public void ToTable_WithStyling(bool headerTextAlignRight, bool rowTextAlignRight, int padding) + [Fact] + public void Rows_SetRowsProperty() { - var table = new Table + var table = new Table(); + table.Rows = new List { - HeaderTextAlignmentRight = headerTextAlignRight, - RowTextAlignmentRight = rowTextAlignRight, - Padding = padding, + new[] { "John", "30" }, + new[] { "Jane", "25" } }; - table.SetHeaders("Name"); - table.AddRow("John"); - table.AddRow("Jane"); - var result = table.ToTable(); - Assert.Contains("Name", result); Assert.Contains("John", result); Assert.Contains("Jane", result); + Assert.Contains("30", result); + Assert.Contains("25", result); } [Fact] - public void Padding_AffectsTableWidth() + public void Rows_OverwritesPreviousRows() { - var table1 = new Table { Padding = 1 }; - table1.SetHeaders("Name"); - table1.AddRow("Test"); - - var table2 = new Table { Padding = 5 }; - table2.SetHeaders("Name"); - table2.AddRow("Test"); - - var result1 = table1.ToTable(); - var result2 = table2.ToTable(); + var table = new Table(); + table.Rows = new List { new[] { "OldRow" } }; + table.Rows = new List { new[] { "NewRow" } }; - // Table with more padding should have longer lines - var lines1 = result1.Split('\n', StringSplitOptions.RemoveEmptyEntries); - var lines2 = result2.Split('\n', StringSplitOptions.RemoveEmptyEntries); + var result = table.ToTable(); - Assert.True(lines2[0].Length > lines1[0].Length); + Assert.DoesNotContain("OldRow", result); + Assert.Contains("NewRow", result); } [Fact] - public void Padding_ZeroPadding_Works() + public void Rows_SetNull_CreatesEmptyList() { - var table = new Table { Padding = 0 }; - table.SetHeaders("Name"); - table.AddRow("Test"); + var table = new Table(); + table.AddRow("InitialRow"); + table.Rows = null; var result = table.ToTable(); - Assert.Contains("Name", result); - Assert.Contains("Test", result); + Assert.Empty(result); + Assert.NotNull(table.Rows); + Assert.Empty(table.Rows); } [Fact] - public void HeaderTextAlignRight_DefaultValue_IsFalse() + public void AddRow_MultipleTimes_AddsAllRows() { var table = new Table(); + table.AddRow("Row1"); + table.AddRow("Row2"); + table.AddRow("Row3"); - Assert.False(table.HeaderTextAlignmentRight); + var result = table.ToTable(); + + Assert.Contains("Row1", result); + Assert.Contains("Row2", result); + Assert.Contains("Row3", result); } [Fact] - public void HeaderTextAlignRight_CanBeSet() + public void AddRows_AddsMultipleRows() { - var table = new Table { HeaderTextAlignmentRight = true }; + var table = new Table(); + table.AddRows( + new[] { "Row1Col1", "Row1Col2" }, + new[] { "Row2Col1", "Row2Col2" }, + new[] { "Row3Col1", "Row3Col2" } + ); - Assert.True(table.HeaderTextAlignmentRight); + var result = table.ToTable(); + + Assert.Contains("Row1Col1", result); + Assert.Contains("Row2Col1", result); + Assert.Contains("Row3Col1", result); } [Fact] - public void RowTextAlignRight_DefaultValue_IsFalse() + public void AddRows_AppendsToExistingRows() { var table = new Table(); + table.AddRow("ExistingRow"); + table.AddRows( + new[] { "NewRow1" }, + new[] { "NewRow2" } + ); - Assert.False(table.RowTextAlignmentRight); + var result = table.ToTable(); + + Assert.Contains("ExistingRow", result); + Assert.Contains("NewRow1", result); + Assert.Contains("NewRow2", result); } [Fact] - public void RowTextAlignRight_CanBeSet() + public void AddRows_WithNull_DoesNotThrow() { - var table = new Table { RowTextAlignmentRight = true }; + var table = new Table(); + table.AddRow("ExistingRow"); - Assert.True(table.RowTextAlignmentRight); + var exception = Record.Exception(() => table.AddRows(null)); + + Assert.Null(exception); + var result = table.ToTable(); + Assert.Contains("ExistingRow", result); } [Fact] - public void ToTable_WithSingleColumn_ReturnsProperCorners() + public void AddRows_ClearsCache() { var table = new Table(); - table.SetHeaders("Single"); + table.AddRow("InitialRow"); + var firstResult = table.ToTable(); - var result = table.ToTable(); + table.AddRows(new[] { "NewRow" }); + var secondResult = table.ToTable(); - // Should have proper corners for single column - Assert.Contains("┌", result); - Assert.Contains("┐", result); - Assert.Contains("└", result); - Assert.Contains("┘", result); + Assert.DoesNotContain("NewRow", firstResult); + Assert.Contains("NewRow", secondResult); } + #endregion + #region Footers [Fact] - public void ToTable_MoreHeadersThanRowColumns_HandlesCorrectly() + public void SetFooters_Success() { var table = new Table(); - table.SetHeaders("Name", "Date", "Number", "Id"); - table.AddRow("name 1", "date 1", "1"); - table.AddRow("name 2", "date 2"); - table.AddRow("name 3"); + table.SetFooters("Footer1", "Footer2"); var result = table.ToTable(); - Assert.Contains("Name", result); - Assert.Contains("Date", result); - Assert.Contains("Number", result); - Assert.Contains("Id", result); - Assert.Contains("name 1", result); - Assert.Contains("name 2", result); - Assert.Contains("name 3", result); + Assert.Contains("Footer1", result); + Assert.Contains("Footer2", result); } [Fact] - public void ToTable_LessHeadersThanRowColumns_HandlesCorrectly() + public void SetFooters_OverwritesPreviousFooters() { var table = new Table(); - table.SetHeaders("Name"); - table.AddRow("name 1", "date 1"); - table.AddRow("name 2", "date 2", "1"); + table.SetFooters("OldFooter1", "OldFooter2"); + table.SetFooters("NewFooter1", "NewFooter2"); var result = table.ToTable(); - Assert.Contains("Name", result); - Assert.Contains("name 1", result); - Assert.Contains("date 1", result); - Assert.Contains("name 2", result); - Assert.Contains("date 2", result); - Assert.Contains("1", result); + Assert.DoesNotContain("OldFooter1", result); + Assert.DoesNotContain("OldFooter2", result); + Assert.Contains("NewFooter1", result); + Assert.Contains("NewFooter2", result); } [Fact] - public void ToTable_VaryingRowWidths_HandlesCorrectly() + public void SetFooters_Null() { var table = new Table(); - table.AddRow("short"); - table.AddRow("much longer text here"); - table.AddRow("mid length"); + table.SetFooters(null); var result = table.ToTable(); - Assert.Contains("short", result); - Assert.Contains("much longer text here", result); - Assert.Contains("mid length", result); + Assert.Empty(result); } [Fact] - public void SetHeaders_OverwritesPreviousHeaders() + public void SetFooters_Empty() { var table = new Table(); - table.SetHeaders("Old1", "Old2"); - table.SetHeaders("New1", "New2"); + table.SetFooters([]); + + var result = table.ToTable(); + + Assert.Empty(result); + } + + [Fact] + public void Footers_Success() + { + var table = new Table(); + table.Footers = new[] { "Footer1", "Footer2" }; + + var result = table.ToTable(); + + Assert.Contains("Footer1", result); + Assert.Contains("Footer2", result); + } + + [Fact] + public void Footers_SetProperty_OverwritesPreviousFooters() + { + var table = new Table(); + table.Footers = new[] { "Old1", "Old2" }; + table.Footers = new[] { "New1", "New2" }; var result = table.ToTable(); @@ -322,74 +366,116 @@ public void SetHeaders_OverwritesPreviousHeaders() } [Fact] - public void AddRow_MultipleTimes_AddsAllRows() + public void Footers_SetFootersProperty_ClearsCache() { var table = new Table(); - table.AddRow("Row1"); - table.AddRow("Row2"); - table.AddRow("Row3"); + table.Footers = new[] { "Footer1" }; + var firstResult = table.ToTable(); + + table.Footers = new[] { "Footer2" }; + var secondResult = table.ToTable(); + + Assert.Contains("Footer1", firstResult); + Assert.DoesNotContain("Footer1", secondResult); + Assert.Contains("Footer2", secondResult); + } + #endregion + + #region Styling + [Theory] + [InlineData(true, true, true, 10)] + [InlineData(false, false, false, 0)] + [InlineData(true, true, true, 0)] + [InlineData(false, true, true, 10)] + [InlineData(true, false, true, 10)] + [InlineData(true, true, false, 10)] + public void ToTable_WithStyling(bool headerTextAlignRight, bool rowTextAlignRight, bool footerTextAlignRight, int padding) + { + var table = new Table + { + HeaderTextAlignmentRight = headerTextAlignRight, + RowTextAlignmentRight = rowTextAlignRight, + FooterTextAlignmentRight = footerTextAlignRight, + Padding = padding, + }; + + table.SetHeaders("Name"); + table.AddRow("John"); + table.AddRow("Jane"); + table.SetFooters("Footer"); var result = table.ToTable(); - Assert.Contains("Row1", result); - Assert.Contains("Row2", result); - Assert.Contains("Row3", result); + Assert.Contains("Name", result); + Assert.Contains("John", result); + Assert.Contains("Jane", result); + Assert.Contains("Footer", result); } [Fact] - public void ToTable_ContainsTableBorderCharacters() + public void Padding_AffectsTableWidth() { - var table = new Table(); - table.SetHeaders("Header"); - table.AddRow("Value"); + var table1 = new Table { Padding = 1 }; + table1.SetHeaders("Name"); + table1.AddRow("Test"); - var result = table.ToTable(); + var table2 = new Table { Padding = 5 }; + table2.SetHeaders("Name"); + table2.AddRow("Test"); - // Check for various border characters - Assert.Contains("│", result); // Vertical line - Assert.Contains("─", result); // Horizontal line - Assert.Contains("├", result); // Left joint - Assert.Contains("┤", result); // Right joint + var result1 = table1.ToTable(); + var result2 = table2.ToTable(); + + // Table with more padding should have longer lines + var lines1 = result1.Split('\n', StringSplitOptions.RemoveEmptyEntries); + var lines2 = result2.Split('\n', StringSplitOptions.RemoveEmptyEntries); + + Assert.True(lines2[0].Length > lines1[0].Length); } [Fact] - public void ToTable_MultipleColumns_ContainsMiddleJoint() + public void Padding_ZeroPadding_Works() { - var table = new Table(); - table.SetHeaders("Col1", "Col2"); - table.AddRow("Val1", "Val2"); + var table = new Table { Padding = 0 }; + table.SetHeaders("Name"); + table.AddRow("Test"); var result = table.ToTable(); - Assert.Contains("┼", result); // Middle joint + Assert.Contains("Name", result); + Assert.Contains("Test", result); } [Fact] - public void ToTable_SingleRowSingleColumn_ReturnsValidTable() + public void HeaderTextAlignRight_DefaultValue_IsFalse() { var table = new Table(); - table.AddRow("X"); - var result = table.ToTable(); + Assert.False(table.HeaderTextAlignmentRight); + } - Assert.Contains("X", result); - Assert.Contains("┌", result); - Assert.Contains("┐", result); - Assert.Contains("└", result); - Assert.Contains("┘", result); + [Fact] + public void HeaderTextAlignRight_CanBeSet() + { + var table = new Table { HeaderTextAlignmentRight = true }; + + Assert.True(table.HeaderTextAlignmentRight); } [Fact] - public void ToTable_EmptyStringValues_HandlesCorrectly() + public void RowTextAlignRight_DefaultValue_IsFalse() { var table = new Table(); - table.SetHeaders("Name", "Value"); - table.AddRow("", ""); - var result = table.ToTable(); + Assert.False(table.RowTextAlignmentRight); + } - Assert.Contains("Name", result); - Assert.Contains("Value", result); + [Fact] + public void RowTextAlignRight_CanBeSet() + { + var table = new Table { RowTextAlignmentRight = true }; + + Assert.True(table.RowTextAlignmentRight); } [Fact] @@ -433,203 +519,258 @@ public void RowTextAlignRight_True_AlignsToDifferentPosition() } [Fact] - public void Headers_SetHeadersProperty() + public void FooterTextAlignRight_True_AlignsToDifferentPosition() { - var table = new Table(); - table.Headers = new[] { "Name", "Age" }; + var tableLeft = new Table { FooterTextAlignmentRight = false }; + tableLeft.SetFooters("F"); + tableLeft.AddRow("VeryLongValue"); - var result = table.ToTable(); + var tableRight = new Table { FooterTextAlignmentRight = true }; + tableRight.SetFooters("F"); + tableRight.AddRow("VeryLongValue"); - Assert.Contains("Name", result); - Assert.Contains("Age", result); + var resultLeft = tableLeft.ToTable(); + var resultRight = tableRight.ToTable(); + + // Both should contain the footer, but in different positions + Assert.Contains("F", resultLeft); + Assert.Contains("F", resultRight); + Assert.NotEqual(resultLeft, resultRight); } + #endregion + #region Cache [Fact] - public void Headers_SetProperty_OverwritesPreviousHeaders() + public void ClearCache() { - var table = new Table(); - table.Headers = new[] { "Old1", "Old2" }; - table.Headers = new[] { "New1", "New2" }; + var table = new Table + { + CachingEnabled = true + }; - var result = table.ToTable(); + table.AddRow("1"); + var firstResult = table.ToTable(); - Assert.DoesNotContain("Old1", result); - Assert.DoesNotContain("Old2", result); - Assert.Contains("New1", result); - Assert.Contains("New2", result); + table.ClearCache(); + var secondResult = table.ToTable(); + + Assert.Contains("1", firstResult); + Assert.Contains("1", secondResult); } - [Fact] - public void Headers_SetHeadersProperty_ClearsCache() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CachingEnabled(bool cachingEnabled) { - var table = new Table(); - table.Headers = new[] { "Header1" }; - var firstResult = table.ToTable(); + var table = new Table + { + CachingEnabled = cachingEnabled + }; - table.Headers = new[] { "Header2" }; + table.AddRow("1"); + + var firstResult = table.ToTable(); var secondResult = table.ToTable(); - Assert.Contains("Header1", firstResult); - Assert.DoesNotContain("Header1", secondResult); - Assert.Contains("Header2", secondResult); + Assert.Contains("1", firstResult); + Assert.Contains("1", secondResult); } + #endregion - [Fact] - public void Rows_SetRowsProperty() + #region Performance + [Theory] + [InlineData(true)] + [InlineData(false)] + public void PerformanceCheck(bool cacheEnabled) { - var table = new Table(); - table.Rows = new List + var table = new Table { - new[] { "John", "30" }, - new[] { "Jane", "25" } + CachingEnabled = cacheEnabled, + HeaderTextAlignmentRight = true, + RowTextAlignmentRight = false, + Padding = 5 }; - var result = table.ToTable(); + var columnCount = 100; + var headers = new List(); + for (var columnPos = 1; columnPos <= columnCount; columnPos++) + { + headers.Add($"Header {columnPos}"); + } + table.Headers = headers.ToArray(); - Assert.Contains("John", result); - Assert.Contains("Jane", result); - Assert.Contains("30", result); - Assert.Contains("25", result); + var rows = new List(); + for (var rowPos = 1; rowPos <= 100000; rowPos++) + { + var row = new string[columnCount]; + for (var columnPos = 1; columnPos <= columnCount; columnPos++) + { + row[columnPos - 1] = $"Row {rowPos} -> Column {columnPos}"; + } + rows.Add(row); + } + table.Rows = rows; + + var footers = new List(); + for (var columnPos = 1; columnPos <= columnCount; columnPos++) + { + footers.Add($"Footer {columnPos}"); + } + table.Footers = footers.ToArray(); + + var tableResult1 = table.ToTable(); + Assert.NotEmpty(tableResult1); + + var tableResult2 = table.ToTable(); + Assert.NotEmpty(tableResult2); } + #endregion + #region General [Fact] - public void Rows_SetRowsProperty_OverwritesPreviousRows() + public void ToTable_EmptyTable_ReturnsEmptyString() { var table = new Table(); - table.Rows = new List { new[] { "OldRow" } }; - table.Rows = new List { new[] { "NewRow" } }; var result = table.ToTable(); - Assert.DoesNotContain("OldRow", result); - Assert.Contains("NewRow", result); + Assert.Equal(string.Empty, result); } [Fact] - public void Rows_SetRowsProperty_ClearsCache() + public void ToTable_WithSingleColumn_ReturnsProperCorners() { var table = new Table(); - table.Rows = new List { new[] { "Row1" } }; - var firstResult = table.ToTable(); + table.SetHeaders("Single"); - table.Rows = new List { new[] { "Row2" } }; - var secondResult = table.ToTable(); + var result = table.ToTable(); - Assert.Contains("Row1", firstResult); - Assert.DoesNotContain("Row1", secondResult); - Assert.Contains("Row2", secondResult); + // Should have proper corners for single column + Assert.Contains("┌", result); + Assert.Contains("┐", result); + Assert.Contains("└", result); + Assert.Contains("┘", result); } [Fact] - public void Rows_SetNull_CreatesEmptyList() + public void ToTable_MoreHeadersThanRowColumns_HandlesCorrectly() { var table = new Table(); - table.AddRow("InitialRow"); - table.Rows = null; + table.SetHeaders("Name", "Date", "Number", "Id"); + table.AddRow("name 1", "date 1", "1"); + table.AddRow("name 2", "date 2"); + table.AddRow("name 3"); + table.SetFooters("Footer 1"); var result = table.ToTable(); - Assert.Empty(result); - Assert.NotNull(table.Rows); - Assert.Empty(table.Rows); + Assert.Contains("Name", result); + Assert.Contains("Date", result); + Assert.Contains("Number", result); + Assert.Contains("Id", result); + Assert.Contains("name 1", result); + Assert.Contains("name 2", result); + Assert.Contains("name 3", result); + Assert.Contains("Footer 1", result); } [Fact] - public void AddRows_AddsMultipleRows() + public void ToTable_LessHeadersThanRowColumns_HandlesCorrectly() { var table = new Table(); - table.AddRows( - new[] { "Row1Col1", "Row1Col2" }, - new[] { "Row2Col1", "Row2Col2" }, - new[] { "Row3Col1", "Row3Col2" } - ); + table.SetHeaders("Name"); + table.AddRow("name 1", "date 1"); + table.AddRow("name 2", "date 2", "city 1"); + table.SetFooters("Footer 1", "Footer 2", "Footer 3", "Footer 4"); var result = table.ToTable(); - Assert.Contains("Row1Col1", result); - Assert.Contains("Row2Col1", result); - Assert.Contains("Row3Col1", result); + Assert.Contains("Name", result); + Assert.Contains("name 1", result); + Assert.Contains("date 1", result); + Assert.Contains("name 2", result); + Assert.Contains("date 2", result); + Assert.Contains("city 1", result); + Assert.Contains("Footer 1", result); + Assert.Contains("Footer 2", result); + Assert.Contains("Footer 3", result); + Assert.Contains("Footer 4", result); } [Fact] - public void AddRows_AppendsToExistingRows() + public void ToTable_VaryingRowWidths_HandlesCorrectly() { var table = new Table(); - table.AddRow("ExistingRow"); - table.AddRows( - new[] { "NewRow1" }, - new[] { "NewRow2" } - ); + table.AddRow("short"); + table.AddRow("much longer text here"); + table.AddRow("mid length"); var result = table.ToTable(); - Assert.Contains("ExistingRow", result); - Assert.Contains("NewRow1", result); - Assert.Contains("NewRow2", result); + Assert.Contains("short", result); + Assert.Contains("much longer text here", result); + Assert.Contains("mid length", result); } [Fact] - public void AddRows_WithNull_DoesNotThrow() + public void ToTable_SingleRowSingleColumn_ReturnsValidTable() { var table = new Table(); - table.AddRow("ExistingRow"); - - var exception = Record.Exception(() => table.AddRows(null)); + table.AddRow("X"); - Assert.Null(exception); var result = table.ToTable(); - Assert.Contains("ExistingRow", result); + + Assert.Contains("X", result); + Assert.Contains("┌", result); + Assert.Contains("┐", result); + Assert.Contains("└", result); + Assert.Contains("┘", result); } [Fact] - public void AddRows_ClearsCache() + public void ToTable_EmptyStringValues_HandlesCorrectly() { var table = new Table(); - table.AddRow("InitialRow"); - var firstResult = table.ToTable(); + table.SetHeaders("Name", "Value"); + table.AddRow("", ""); + table.SetFooters("Footer"); - table.AddRows(new[] { "NewRow" }); - var secondResult = table.ToTable(); + var result = table.ToTable(); - Assert.DoesNotContain("NewRow", firstResult); - Assert.Contains("NewRow", secondResult); + Assert.Contains("Name", result); + Assert.Contains("Value", result); + Assert.Contains("Footer", result); } [Fact] - public void ClearCache() + public void ToTable_ContainsTableBorderCharacters() { - var table = new Table - { - CachingEnabled = true - }; - - table.AddRow("1"); - var firstResult = table.ToTable(); + var table = new Table(); + table.SetHeaders("Header"); + table.AddRow("Value"); + table.SetFooters("Footer"); - table.ClearCache(); - var secondResult = table.ToTable(); + var result = table.ToTable(); - Assert.Contains("1", firstResult); - Assert.Contains("1", secondResult); + // Check for various border characters + Assert.Contains("│", result); // Vertical line + Assert.Contains("─", result); // Horizontal line + Assert.Contains("├", result); // Left joint + Assert.Contains("┤", result); // Right joint } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CachingEnabled(bool cachingEnabled) + [Fact] + public void ToTable_MultipleColumns_ContainsMiddleJoint() { - var table = new Table - { - CachingEnabled = cachingEnabled - }; - - table.AddRow("1"); + var table = new Table(); + table.SetHeaders("Col1", "Col2"); + table.AddRow("Val1", "Val2"); + table.SetFooters("Footer1", "Footer2"); - var firstResult = table.ToTable(); - var secondResult = table.ToTable(); + var result = table.ToTable(); - Assert.Contains("1", firstResult); - Assert.Contains("1", secondResult); + Assert.Contains("┼", result); // Middle joint } [Fact] @@ -638,6 +779,7 @@ public void ToString_ReturnsFormattedTable() var table = new Table(); table.SetHeaders("Name", "Age"); table.AddRow("John", "30"); + table.SetFooters("Footer1", "Footer2"); var result = table.ToString(); @@ -646,4 +788,20 @@ public void ToString_ReturnsFormattedTable() Assert.Contains("John", result); Assert.Contains("30", result); } + + [Fact] + public void Clear_IsEmpty() + { + var table = new Table(); + table.SetHeaders("Name", "Age"); + table.AddRow("John", "30"); + table.SetFooters("Footer1", "Footer2"); + + table.Clear(); + + var result = table.ToTable(); + + Assert.Empty(result); + } + #endregion }