using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Linq.Expressions; #nullable enable namespace LibationCli; public enum Justify { Left, Right, Center } public class TextTableOptions { public Justify Justify { get; set; } public Justify CenterTiebreak { get; set; } public char PaddingCharacter { get; set; } = ' '; public int SideBorderPadding { get; set; } = 1; public int IntercellPadding { get; set; } = 1; public bool DrawBorder { get; set; } = true; public bool DrawHeader { get; set; } = true; public BorderDefinition Border { get; set; } = BorderDefinition.LightRounded; } public record BorderDefinition { public char Vertical { get; set; } public char Horizontal { get; set; } public char VerticalSeparator { get; set; } public char HorizontalSeparator { get; set; } public char CornerTopLeft { get; set; } public char CornerTopRight { get; set; } public char CornerBottomLeft { get; set; } public char CornerBottomRight { get; set; } public char Tee { get; set; } public char TeeTop { get; set; } public char TeeBottom { get; set; } public char TeeLeft { get; set; } public char TeeRight { get; set; } public BorderDefinition( char vertical, char horizontal, char verticalSeparator, char horizontalSeparator, char cornerTopLef, char cornerTopRight, char cornerBottomLeft, char cornerBottomRight, char tee, char teeTop, char teeBottom, char teeLeft, char teeRight) { Vertical = vertical; Horizontal = horizontal; VerticalSeparator = verticalSeparator; HorizontalSeparator = horizontalSeparator; CornerTopLeft = cornerTopLef; CornerTopRight = cornerTopRight; CornerBottomLeft = cornerBottomLeft; CornerBottomRight = cornerBottomRight; Tee = tee; TeeTop = teeTop; TeeBottom = teeBottom; TeeLeft = teeLeft; TeeRight = teeRight; } public void TestPrint(TextWriter writer) => writer.DrawTable([], new TextTableOptions { Border = this }, t => t.ColA, t => t.ColB, t => t.ColC); public static BorderDefinition Ascii => new BorderDefinition('|', '-', '|', '-', '-', '-', '-', '-', '|', '-', '-', '|', '|'); public static BorderDefinition Light => new BorderDefinition('│', '─', '│', '─', '┌', '┐', '└', '┘', '┼', '┬', '┴', '├', '┤'); public static BorderDefinition Heavy => new BorderDefinition('┃', '━', '┃', '━', '┏', '┓', '┗', '┛', '╋', '┳', '┻', '┣', '┫'); public static BorderDefinition Double => new BorderDefinition('║', '═', '║', '═', '╔', '╗', '╚', '╝', '╬', '╦', '╩', '╠', '╣'); public static BorderDefinition LightRounded => Light with { CornerTopLeft = '╭', CornerTopRight = '╮', CornerBottomLeft = '╰', CornerBottomRight = '╯' }; public static BorderDefinition DoubleHorizontal => Light with { HorizontalSeparator = '═', Tee = '╪', TeeLeft = '╞', TeeRight = '╡' }; public static BorderDefinition DoubleVertical => Light with { VerticalSeparator = '║', Tee = '╫', TeeTop = '╥', TeeBottom = '╨' }; public static BorderDefinition DoubleOuter => Double with { VerticalSeparator = '│', HorizontalSeparator = '─', TeeLeft = '╟', TeeRight = '╢', Tee = '┼', TeeTop = '╤', TeeBottom = '╧' }; public static BorderDefinition DoubleInner => Light with { VerticalSeparator = '║', HorizontalSeparator = '═', TeeLeft = '╞', TeeRight = '╡', Tee = '╬', TeeTop = '╥', TeeBottom = '╨' }; private record TestObject(string ColA, string ColB, string ColC); } public record ColumnDef(string ColumnName, Func ValueGetter); public static class TextTableExtention { /// /// Draw a text-based table to the provided TextWriter. /// /// Data row type /// /// Data rows to be drawn /// Table drawing options /// Data cell selector. Header name is based on member name public static void DrawTable(this TextWriter textWriter, IEnumerable rows, TextTableOptions options, params Expression>[] columnSelectors) { //Convert MemberExpression to ColumnDef var columnDefs = new ColumnDef[columnSelectors.Length]; for (int i = 0; i < columnDefs.Length; i++) { var exp = columnSelectors[i].Body as MemberExpression ?? throw new ArgumentException($"Expression at index {i} is not a member access expression", nameof(columnSelectors)); columnDefs[i] = new ColumnDef(exp.Member.Name, columnSelectors[i].Compile()); } textWriter.DrawTable(rows, options, columnDefs); } /// /// Draw a text-based table to the provided TextWriter. /// /// Data row type /// /// Data rows to be drawn /// Table drawing options /// Column header name and cell value selector. public static void DrawTable(this TextWriter textWriter, IEnumerable rows, TextTableOptions options, params ColumnDef[] columnSelectors) { var rowsArray = rows.ToArray(); var colNames = columnSelectors.Select(c => c.ColumnName).ToArray(); var colWidths = new int[columnSelectors.Length]; for (int i = 0; i < columnSelectors.Length; i++) { var nameWidth = options.DrawHeader ? StrLen(colNames[i]) : 0; var maxValueWidth = rowsArray.Length == 0 ? 0 : rows.Max(o => StrLen(columnSelectors[i].ValueGetter(o))); colWidths[i] = Math.Max(nameWidth, maxValueWidth); } textWriter.DrawTop(colWidths, options); textWriter.DrawHeader(colNames, colWidths, options); foreach (var row in rowsArray) { textWriter.DrawLeft(options, options.Border.Vertical, options.PaddingCharacter); var cellValues = columnSelectors.Select((def, j) => def.ValueGetter(row).PadText(colWidths[j], options)); textWriter.DrawRow(options, options.Border.VerticalSeparator, options.PaddingCharacter, cellValues); textWriter.DrawRight(options, options.Border.Vertical, options.PaddingCharacter); } textWriter.DrawBottom(colWidths, options); } private static void DrawHeader(this TextWriter textWriter, string[] colNames, int[] colWidths, TextTableOptions options) { if (!options.DrawHeader) return; //Draw column header names textWriter.DrawLeft(options, options.Border.Vertical, options.PaddingCharacter); var cellValues = colNames.Select((n, i) => n.PadText(colWidths[i], options)); textWriter.DrawRow(options, options.Border.VerticalSeparator, options.PaddingCharacter, cellValues); textWriter.DrawRight(options, options.Border.Vertical, options.PaddingCharacter); //Draw header separator textWriter.DrawLeft(options, options.Border.TeeLeft, options.Border.HorizontalSeparator); cellValues = colWidths.Select(w => new string(options.Border.HorizontalSeparator, w)); textWriter.DrawRow(options, options.Border.Tee, options.Border.HorizontalSeparator, cellValues); textWriter.DrawRight(options, options.Border.TeeRight, options.Border.HorizontalSeparator); } private static void DrawTop(this TextWriter textWriter, int[] colWidths, TextTableOptions options) { if (!options.DrawBorder) return; textWriter.DrawLeft(options, options.Border.CornerTopLeft, options.Border.Horizontal); var cellValues = colWidths.Select(w => new string(options.Border.Horizontal, w)); textWriter.DrawRow(options, options.Border.TeeTop, options.Border.Horizontal, cellValues); textWriter.DrawRight(options, options.Border.CornerTopRight, options.Border.Horizontal); } private static void DrawBottom(this TextWriter textWriter, int[] colWidths, TextTableOptions options) { if (!options.DrawBorder) return; textWriter.DrawLeft(options, options.Border.CornerBottomLeft, options.Border.Horizontal); var cellValues = colWidths.Select(w => new string(options.Border.Horizontal, w)); textWriter.DrawRow(options, options.Border.TeeBottom, options.Border.Horizontal, cellValues); textWriter.DrawRight(options, options.Border.CornerBottomRight, options.Border.Horizontal); } private static void DrawLeft(this TextWriter textWriter, TextTableOptions options, char borderChar, char cellPadChar) { if (!options.DrawBorder) return; textWriter.Write(borderChar); textWriter.Write(new string(cellPadChar, options.SideBorderPadding)); } private static void DrawRight(this TextWriter textWriter, TextTableOptions options, char borderChar, char cellPadChar) { if (options.DrawBorder) { textWriter.Write(new string(cellPadChar, options.SideBorderPadding)); textWriter.WriteLine(borderChar); } else { textWriter.WriteLine(); } } private static void DrawRow(this TextWriter textWriter, TextTableOptions options, char colSeparator, char cellPadChar, IEnumerable cellValues) { var cellPadding = new string(cellPadChar, options.IntercellPadding); var separator = cellPadding + colSeparator + cellPadding; textWriter.Write(string.Join(separator, cellValues)); } private static string PadText(this string? text, int totalWidth, TextTableOptions options) { if (string.IsNullOrEmpty(text)) return new string(options.PaddingCharacter, totalWidth); else if (StrLen(text) >= totalWidth) return text; return options.Justify switch { Justify.Right => PadLeft(text), Justify.Center => PadCenter(text), _ or Justify.Left => PadRight(text), }; string PadCenter(string text) { var half = (totalWidth - StrLen(text)) / 2; text = options.CenterTiebreak == Justify.Right ? new string(options.PaddingCharacter, half) + text : text + new string(options.PaddingCharacter, half); return options.CenterTiebreak == Justify.Right ? text.PadRight(totalWidth, options.PaddingCharacter) : text.PadLeft(totalWidth, options.PaddingCharacter); } string PadLeft(string text) { var padSize = totalWidth - StrLen(text); return new string(options.PaddingCharacter, padSize) + text; } string PadRight(string text) { var padSize = totalWidth - StrLen(text); return text + new string(options.PaddingCharacter, padSize); } } /// /// Determine the width of the string in console characters, accounting for wide unicode characters. /// private static int StrLen(string? str) => string.IsNullOrEmpty(str) ? 0 : str.Sum(c => CharIsWide(c) ? 2 : 1); /// /// Determines if the character is a unicode "Full Width" character which takes up two spaces in the console. /// static bool CharIsWide(char c) => (c >= '\uFF01' && c <= '\uFF61') || (c >= '\uFFE0' && c <= '\uFFE6'); }