1 /++ 2 + Asciitable to nicely format tables of strings. 3 + 4 + Authors: Christian Koestlin 5 + Copyright: Copyright © 2018, Christian Köstlin 6 + License: MIT 7 +/ 8 9 module asciitable; 10 11 public import asciitable.packageversion; 12 13 import std.string; 14 import std.algorithm; 15 import std.range; 16 import std.conv; 17 import colored; 18 19 class Cell 20 { 21 AsciiTable table; 22 Row row; 23 string[] lines; 24 ulong width; 25 this(AsciiTable table, Row row, string formatted) 26 { 27 import std.array; 28 this.table = table; 29 this.row = row; 30 this.lines = formatted.split("\n"); 31 this.width = lines 32 .map!(line => line.unformattedLength) 33 .maxElement; 34 } 35 ulong height() 36 { 37 return lines.length; 38 } 39 string render(ulong row) { 40 return (row < lines.length ? lines[row] : "").leftJustifyFormattedString(width); 41 } 42 override string toString() { 43 return super.toString ~ " { lines: %s }".format(lines); 44 } 45 } 46 47 48 class Row 49 { 50 AsciiTable table; 51 ulong nrOfColumns; 52 bool header; 53 Cell[] cells; 54 public ulong height = 0; 55 56 this(AsciiTable table, ulong nrOfColumns, bool header) 57 { 58 this.table = table; 59 this.nrOfColumns = nrOfColumns; 60 this.header = header; 61 } 62 63 auto add(V)(V v) 64 { 65 if (cells.length == nrOfColumns) 66 { 67 throw new Exception("too many elements in row nrOfColumns=%s cells=%s".format(nrOfColumns, cells.length)); 68 } 69 70 auto cell = new Cell(table, this, v.to!string); 71 cells ~= cell; 72 this.height = max(height, cell.height); 73 return this; 74 } 75 76 auto row() 77 { 78 return table.row(); 79 } 80 81 auto format() 82 { 83 return table.format(); 84 } 85 86 auto render(ulong row, string columnSeparator) { 87 if (!columnSeparator) { 88 columnSeparator = ""; 89 } 90 return columnSeparator 91 ~ cells.map!(cell => cell.render(row)).join(columnSeparator) 92 ~ columnSeparator; 93 } 94 auto width(string columnSeparator) { 95 return cells.fold!((memo, cell) => memo + cell.width)(0UL) 96 + (cells.length+1) * columnSeparator.length; 97 } 98 override string toString() { 99 return super.toString ~ " { nrOfColumns: %s, cells: %s }".format(nrOfColumns, cells); 100 } 101 } 102 /++ 103 + Build your table and later format it 104 +/ 105 class AsciiTable 106 { 107 size_t nrOfColumns; 108 Row[] rows; 109 110 /++ create a new asciitable 111 + Params: 112 + minimumWidths = minimum widths of the columns. columns are adjusted so that all entries of a column fit. 113 +/ 114 this(size_t nrOfColumns) 115 { 116 this.nrOfColumns = nrOfColumns; 117 } 118 119 /// Open a row 120 auto row() 121 { 122 return add(new Row(this, nrOfColumns, false)); 123 } 124 auto header() 125 { 126 return add(new Row(this, nrOfColumns, true)); 127 } 128 private auto add(Row row) { 129 rows ~= row; 130 return row; 131 } 132 /// create formatter to fine tune tabular presentation 133 Formatter format() 134 { 135 return Formatter(this); 136 } 137 138 } 139 140 /// the formatter collects format parameters and prints the table 141 struct Formatter 142 { 143 private AsciiTable table; 144 private string mPrefix = null; 145 private string mRowSeparator = null; 146 private string mHeaderSeparator = null; 147 private string mColumnSeparator = null; 148 private ulong[] mColumnWidths = null; 149 this(AsciiTable newTable) 150 { 151 table = newTable; 152 } 153 /// change the prefix that is printed in front of each row 154 auto prefix(string newPrefix) 155 { 156 mPrefix = newPrefix; 157 return this; 158 } 159 160 /// change the separator between columns, use null for no separator 161 auto columnSeparator(string s) 162 { 163 mColumnSeparator = s; 164 return this; 165 } 166 /// change the separator between rows, use null for no separator 167 auto rowSeparator(string s) 168 { 169 mRowSeparator = s; 170 return this; 171 } 172 auto headerSeparator(string s) 173 { 174 mHeaderSeparator = s; 175 return this; 176 } 177 auto columnWidths(ulong[] widths) 178 { 179 if (widths.length != table.nrOfColumns) { 180 throw new Exception("Wrong number of widths"); 181 } 182 mColumnWidths = widths; 183 return this; 184 } 185 186 private string calcSeparatorRow(size_t length, string s) { 187 string res = ""; 188 for (size_t i=0; i<length; ++i) { 189 res ~= s; 190 } 191 return res; 192 } 193 194 private auto renderRow(Row row, string[] lines) { 195 for (int i=0; i<row.height; ++i) { 196 lines ~= row.render(i, mColumnSeparator); 197 } 198 return lines; 199 } 200 201 private void updateCellWidths() { 202 if (table.rows.length == 0) { 203 return; 204 } 205 for (int i=0; i<table.rows[0].cells.length; ++i) { 206 ulong width = mColumnWidths ? mColumnWidths[i] : 0; 207 for (int j=0; j<table.rows.length; ++j) { 208 width = max(width, table.rows[j].cells[i].width); 209 } 210 for (int j=0; j<table.rows.length; ++j) { 211 table.rows[j].cells[i].width = width; 212 } 213 } 214 } 215 /// Convert to tabular presentation 216 string toString() 217 { 218 updateCellWidths(); 219 ulong width= table.rows[0].width(mColumnSeparator); 220 221 auto rSeparator = mRowSeparator ? mRowSeparator.replicate(width) : null; 222 auto hSeparator = mHeaderSeparator ? mHeaderSeparator.replicate(width) : null; 223 auto prefix = mPrefix ? mPrefix : ""; 224 auto lines = rSeparator ? [rSeparator] : []; 225 return table 226 .rows 227 .fold!((memo, row) => toString(memo, table, row, hSeparator, rSeparator))(lines) 228 .map!(line => prefix ~ line) 229 .join("\n"); 230 } 231 232 private auto toString(string[] lines, AsciiTable table, Row row, string hSeparator, string rSeparator) { 233 if (row.cells.length != table.nrOfColumns) { 234 throw new Exception("row %s not fully filled".format(row)); 235 } 236 lines = renderRow(row, lines); 237 if (row.header && hSeparator) { 238 lines ~= hSeparator; 239 } else if (rSeparator) { 240 lines ~= rSeparator; 241 } 242 return lines; 243 } 244 } 245 246 /// 247 @("example") unittest 248 { 249 import unit_threaded; 250 import std.conv; 251 252 auto table = new AsciiTable(2) 253 .row.add("1").add("2") 254 .row.add("3").add("4") 255 .table; 256 auto f1 = table.format.to!string; 257 f1.shouldEqual("12\n34"); 258 259 auto f2 = table.format.prefix(" ").rowSeparator("-").to!string; 260 f2.shouldEqual(" --\n 12\n --\n 34\n --"); 261 262 auto f3 = table.format.prefix(" ").columnSeparator("|").to!string; 263 f3.shouldEqual(" |1|2|\n |3|4|"); 264 265 auto f4 = table.format.prefix(" ").columnSeparator("|").rowSeparator("-").to!string; 266 f4.shouldEqual(" -----\n |1|2|\n -----\n |3|4|\n -----"); 267 } 268 269 /// 270 @("multiline cells") unittest 271 { 272 import unit_threaded; 273 auto table = new AsciiTable(2) 274 .row.add("1\n2").add("3") 275 .row.add("4").add("5\n6") 276 .table; 277 auto f = table.format.prefix("test:").to!string; 278 f.shouldEqual("test:13\ntest:2 \ntest:45\ntest: 6"); 279 } 280 281 /// 282 @("headers") unittest 283 { 284 import unit_threaded; 285 auto table = new AsciiTable(1) 286 .header.add("1") 287 .row.add(2) 288 .table; 289 auto f1 = table.format.headerSeparator("=").rowSeparator("-").to!string; 290 f1.shouldEqual("-\n1\n=\n2\n-"); 291 } 292 293 @("wrong usage of ascii table") unittest 294 { 295 import unit_threaded; 296 297 new AsciiTable(2) 298 .row.add("1").add("2").add("3") 299 .shouldThrow!Exception; 300 } 301 302 @("auto expand columns") unittest 303 { 304 import unit_threaded; 305 306 new AsciiTable(1) 307 .row.add("test") 308 .format.columnWidths([10]) 309 .to!string 310 .shouldEqual("test "); 311 } 312 313 @("row not fully filled") unittest 314 { 315 import unit_threaded; 316 317 new AsciiTable(2) 318 .row.add("test") 319 .format 320 .to!string 321 .shouldThrow!Exception; 322 }