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   }