C# Tips: Draw a data table in console
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace datatable 8 { 9 public class ConsoleTable 10 { 11 /// <summary> 12 /// This will hold the header of the table. 13 /// </summary> 14 private string[] header; 15 16 /// <summary> 17 /// This will hold the rows (lines) in the table, not including the 18 /// header. I'm using a List of lists because it's easier to deal with... 19 /// </summary> 20 private List<List<string>> rows; 21 22 /// <summary> 23 /// This is the default element (character/string) that will be put 24 /// in the table when user adds invalid data, example: 25 /// ConsoleTable ct = new ConsoleTable(); 26 /// ct.AddRow(new List<string> { null, "bla", "bla" }); 27 /// That null will be replaced with "DefaultElement", also, empty 28 /// strings will be replaced with this value. 29 /// </summary> 30 private const string DefaultElement = "X"; 31 32 public enum AlignText 33 { 34 ALIGN_RIGHT, 35 ALIGN_LEFT, 36 } 37 38 public ConsoleTable() 39 { 40 header = null; 41 rows = new List<List<string>>(); 42 TextAlignment = AlignText.ALIGN_LEFT; 43 } 44 45 /// <summary> 46 /// Set text alignment in table cells, either RIGHT or LEFT. 47 /// </summary> 48 public AlignText TextAlignment 49 { 50 get; 51 set; 52 } 53 54 public void SetHeaders(string[] h) 55 { 56 header = h; 57 } 58 59 public void AddRow(List<string> row) 60 { 61 rows.Add(row); 62 } 63 64 private void AppendLine(StringBuilder hsb, int length) 65 { 66 // " " length is 1 67 // "\r\n" length is 2 68 // +1 length because I want the output to be prettier 69 // Hence the length - 4 ... 70 hsb.Append(" "); 71 hsb.Append(new string('-', length - 4)); 72 hsb.Append("\r\n"); 73 } 74 75 /// <summary> 76 /// This function returns the maximum possible length of an 77 /// individual row (line). Of course that if we use table header, 78 /// the maximum length of an individual row should equal the 79 /// length of the header. 80 /// </summary> 81 private int GetMaxRowLength() 82 { 83 if (header != null) 84 return header.Length; 85 else 86 { 87 int maxlen = rows[0].Count; 88 for (int i = 1; i < rows.Count; i++) 89 if (rows[i].Count > maxlen) 90 maxlen = rows[i].Count; 91 92 return maxlen; 93 } 94 } 95 96 private void PutDefaultElementAndRemoveExtra() 97 { 98 int maxlen = GetMaxRowLength(); 99 100 for (int i = 0; i < rows.Count; i++) 101 { 102 // If we find a line that is smaller than the biggest line, 103 // we'll add DefaultElement at the end of that line. In the end 104 // the line will be as big as the biggest line. 105 if (rows[i].Count < maxlen) 106 { 107 int loops = maxlen - rows[i].Count; 108 for (int k = 0; k < loops; k++) 109 rows[i].Add(DefaultElement); 110 } 111 else if (rows[i].Count > maxlen) 112 { 113 // This will apply only when header != null, and we try to 114 // add a line bigger than the header line. Remove the elements 115 // of the line, from right to left, until the line is equal 116 // with the header line. 117 rows[i].RemoveRange(maxlen, rows[i].Count - maxlen); 118 } 119 120 // Find bad data, loop through all table elements. 121 for (int j = 0; j < rows[i].Count; j++) 122 { 123 if (rows[i][j] == null) 124 rows[i][j] = DefaultElement; 125 else if (rows[i][j] == "") 126 rows[i][j] = DefaultElement; 127 } 128 } 129 } 130 131 /// <summary> 132 /// This function will return an array of integers, an element at 133 /// position 'i' will return the maximum length from column 'i' 134 /// of the table (if we look at the table as a matrix). 135 /// </summary> 136 private int[] GetWidths() 137 { 138 int[] widths = null; 139 if (header != null) 140 { 141 // Initially we assume that the maximum length from column 'i' 142 // is exactly the length of the header from column 'i'. 143 widths = new int[header.Length]; 144 for (int i = 0; i < header.Length; i++) 145 widths[i] = header[i].ToString().Length; 146 } 147 else 148 { 149 int count = GetMaxRowLength(); 150 widths = new int[count]; 151 for (int i = 0; i < count; i++) 152 widths[i] = -1; 153 } 154 155 foreach (List<string> s in rows) 156 { 157 for (int i = 0; i < s.Count; i++) 158 { 159 s[i] = s[i].Trim(); 160 if (s[i].Length > widths[i]) 161 widths[i] = s[i].Length; 162 } 163 } 164 165 return widths; 166 } 167 168 /// <summary> 169 /// Returns a valid format that is to be passed to AppendFormat 170 /// member function of StringBuilder. 171 /// General form: "|{i, +/-widths[i]}|", where 0 <= i <= widths.Length - 1 172 /// and widths[i] represents the maximum width from column 'i'. 173 /// </summary> 174 /// <param name="widths">The array of widths presented above.</param> 175 private string BuildRowFormat(int[] widths) 176 { 177 string rowFormat = String.Empty; 178 for (int i = 0; i < widths.Length; i++) 179 { 180 if (TextAlignment == AlignText.ALIGN_LEFT) 181 rowFormat += "| {" + i.ToString() + ",-" + (widths[i]) + "} "; 182 else 183 rowFormat += "| {" + i.ToString() + "," + (widths[i]) + "} "; 184 } 185 186 rowFormat = rowFormat.Insert(rowFormat.Length, "|\r\n"); 187 return rowFormat; 188 } 189 190 /// <summary> 191 /// Prints the table, main function. 192 /// </summary> 193 public void PrintTable() 194 { 195 if (rows.Count == 0) 196 { 197 Console.WriteLine("Can't create a table without any rows."); 198 return; 199 } 200 PutDefaultElementAndRemoveExtra(); 201 202 int[] widths = GetWidths(); 203 string rowFormat = BuildRowFormat(widths); 204 205 // I'm using a temporary string builder to find the total width 206 // of the table, and increase BufferWidth of Console if necessary. 207 StringBuilder toFindLen = new StringBuilder(); 208 toFindLen.AppendFormat(rowFormat, (header == null ? rows[0].ToArray() : header)); 209 int length = toFindLen.Length; 210 if (Console.BufferWidth < length) 211 Console.BufferWidth = length; 212 213 // Print the first row, or header (if it exist), you can see that AppendLine 214 // is called before/after every AppendFormat. 215 StringBuilder hsb = new StringBuilder(); 216 AppendLine(hsb, length); 217 hsb.AppendFormat(rowFormat, (header == null ? rows[0].ToArray() : header)); 218 AppendLine(hsb, length); 219 220 // If header does't exist, we start from 1 because the first row 221 // was already printed above. 222 int idx = 0; 223 if (header == null) 224 idx = 1; 225 for (int i = idx; i < rows.Count; i++) 226 { 227 hsb.AppendFormat(rowFormat, rows[i].ToArray()); 228 AppendLine(hsb, length); 229 } 230 231 Console.WriteLine(hsb.ToString()); 232 } 233 234 235 static void Main(string[] args) 236 { 237 // Some test table, with header, and 3 lines by 3 columns. 238 ConsoleTable ct = new ConsoleTable(); 239 ct.TextAlignment = ConsoleTable.AlignText.ALIGN_RIGHT; 240 ct.SetHeaders(new string[] { "ID", "Name", "City" }); 241 ct.AddRow(new List<string> { "1", "John", "New York" }); 242 ct.AddRow(new List<string> { "2", "Mark", "Washington" }); 243 ct.AddRow(new List<string> { "3", "Alice", "Chicago" }); 244 ct.PrintTable(); 245 } 246 } 247 }