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 import std.stdio; // TODO 20 class Cell 21 { 22 AsciiTable table; 23 Row row; 24 string[] lines; 25 ulong width; 26 this(AsciiTable table, Row row, string formatted) 27 { 28 import std.array; 29 30 this.table = table; 31 this.row = row; 32 this.lines = formatted.split("\n"); 33 this.width = lines.map!(line => line.unformattedLength).maxElement; 34 } 35 36 ulong height() 37 { 38 return lines.length; 39 } 40 41 string render(ulong row) 42 { 43 return colored.leftJustifyFormattedString(row < lines.length ? lines[row] : "", width); 44 } 45 46 override string toString() 47 { 48 return super.toString ~ " { lines: %s }".format(lines); 49 } 50 } 51 52 class Row 53 { 54 AsciiTable table; 55 ulong nrOfColumns; 56 bool header; 57 Cell[] cells; 58 public ulong height = 0; 59 60 this(AsciiTable table, ulong nrOfColumns, bool header) 61 { 62 this.table = table; 63 this.nrOfColumns = nrOfColumns; 64 this.header = header; 65 } 66 67 auto add(V)(V v) 68 { 69 if (cells.length == nrOfColumns) 70 { 71 throw new Exception("too many elements in row nrOfColumns=%s cells=%s".format(nrOfColumns, 72 cells.length)); 73 } 74 75 auto cell = new Cell(table, this, v.to!string); 76 cells ~= cell; 77 this.height = max(height, cell.height); 78 return this; 79 } 80 81 auto row() 82 { 83 return table.row(); 84 } 85 86 auto format() 87 { 88 return table.format(); 89 } 90 91 auto render(ulong row, string leftBorder, string columnSeparator, string rightBorder) 92 { 93 auto res = leftBorder ~ cells.map!(cell => cell.render(row)) 94 .join(columnSeparator) ~ rightBorder; 95 return res; 96 } 97 98 auto width(string columnSeparator) 99 { 100 return cells.fold!((memo, 101 cell) => memo + cell.width)(0UL) + (cells.length + 1) * columnSeparator.length; 102 } 103 104 override string toString() 105 { 106 return super.toString ~ " { nrOfColumns: %s, cells: %s }".format(nrOfColumns, cells); 107 } 108 } 109 /++ 110 + Build your table and later format it 111 +/ 112 class AsciiTable 113 { 114 size_t nrOfColumns; 115 Row[] rows; 116 117 /++ create a new asciitable 118 +/ 119 this(size_t nrOfColumns) 120 { 121 this.nrOfColumns = nrOfColumns; 122 } 123 124 /// Open a row 125 auto row() 126 { 127 return add(new Row(this, nrOfColumns, false)); 128 } 129 130 auto header() 131 { 132 return add(new Row(this, nrOfColumns, true)); 133 } 134 135 private auto add(Row row) 136 { 137 rows ~= row; 138 return row; 139 } 140 /// create formatter to fine tune tabular presentation 141 Formatter format() 142 { 143 return Formatter(this); 144 } 145 146 } 147 148 class Parts 149 { 150 string horizontal; 151 string vertical; 152 string crossing; 153 154 string topLeft; 155 string topRight; 156 string bottomLeft; 157 string bottomRight; 158 string topCrossing; 159 string leftCrossing; 160 string bottomCrossing; 161 string rightCrossing; 162 string headerHorizontal; 163 string headerCrossing; 164 string headerLeftCrossing; 165 string headerRightCrossing; 166 this(string horizontal, string vertical, string crossing, string topLeft, string topRight, string bottomLeft, 167 string bottomRight, string topCrossing, string bottomCrossing, 168 string leftCrossing, string rightCrossing, string headerHorizontal, 169 string headerCrossing, string headerLeftCrossing, string headerRightCrossing) 170 { 171 this.horizontal = horizontal; 172 this.headerHorizontal = headerHorizontal; 173 this.vertical = vertical; 174 this.crossing = crossing; 175 this.headerCrossing = headerCrossing; 176 this.topLeft = topLeft; 177 this.topRight = topRight; 178 this.bottomLeft = bottomLeft; 179 this.bottomRight = bottomRight; 180 this.topCrossing = topCrossing; 181 this.bottomCrossing = bottomCrossing; 182 this.leftCrossing = leftCrossing; 183 this.headerLeftCrossing = headerLeftCrossing; 184 this.rightCrossing = rightCrossing; 185 this.headerRightCrossing = headerRightCrossing; 186 } 187 } 188 189 class AsciiParts : Parts 190 { 191 this() 192 { 193 super("-", "|", "+", "+", "+", "+", "+", "+", "+", "+", "+", "=", "+", "+", "+"); 194 } 195 } 196 197 class UnicodeParts : Parts 198 { 199 this() 200 { 201 super("─", "│", "┼", "┌", "┐", "└", "┘", "┬", "┴", 202 "├", "┤", "═", "╪", "╞", "╡"); 203 } 204 } 205 /// the formatter collects format parameters and prints the table 206 struct Formatter 207 { 208 private AsciiTable table; 209 private string mPrefix = null; 210 private bool mRowSeparator = false; 211 private bool mHeaderSeparator = false; 212 private bool mColumnSeparator = false; 213 private ulong[] mColumnWidths = null; 214 private Parts mParts = null; 215 private bool mTopBorder = false; 216 private bool mLeftBorder = false; 217 private bool mBottomBorder = false; 218 private bool mRightBorder = false; 219 this(AsciiTable newTable, Parts parts = new AsciiParts) 220 { 221 table = newTable; 222 mParts = parts; 223 } 224 225 auto parts(Parts parts) 226 { 227 mParts = parts; 228 return this; 229 } 230 231 auto borders(bool borders) 232 { 233 topBorder(borders); 234 leftBorder(borders); 235 bottomBorder(borders); 236 rightBorder(borders); 237 return this; 238 } 239 240 /// Switch top and bottom border 241 auto horizontalBorders(bool borders) 242 { 243 topBorder(borders); 244 bottomBorder(borders); 245 return this; 246 } 247 /// Switch left and right border 248 auto verticalBorders(bool borders) 249 { 250 leftBorder(borders); 251 rightBorder(borders); 252 return this; 253 } 254 255 auto topBorder(bool topBorder) 256 { 257 mTopBorder = topBorder; 258 return this; 259 } 260 261 auto leftBorder(bool leftBorder) 262 { 263 mLeftBorder = leftBorder; 264 return this; 265 } 266 267 auto bottomBorder(bool bottomBorder) 268 { 269 mBottomBorder = bottomBorder; 270 return this; 271 } 272 273 auto rightBorder(bool rightBorder) 274 { 275 mRightBorder = rightBorder; 276 return this; 277 } 278 /// change the prefix that is printed in front of each row 279 auto prefix(string newPrefix) 280 { 281 mPrefix = newPrefix; 282 return this; 283 } 284 285 auto separators(bool active) 286 { 287 columnSeparator(active); 288 rowSeparator(active); 289 headerSeparator(active); 290 return this; 291 } 292 /// change the separator between columns, use null for no separator 293 auto columnSeparator(bool columnSeparator) 294 { 295 mColumnSeparator = columnSeparator; 296 return this; 297 } 298 /// change the separator between rows, use null for no separator 299 auto rowSeparator(bool rowSeparator) 300 { 301 mRowSeparator = rowSeparator; 302 return this; 303 } 304 305 auto headerSeparator(bool headerSeparator) 306 { 307 mHeaderSeparator = headerSeparator; 308 return this; 309 } 310 311 auto columnWidths(ulong[] widths) 312 { 313 if (widths.length != table.nrOfColumns) 314 { 315 throw new Exception("Wrong number of widths"); 316 } 317 mColumnWidths = widths; 318 return this; 319 } 320 321 private string calcSeparatorRow(size_t length, string s) 322 { 323 string res = ""; 324 for (size_t i = 0; i < length; ++i) 325 { 326 res ~= s; 327 } 328 return res; 329 } 330 331 private auto renderRow(Row row) 332 { 333 string[] lines = []; 334 for (int i = 0; i < row.height; ++i) 335 { 336 lines ~= row.render(i, mLeftBorder ? mParts.vertical : "", mColumnSeparator 337 ? mParts.vertical : "", mRightBorder ? mParts.vertical : ""); 338 } 339 return lines; 340 } 341 342 private void updateCellWidths() 343 { 344 if (table.rows.length == 0) 345 { 346 return; 347 } 348 for (int i = 0; i < table.rows[0].cells.length; ++i) 349 { 350 ulong width = mColumnWidths ? mColumnWidths[i] : 0; 351 for (int j = 0; j < table.rows.length; ++j) 352 { 353 width = max(width, table.rows[j].cells[i].width); 354 } 355 for (int j = 0; j < table.rows.length; ++j) 356 { 357 table.rows[j].cells[i].width = width; 358 } 359 } 360 } 361 362 string calcHorizontalSeparator(string normal, string left, string middle, string right) 363 { 364 return (mLeftBorder ? left : "") ~ table.rows[0].cells.map!( 365 cell => normal.replicate(cell.width)).join(mColumnSeparator 366 ? middle : normal) ~ (mRightBorder ? right : ""); 367 } 368 /// Convert to tabular presentation 369 string toString() 370 { 371 updateCellWidths(); 372 auto rSeparator = mRowSeparator ? calcHorizontalSeparator(mParts.horizontal, 373 mParts.leftCrossing, mParts.crossing, mParts.rightCrossing) : null; 374 auto hSeparator = mHeaderSeparator ? calcHorizontalSeparator(mParts.headerHorizontal, 375 mParts.headerLeftCrossing, mParts.headerCrossing, mParts.headerRightCrossing) : null; 376 auto prefix = mPrefix ? mPrefix : ""; 377 string[] lines = []; 378 if (mTopBorder) 379 { 380 lines ~= calcHorizontalSeparator(mParts.horizontal, mParts.topLeft, 381 mParts.topCrossing, mParts.topRight); 382 } 383 foreach (idx, row; table.rows) 384 { 385 auto newLines = toString(table, row, idx == table.rows.length - 1, 386 hSeparator, rSeparator).map!(line => prefix ~ line); 387 foreach (newLine; newLines) 388 { 389 lines ~= newLine; 390 } 391 } 392 if (mBottomBorder) 393 { 394 lines ~= calcHorizontalSeparator(mParts.horizontal, 395 mParts.bottomLeft, mParts.bottomCrossing, mParts.bottomRight); 396 } 397 return lines.join("\n"); 398 } 399 400 private auto toString(AsciiTable table, Row row, bool last, 401 string headerSeparator, string rowSeparator) 402 { 403 if (row.cells.length != table.nrOfColumns) 404 { 405 throw new Exception("row %s not fully filled".format(row)); 406 } 407 auto res = renderRow(row); 408 if (last) 409 { 410 return res; 411 } 412 else 413 { 414 if (mHeaderSeparator) 415 { 416 if (row.header) 417 { 418 return res ~ headerSeparator; 419 } 420 } 421 if (mRowSeparator) 422 { 423 return res ~ rowSeparator; 424 } 425 } 426 return res; 427 } 428 } 429 430 @("emtpy table") unittest 431 { 432 new AsciiTable(2).format.to!string; 433 } 434 435 /// 436 @("example") unittest 437 { 438 import unit_threaded; 439 import std.conv; 440 441 // dfmt off 442 auto table = new AsciiTable(2) 443 .header.add("HA").add("HB") 444 .row.add("C").add("D") 445 .row.add("E").add("F") 446 .table; 447 448 auto f1 = table 449 .format 450 .parts(new UnicodeParts) 451 .borders(true) 452 .separators(true) 453 .to!string; 454 // dfmt on 455 std.stdio.writeln(f1); 456 f1.shouldEqual(`┌──┬──┐ 457 │HA│HB│ 458 ╞══╪══╡ 459 │C │D │ 460 ├──┼──┤ 461 │E │F │ 462 └──┴──┘`); 463 464 // dfmt off 465 auto f2 = table 466 .format 467 .parts(new UnicodeParts) 468 .prefix(" ") 469 .rowSeparator(true) 470 .to!string; 471 // dfmt on 472 f2.shouldEqual(` HAHB 473 ───── 474 C D 475 ───── 476 E F `); 477 // dfmt off 478 auto f3 = table 479 .format 480 .parts(new UnicodeParts) 481 .columnSeparator(true) 482 .to!string; 483 // dfmt on 484 f3.shouldEqual(`HA│HB 485 C │D 486 E │F `); 487 } 488 489 /// 490 @("multiline cells") unittest 491 { 492 import unit_threaded; 493 494 auto table = new AsciiTable(2).row.add("1\n2").add("3").row.add("4").add("5\n6").table; 495 auto f = table.format.prefix("test:").to!string; 496 f.shouldEqual(`test:13 497 test:2 498 test:45 499 test: 6`); 500 } 501 502 /// 503 @("headers") unittest 504 { 505 import unit_threaded; 506 507 auto table = new AsciiTable(1).header.add("1").row.add(2).table; 508 auto f1 = table.format.headerSeparator(true).rowSeparator(true) 509 .topBorder(true).bottomBorder(true).to!string; 510 f1.shouldEqual(`- 511 1 512 = 513 2 514 -`); 515 } 516 517 @("wrong usage of ascii table") unittest 518 { 519 import unit_threaded; 520 521 new AsciiTable(2).row.add("1").add("2").add("3").shouldThrow!Exception; 522 } 523 524 @("auto expand columns") unittest 525 { 526 import unit_threaded; 527 528 new AsciiTable(1).row.add("test").format.columnWidths([10]) 529 .to!string.shouldEqual("test "); 530 } 531 532 @("row not fully filled") unittest 533 { 534 import unit_threaded; 535 536 new AsciiTable(2).row.add("test").format.to!string.shouldThrow!Exception; 537 }