1 /******************************************************************************* 2 3 Classes to draw auto-formatted tables to the console. 4 5 The number of columns in the table must be specified either at construction, 6 or by calling the init() method. Rows can be be added using the firstRow() & 7 nextRow() methods. (firstRow() is essentially a reset method.) 8 9 Usage example: 10 11 --- 12 13 // A table with 3 columns 14 scope table = new Table(3); 15 16 // First row is just a divider 17 table.firstRow.setDivider(); 18 19 // Next row contains the headings, a series of strings 20 table.nextRow.set( 21 Table.Cell.String("Address"), 22 Table.Cell.String("Port"), 23 Table.Cell.String("Connections")); 24 25 // Next row is another divider 26 table.nextRow.setDivider(); 27 28 // Now we add one row for each of a set of 'nodes' 29 foreach ( node; this.nodes ) 30 { 31 table.nextRow.set( 32 Table.Cell.String(node.address), 33 Table.Cell.Integer(node.port), 34 Table.Cell.Integer(node.connections)); 35 } 36 37 // The last row is another divider 38 table.nextRow.setDivider(); 39 40 // Display the table to Stdout 41 table.display(); 42 43 --- 44 45 It's also possible to draw smart tables where certain cells in some rows 46 are merged together, something like this, for example: 47 48 --- 49 50 |-----------------------------------------------------| 51 | 0xdb6db6e4 .. 0xedb6db76 | 0xedb6db77 .. 0xffffffff | 52 |-----------------------------------------------------| 53 | Records | Bytes | Records | Bytes | 54 |-----------------------------------------------------| 55 | 26,707 | 11,756,806 | 27,072 | 11,918,447 | 56 | 6,292 | 1,600,360 | 6,424 | 1,628,086 | 57 | 1,177,809 | 56,797,520 | 1,176,532 | 56,736,224 | 58 |-----------------------------------------------------| 59 60 --- 61 62 In this example, columns 0 & 1 and 2 & 3 in row 1 are merged. 63 64 Merged cells usage example: 65 66 --- 67 68 // A table with 4 columns 69 scope table = new Table(4); 70 71 // First row is just a divider 72 table.firstRow.setDivider(); 73 74 // Next row contains a hash range occupying two (merged) cells. Note 75 // that this is the widest column -- the other columns adapt to allow it 76 // to fit. 77 table.nextRow.set(Table.Cell.Merged, Table.Cell.String("0xdb6db6e4 .. 0xedb6db76"), 78 Table.Cell.Merged, Table.Cell.String("0xedb6db77 .. 0xffffffff")); 79 80 // Next row is another divider 81 table.nextRow.setDivider(); 82 83 // Next row contains the headings, a series of strings 84 table.nextRow.set(Table.Cell.String("Records"), Table.Cell.String("Bytes"), 85 Table.Cell.String("Records"), Table.Cell.String("Bytes")); 86 87 // Next row is another divider 88 table.nextRow.setDivider(); 89 90 // Now we add one row for each of a set of 'nodes' 91 foreach ( node; this.nodes ) 92 { 93 table.nextRow.set(Table.Cell.Integer(node.records1), Table.Cell.Integer(node.bytes1), 94 Table.Cell.Integer(node.records2), Table.Cell.Integer(node.bytes2)); 95 } 96 97 // The last row is another divider 98 table.nextRow.setDivider(); 99 100 // Display the table to Stdout 101 table.display(); 102 103 --- 104 105 Copyright: 106 Copyright (c) 2009-2016 dunnhumby Germany GmbH. 107 All rights reserved. 108 109 License: 110 Boost Software License Version 1.0. See LICENSE_BOOST.txt for details. 111 Alternatively, this file may be distributed under the terms of the Tango 112 3-Clause BSD License (see LICENSE_BSD.txt for details). 113 114 *******************************************************************************/ 115 116 module ocean.io.console.Tables; 117 118 119 120 121 import ocean.transition; 122 123 import ocean.core.Array : copy, appendCopy, concat; 124 import ocean.core.Verify; 125 126 import ocean.text.utf.UtfUtil; 127 128 import ocean.text.util.DigitGrouping; 129 import ocean.text.util.MetricPrefix; 130 131 import ocean.io.stream.Format; 132 133 import ocean.text.convert.Formatter; 134 135 import ocean.io.Stdout; 136 137 import ocean.io.Terminal; 138 139 version(UnitTest) import ocean.core.Test; 140 141 /******************************************************************************* 142 143 Table 144 145 *******************************************************************************/ 146 147 public class Table 148 { 149 /*************************************************************************** 150 151 Alias for a console outputter (basically Stdout / Stderr) 152 153 ***************************************************************************/ 154 155 public alias FormatOutput Output; 156 157 158 /*************************************************************************** 159 160 Row 161 162 ***************************************************************************/ 163 164 public class Row 165 { 166 /*********************************************************************** 167 168 Cell 169 170 ***********************************************************************/ 171 172 public struct Cell 173 { 174 /******************************************************************* 175 176 Number of characters in-between each cell 177 178 *******************************************************************/ 179 180 public enum inter_cell_spacing = 3; // = " | " 181 182 183 /******************************************************************* 184 185 Static opCall method to create a cell containing a string. 186 187 Params: 188 str = string to put in cell 189 190 Returns: 191 new cell struct 192 193 *******************************************************************/ 194 195 static public Cell String ( cstring str ) 196 { 197 Cell cell; 198 cell.setString(str); 199 return cell; 200 } 201 202 203 /******************************************************************* 204 205 Static opCall method to create a cell containing an integer. 206 207 Params: 208 integer = integer to put in cell 209 use_thousands_separator = if true the integer will be 210 "thousands" comma-separated. 211 212 Returns: 213 new cell struct 214 215 *******************************************************************/ 216 217 static public Cell Integer ( ulong integer, 218 bool use_thousands_separator = true ) 219 { 220 Cell cell; 221 cell.setInteger(integer, use_thousands_separator); 222 return cell; 223 } 224 225 226 /******************************************************************* 227 228 Static opCall method to create a cell containing an integer 229 scaled into a binary metric representation (Ki, Mi, Gi, Ti, 230 etc). 231 232 Params: 233 integer = integer to put in cell 234 metric_string = metric identifier (eg bytes, Kbytes, Mbytes, 235 etc.) 236 237 Returns: 238 new cell struct 239 240 *******************************************************************/ 241 242 static public Cell BinaryMetric ( ulong integer, cstring metric_string = "" ) 243 { 244 Cell cell; 245 cell.setBinaryMetric(integer, metric_string); 246 return cell; 247 } 248 249 250 /******************************************************************* 251 252 Static opCall method to create a cell containing an integer 253 scaled into a decimal metric representation (K, M, G, T, etc). 254 255 Params: 256 integer = integer to put in cell 257 metric_string = metric identifier (eg bytes, Kbytes, Mbytes, 258 etc.) 259 260 Returns: 261 new cell struct 262 263 *******************************************************************/ 264 265 static public Cell DecimalMetric ( ulong integer, cstring metric_string = "" ) 266 { 267 Cell cell; 268 cell.setDecimalMetric(integer, metric_string); 269 return cell; 270 } 271 272 273 /******************************************************************* 274 275 Static opCall method to create a cell containing a float. 276 277 Params: 278 floating = float to put in cell 279 280 Returns: 281 new cell struct 282 283 *******************************************************************/ 284 285 static public Cell Float ( double floating ) 286 { 287 Cell cell; 288 cell.setFloat(floating); 289 return cell; 290 } 291 292 293 /******************************************************************* 294 295 Static opCall method to create a cell merged with the one to the 296 right. 297 298 Returns: 299 new cell struct 300 301 *******************************************************************/ 302 303 static public Cell Merged ( ) 304 { 305 Cell cell; 306 cell.setMerged(); 307 return cell; 308 } 309 310 311 /******************************************************************* 312 313 Static opCall method to create an empty cell. 314 315 Returns: 316 new cell struct 317 318 *******************************************************************/ 319 320 static public Cell Empty ( ) 321 { 322 Cell cell; 323 cell.setEmpty(); 324 return cell; 325 } 326 327 328 /******************************************************************* 329 330 Cell types enum 331 332 *******************************************************************/ 333 334 public enum Type 335 { 336 Empty, // no content 337 Divider, // horizontal dividing line ------------------ 338 Integer, // contains an integer 339 BinaryMetric, // contains an integer scaled to a binary metric 340 DecimalMetric, // contains an integer scaled to a decimal metric 341 Float, // contains a floating point number 342 String, // contains a string 343 Merged // merged with cell to the right 344 } 345 346 public Type type; 347 348 349 /******************************************************************* 350 351 Cell contents union 352 353 *******************************************************************/ 354 355 public union Contents 356 { 357 public ulong integer; 358 public double floating; 359 public mstring utf8; 360 } 361 362 public Contents contents; 363 364 365 /******************************************************************* 366 367 Metric postfix string (used by BinaryMetric and DecimalMetric 368 cell types) 369 370 *******************************************************************/ 371 372 public mstring metric_string; 373 374 375 /******************************************************************* 376 377 Colour code strings, used to determine the color of this cell's 378 contents for output. One for foreground colour, and one for 379 background colour. If a string is empty, terminal's default 380 colour will be used for output. 381 382 *******************************************************************/ 383 384 private mstring fg_colour_string; 385 386 private mstring bg_colour_string; 387 388 389 /******************************************************************* 390 391 When enabled and the type is an integer, the output will be 392 "thousands" comma-separated, e.g.: "1,234,567" 393 394 *******************************************************************/ 395 396 private bool use_thousands_separator; 397 398 399 /******************************************************************* 400 401 Sets the cell to contain a string. 402 403 Params: 404 str = string to set 405 406 Returns: 407 this instance for method chaining 408 409 *******************************************************************/ 410 411 public typeof((&this)) setString ( cstring str ) 412 { 413 (&this).type = Type.String; 414 (&this).contents.utf8.copy(str); 415 416 return (&this); 417 } 418 419 420 /******************************************************************* 421 422 Sets the cell to contain an integer. 423 424 Params: 425 num = integer to set 426 use_thousands_separator = if true the integer will be 427 "thousands" comma-separated. 428 429 Returns: 430 this instance for method chaining 431 432 *******************************************************************/ 433 434 public typeof((&this)) setInteger ( ulong num, 435 bool use_thousands_separator = true ) 436 { 437 (&this).type = Type.Integer; 438 (&this).use_thousands_separator = use_thousands_separator; 439 (&this).contents.integer = num; 440 441 return (&this); 442 } 443 444 445 /******************************************************************* 446 447 Sets the cell to contain an integer scaled into a binary metric 448 representation (Ki, Mi, Gi, Ti, etc). 449 450 Params: 451 num = integer to set 452 metric_string = metric identifier (eg bytes, Kbytes, Mbytes, 453 etc.) 454 455 Returns: 456 this instance for method chaining 457 458 *******************************************************************/ 459 460 public typeof((&this)) setBinaryMetric ( ulong num, cstring metric_string = "" ) 461 { 462 (&this).type = Type.BinaryMetric; 463 (&this).contents.integer = num; 464 (&this).metric_string.copy(metric_string); 465 466 return (&this); 467 } 468 469 470 /******************************************************************* 471 472 Sets the cell to contain an integer scaled into a decimal metric 473 representation (K, M, G, T, etc). 474 475 Params: 476 num = integer to set 477 metric_string = metric identifier (eg bytes, Kbytes, Mbytes, 478 etc.) 479 480 Returns: 481 this instance for method chaining 482 483 *******************************************************************/ 484 485 public typeof((&this)) setDecimalMetric ( ulong num, cstring metric_string = "" ) 486 { 487 (&this).type = Type.DecimalMetric; 488 (&this).contents.integer = num; 489 (&this).metric_string.copy(metric_string); 490 491 return (&this); 492 } 493 494 495 /******************************************************************* 496 497 Sets the cell to contain a float. 498 499 Params: 500 num = float to set 501 502 Returns: 503 this instance for method chaining 504 505 *******************************************************************/ 506 507 public typeof((&this)) setFloat ( double num ) 508 { 509 (&this).type = Type.Float; 510 (&this).contents.floating = num; 511 512 return (&this); 513 } 514 515 516 /******************************************************************* 517 518 Sets the cell to contain nothing. 519 520 Returns: 521 this instance for method chaining 522 523 *******************************************************************/ 524 525 public typeof((&this)) setEmpty ( ) 526 { 527 (&this).type = Type.Empty; 528 529 return (&this); 530 } 531 532 533 /******************************************************************* 534 535 Sets the cell to contain a horizontal divider. 536 537 Returns: 538 this instance for method chaining 539 540 *******************************************************************/ 541 542 public typeof((&this)) setDivider ( ) 543 { 544 (&this).type = Type.Divider; 545 546 return (&this); 547 } 548 549 550 /******************************************************************* 551 552 Sets the cell to be merged with the cell to its right. 553 554 Returns: 555 this instance for method chaining 556 557 *******************************************************************/ 558 559 public typeof((&this)) setMerged ( ) 560 { 561 (&this).type = Type.Merged; 562 563 return (&this); 564 } 565 566 567 /******************************************************************* 568 569 Sets the foreground colour of this cell 570 571 Params: 572 colour = The colour to use 573 574 Returns: 575 this instance for method chaining 576 577 *******************************************************************/ 578 579 public typeof((&this)) setForegroundColour ( Terminal.Colour colour ) 580 { 581 auto colour_str = Terminal.fg_colour_codes[colour]; 582 (&this).fg_colour_string.concat(Terminal.CSI, colour_str); 583 584 return (&this); 585 } 586 587 588 /******************************************************************* 589 590 Sets the background colour of this cell 591 592 Params: 593 colour = The colour to use 594 595 Returns: 596 this instance for method chaining 597 598 *******************************************************************/ 599 600 public typeof((&this)) setBackgroundColour ( Terminal.Colour colour ) 601 { 602 auto colour_str = Terminal.bg_colour_codes[colour]; 603 (&this).bg_colour_string.concat(Terminal.CSI, colour_str); 604 605 return (&this); 606 } 607 608 609 /******************************************************************* 610 611 Returns: 612 the width of cell's contents, in characters 613 614 *******************************************************************/ 615 616 public size_t width ( ) 617 { 618 switch ( (&this).type ) 619 { 620 case Cell.Type.Merged: 621 case Cell.Type.Empty: 622 case Cell.Type.Divider: 623 return 0; 624 case Cell.Type.BinaryMetric: 625 MetricPrefix metric; 626 metric.bin((&this).contents.integer); 627 return (&this).floatWidth(metric.scaled) + 3 + 628 (&this).metric_string.length; 629 case Cell.Type.DecimalMetric: 630 MetricPrefix metric; 631 metric.dec((&this).contents.integer); 632 return (&this).floatWidth(metric.scaled) + 2 + 633 (&this).metric_string.length; 634 case Cell.Type.Integer: 635 return (&this).integerWidth((&this).contents.integer); 636 case Cell.Type.Float: 637 return (&this).floatWidth((&this).contents.floating); 638 case Cell.Type.String: 639 return utf8Length((&this).contents.utf8); 640 641 default: 642 assert(0); 643 } 644 } 645 646 647 /******************************************************************* 648 649 Displays the cell to the specified output. 650 651 Params: 652 output = output to send cell to 653 width = display width of cell 654 content_buf = string buffer to use for formatting cell 655 contents 656 spacing_buf = string buffer to use for formatting spacing to 657 the left of the cell's contents 658 659 *******************************************************************/ 660 661 public void display ( Output output, size_t width, ref mstring 662 content_buf, ref mstring spacing_buf ) 663 { 664 // sequence of control characters to reset output colors to default 665 istring default_colours = 666 Terminal.CSI ~ Terminal.fg_colour_codes[Terminal.Colour.Default] ~ 667 Terminal.CSI ~ Terminal.bg_colour_codes[Terminal.Colour.Default]; 668 669 // set the colour of this cell 670 if ( (&this).fg_colour_string.length > 0 || 671 (&this).bg_colour_string.length > 0 ) 672 { 673 output.format("{}{}", (&this).fg_colour_string, (&this).bg_colour_string); 674 } 675 676 if ( (&this).type == Type.Divider ) 677 { 678 content_buf.length = width + inter_cell_spacing; 679 enableStomping(content_buf); 680 content_buf[] = '-'; 681 682 output.format("{}", content_buf); 683 684 // reset colour to default 685 if ( (&this).fg_colour_string.length > 0 || 686 (&this).bg_colour_string.length > 0 ) 687 { 688 output.format("{}", default_colours); 689 } 690 } 691 else 692 { 693 switch ( (&this).type ) 694 { 695 case Type.Empty: 696 content_buf.length = 0; 697 enableStomping(content_buf); 698 break; 699 case Type.BinaryMetric: 700 content_buf.length = 0; 701 enableStomping(content_buf); 702 703 MetricPrefix metric; 704 metric.bin((&this).contents.integer); 705 706 if ( metric.prefix == ' ' ) 707 { 708 sformat(content_buf, 709 "{} {}", cast(uint)metric.scaled, 710 (&this).metric_string); 711 } 712 else 713 { 714 sformat(content_buf, 715 "{} {}i{}", metric.scaled, metric.prefix, 716 (&this).metric_string); 717 } 718 break; 719 case Type.DecimalMetric: 720 content_buf.length = 0; 721 enableStomping(content_buf); 722 723 MetricPrefix metric; 724 metric.dec((&this).contents.integer); 725 726 if ( metric.prefix == ' ' ) 727 { 728 sformat(content_buf, 729 "{} {}", cast(uint)metric.scaled, 730 (&this).metric_string); 731 } 732 else 733 { 734 sformat(content_buf, 735 "{} {}{}", metric.scaled, metric.prefix, 736 (&this).metric_string); 737 } 738 break; 739 case Type.Integer: 740 if ( (&this).use_thousands_separator ) 741 { 742 DigitGrouping.format((&this).contents.integer, content_buf); 743 } 744 else 745 { 746 content_buf.length = 0; 747 enableStomping(content_buf); 748 sformat(content_buf, 749 "{}", (&this).contents.integer); 750 } 751 break; 752 case Type.Float: 753 content_buf.length = 0; 754 enableStomping(content_buf); 755 sformat(content_buf, 756 "{}", (&this).contents.floating); 757 break; 758 case Type.String: 759 content_buf = (&this).contents.utf8; 760 break; 761 default: 762 return; 763 } 764 765 verify(width >= utf8Length(content_buf), "column not wide enough"); 766 767 spacing_buf.length = width - utf8Length(content_buf); 768 enableStomping(spacing_buf); 769 spacing_buf[] = ' '; 770 output.format(" {}{} ", spacing_buf, content_buf); 771 772 // reset colour to default 773 output.format("{}", default_colours); 774 775 output.format("|"); 776 } 777 } 778 779 780 /******************************************************************* 781 782 Calculates the number of characters required to display 783 an integer (the number of digits) 784 785 Params: 786 i = integer to calculate width of 787 788 Returns: 789 number of characters required to display i 790 791 *******************************************************************/ 792 793 private size_t integerWidth ( ulong i ) 794 { 795 if ( (&this).use_thousands_separator ) 796 { 797 return DigitGrouping.length(i); 798 } 799 800 size_t digits; 801 while (i) 802 { 803 i /= 10; 804 ++digits; 805 } 806 807 return digits; 808 } 809 810 811 /******************************************************************* 812 813 Calculates the number of characters required to display a float. 814 815 Params: 816 f = float to calculate width of 817 818 Returns: 819 number of character required to display f 820 821 *******************************************************************/ 822 823 private size_t floatWidth ( double f ) 824 { 825 size_t width = 4; // 0.00 826 if ( f < 0 ) 827 { 828 f = -f; 829 width++; // minus symbol 830 } 831 832 double dec = 10; 833 while ( f >= dec ) 834 { 835 width++; 836 dec *= 10; 837 } 838 839 return width; 840 } 841 } 842 843 844 /*********************************************************************** 845 846 List of cells in row 847 848 ***********************************************************************/ 849 850 public Cell[] cells; 851 852 853 /*********************************************************************** 854 855 Returns: 856 the number of cells in this row 857 858 ***********************************************************************/ 859 860 public size_t length ( ) 861 { 862 return this.cells.length; 863 } 864 865 866 /*********************************************************************** 867 868 Sets the number of cells in this row. 869 870 Params: 871 width = numebr of cells 872 873 ***********************************************************************/ 874 875 public void setWidth ( size_t width ) 876 { 877 this.cells.length = width; 878 enableStomping(this.cells); 879 } 880 881 882 /*********************************************************************** 883 884 Gets the cell in this row at the specified column. 885 886 Params: 887 col = column number 888 889 Returns: 890 pointer to cell in specified column, null if out of range 891 892 ***********************************************************************/ 893 894 public Cell* opIndex ( size_t col ) 895 { 896 Cell* c; 897 898 if ( col < this.cells.length ) 899 { 900 return &this.cells[col]; 901 } 902 903 return c; 904 } 905 906 907 /*********************************************************************** 908 909 foreach iterator over the cells in this row. 910 911 ***********************************************************************/ 912 913 public int opApply ( scope int delegate ( ref Cell cell ) dg ) 914 { 915 int res; 916 foreach ( cell; this.cells ) 917 { 918 res = dg(cell); 919 if ( !res ) break; 920 } 921 922 return res; 923 } 924 925 926 /*********************************************************************** 927 928 foreach iterator over the cells in this row and their indices. 929 930 ***********************************************************************/ 931 932 public int opApply ( scope int delegate ( ref size_t i, ref Cell cell ) dg ) 933 { 934 int res; 935 foreach ( i, cell; this.cells ) 936 { 937 res = dg(i, cell); 938 if ( res ) break; 939 } 940 941 return res; 942 } 943 944 945 /*********************************************************************** 946 947 Sets the cells in this row. The passed list must be of equal length 948 to the length of this row. 949 950 Params: 951 cells = variadic list of cells 952 953 ***********************************************************************/ 954 955 public void set ( Cell[] cells ... ) 956 { 957 verify(cells.length == this.cells.length, "row length mismatch"); 958 959 foreach ( i, cell; cells ) 960 { 961 this.cells[i] = cell; 962 } 963 } 964 965 966 /*********************************************************************** 967 968 Sets all the cells in this row to be dividers, optionally with some 969 empty cells at the left. 970 971 Params: 972 empty_cells_at_left = number of empty cells to leave at the left 973 of the row (all others will be dividers) 974 975 ***********************************************************************/ 976 977 public void setDivider ( size_t empty_cells_at_left = 0 ) 978 { 979 foreach ( i, ref cell; this.cells ) 980 { 981 if ( i < empty_cells_at_left ) 982 { 983 cell.setEmpty(); 984 } 985 else 986 { 987 cell.setDivider(); 988 } 989 } 990 } 991 992 993 /*********************************************************************** 994 995 Displays this row to the specified output, terminated with a 996 newline. 997 998 Params: 999 output = output to send cell to 1000 column_widths = display width of cells 1001 content_buf = string buffer to use for formatting cell 1002 contents 1003 spacing_buf = string buffer to use for formatting spacing to 1004 the left of the cells' contents 1005 1006 ***********************************************************************/ 1007 1008 public void display ( Output output, size_t[] column_widths, ref mstring 1009 content_buf, ref mstring spacing_buf ) 1010 { 1011 verify(column_widths.length == this.length); 1012 1013 uint merged; 1014 size_t merged_width; 1015 1016 foreach ( i, cell; this.cells ) 1017 { 1018 if ( cell.type == Cell.Type.Merged ) 1019 { 1020 merged++; 1021 merged_width += column_widths[i] + Cell.inter_cell_spacing; 1022 } 1023 else 1024 { 1025 cell.display(output, merged_width + column_widths[i], content_buf, spacing_buf); 1026 1027 merged = 0; 1028 merged_width = 0; 1029 } 1030 } 1031 1032 output.formatln(""); 1033 } 1034 } 1035 1036 1037 /*************************************************************************** 1038 1039 Convenience alias, allows the Cell struct to be accessed from the 1040 outside as Table.Cell. 1041 1042 ***************************************************************************/ 1043 1044 public alias Row.Cell Cell; 1045 1046 1047 /*************************************************************************** 1048 1049 Number of columns in the table 1050 1051 ***************************************************************************/ 1052 1053 private size_t num_columns; 1054 1055 1056 /*************************************************************************** 1057 1058 Number of characters in each column (auto calculated by the 1059 calculateColumnWidths() method) 1060 1061 ***************************************************************************/ 1062 1063 private size_t[] column_widths; 1064 1065 1066 /*************************************************************************** 1067 1068 List of table rows 1069 1070 ***************************************************************************/ 1071 1072 private Row[] rows; 1073 1074 1075 /*************************************************************************** 1076 1077 Index of the current row 1078 1079 ***************************************************************************/ 1080 1081 private size_t row_index; 1082 1083 1084 /*************************************************************************** 1085 1086 String buffers used for formatting. 1087 1088 ***************************************************************************/ 1089 1090 private char[] content_buf, spacing_buf; 1091 1092 1093 /*************************************************************************** 1094 1095 Information on merged cells -- used by scanMergedCells(). 1096 1097 ***************************************************************************/ 1098 1099 private struct MergeInfo 1100 { 1101 size_t total_width; 1102 size_t first_column; 1103 size_t last_column; 1104 } 1105 1106 private MergeInfo[] merged; 1107 1108 1109 /*************************************************************************** 1110 1111 Constructor. 1112 1113 Note: if you create a Table with this default constructor, you must call 1114 init() when you're ready to use it. 1115 1116 ***************************************************************************/ 1117 1118 public this ( ) 1119 { 1120 } 1121 1122 1123 /*************************************************************************** 1124 1125 Constructor. Sets the number of columns in the table. 1126 1127 Params: 1128 num_columns = number of columns in the table 1129 1130 ***************************************************************************/ 1131 1132 public this ( size_t num_columns ) 1133 { 1134 this.init(num_columns); 1135 } 1136 1137 1138 /*************************************************************************** 1139 1140 Init method. Must be called before any other methods are used. 1141 1142 Params: 1143 num_columns = number of columns in the table 1144 1145 ***************************************************************************/ 1146 1147 public void init ( size_t num_columns ) 1148 { 1149 this.num_columns = num_columns; 1150 this.rows.length = 0; 1151 this.row_index = 0; 1152 } 1153 1154 1155 /*************************************************************************** 1156 1157 Gets the first row in the table. 1158 1159 Returns: 1160 reference to the table's first row 1161 1162 ***************************************************************************/ 1163 1164 public Row firstRow ( ) 1165 { 1166 this.rows.length = 0; 1167 this.row_index = 0; 1168 return this.currentRow(); 1169 } 1170 1171 1172 /*************************************************************************** 1173 1174 Gets the current row in the table. 1175 1176 Returns: 1177 reference to the table's current row 1178 1179 ***************************************************************************/ 1180 1181 public Row currentRow ( ) 1182 { 1183 this.ensureRowExists(); 1184 return this.rows[this.row_index]; 1185 } 1186 1187 1188 /*************************************************************************** 1189 1190 Gets the next row in the table, adding a new row if the current row is 1191 currently the last. 1192 1193 Returns: 1194 reference to the table's next row 1195 1196 ***************************************************************************/ 1197 1198 public Row nextRow ( ) 1199 { 1200 this.row_index++; 1201 this.ensureRowExists(); 1202 return this.rows[this.row_index]; 1203 } 1204 1205 1206 /*************************************************************************** 1207 1208 Displays the table to the specified output. 1209 1210 Returns: 1211 output = output to display table to 1212 1213 ***************************************************************************/ 1214 1215 public void display ( Output output = Stdout ) 1216 { 1217 this.calculateColumnWidths(); 1218 1219 foreach ( row; this.rows ) 1220 { 1221 row.display(output, this.column_widths, this.content_buf, this.spacing_buf); 1222 } 1223 } 1224 1225 1226 /*************************************************************************** 1227 1228 Checks whether the current row already exists, and creates it if it 1229 doesn't. 1230 1231 ***************************************************************************/ 1232 1233 private void ensureRowExists ( ) 1234 { 1235 verify(this.num_columns > 0, "num_columns not set, please call init()"); 1236 1237 if ( this.rows.length <= this.row_index ) 1238 { 1239 this.rows.length = this.row_index + 1; 1240 foreach ( ref row; this.rows ) 1241 { 1242 if ( !row ) 1243 { 1244 // TODO: repeatedly calling init() will cause a memory leak 1245 row = new Row; 1246 } 1247 row.setWidth(this.num_columns); 1248 } 1249 } 1250 } 1251 1252 1253 /*************************************************************************** 1254 1255 Calculates the optimal width for each column, setting the column_widths 1256 member. 1257 1258 ***************************************************************************/ 1259 1260 private void calculateColumnWidths ( ) 1261 { 1262 this.column_widths.length = this.num_columns; 1263 this.column_widths[] = 0; 1264 1265 if ( !this.rows.length ) 1266 { 1267 return; 1268 } 1269 1270 // Find basic column widths, excluding merged cells 1271 bool in_merge; 1272 foreach ( row; this.rows ) 1273 { 1274 in_merge = false; 1275 1276 foreach ( i, cell; row ) 1277 { 1278 if ( in_merge ) 1279 { 1280 if ( cell.type != Row.Cell.Type.Merged ) 1281 { 1282 in_merge = false; 1283 } 1284 } 1285 else 1286 { 1287 if ( cell.type == Row.Cell.Type.Merged ) 1288 { 1289 in_merge = true; 1290 } 1291 else 1292 { 1293 this.column_widths[i] = 1294 cell.width > this.column_widths[i] 1295 ? cell.width 1296 : this.column_widths[i]; 1297 } 1298 } 1299 } 1300 } 1301 1302 // Find merged columns and work out how many cells they span 1303 auto merged = this.scanMergedCells(); 1304 1305 // Adjust widths of non-merged columns to fit merged columns 1306 foreach ( i, row; this.rows ) 1307 { 1308 foreach ( merge; merged ) 1309 { 1310 if ( row.cells[merge.first_column].type != Row.Cell.Type.Merged ) 1311 { 1312 // Calculate current width of all columns which merged cells 1313 // cover 1314 size_t width; 1315 foreach ( w; this.column_widths[merge.first_column..merge.last_column + 1] ) 1316 { 1317 width += w; 1318 } 1319 1320 // Add extra width to columns if the merged cells are larger 1321 // than the currently set column widths. 1322 if ( merge.total_width > width ) 1323 { 1324 auto num_merged = merge.last_column - merge.first_column; 1325 ptrdiff_t difference = merge.total_width - width - num_merged 1326 * Row.Cell.inter_cell_spacing; 1327 1328 if ( difference > 0 ) 1329 { 1330 this.expandColumns(merge.first_column, 1331 merge.last_column, difference); 1332 } 1333 } 1334 } 1335 } 1336 } 1337 } 1338 1339 1340 /*************************************************************************** 1341 1342 Find sets of merged cells. 1343 1344 Returns: 1345 list of merged cells sets 1346 1347 ***************************************************************************/ 1348 1349 private MergeInfo[] scanMergedCells ( ) 1350 { 1351 this.merged.length = 0; 1352 1353 foreach ( row; this.rows ) 1354 { 1355 bool in_merge = false; 1356 1357 foreach ( i, cell; row ) 1358 { 1359 if ( in_merge ) 1360 { 1361 this.merged[$-1].last_column = i; 1362 this.merged[$-1].total_width += cell.width; 1363 1364 if ( cell.type != Row.Cell.Type.Merged ) 1365 { 1366 in_merge = false; 1367 } 1368 } 1369 else 1370 { 1371 if ( cell.type == Row.Cell.Type.Merged ) 1372 { 1373 in_merge = true; 1374 this.merged.length = this.merged.length + 1; 1375 this.merged[$-1].first_column = i; 1376 this.merged[$-1].total_width += cell.width; 1377 } 1378 } 1379 } 1380 } 1381 1382 return this.merged; 1383 } 1384 1385 1386 /*************************************************************************** 1387 1388 Adds extra width to a specified range of columns. Extra width is 1389 distriubuted evenly between all columns in the specified range. 1390 1391 Params: 1392 first_column = index of first column in range to add extra width to 1393 last_column = index of last column in range to add extra width to 1394 extra_width = characters of extra width to distribute between all 1395 columns in the specified range 1396 1397 ***************************************************************************/ 1398 1399 private void expandColumns ( size_t first_column, size_t last_column, size_t extra_width ) 1400 { 1401 size_t column = first_column; 1402 while ( extra_width > 0 ) 1403 { 1404 this.column_widths[column]++; 1405 if ( ++column > last_column ) 1406 { 1407 column = first_column; 1408 } 1409 extra_width--; 1410 } 1411 } 1412 } 1413 1414 version ( UnitTest ) import ocean.io.device.Array : Array; 1415 1416 unittest 1417 { 1418 auto buffer = new Array(1024, 1024); 1419 1420 scope output = new FormatOutput(buffer); 1421 1422 scope table = new Table(4); 1423 1424 table.firstRow.setDivider(); 1425 1426 table.nextRow.set(Table.Cell.Merged, Table.Cell.String("0xdb6db6e4 .. 0xedb6db76"), 1427 Table.Cell.Merged, Table.Cell.String("0xedb6db77 .. 0xffffffff")); 1428 1429 table.nextRow.setDivider(); 1430 1431 table.nextRow.set(Table.Cell.String("Records"), Table.Cell.String("Bytes"), 1432 Table.Cell.String("Records"), Table.Cell.String("Bytes")); 1433 1434 table.nextRow.setDivider(); 1435 1436 struct Node 1437 { 1438 int records1, records2, bytes1, bytes2; 1439 } 1440 1441 static immutable nodes = 1442 [ 1443 Node(123456, 789012, 345678, 901234), 1444 Node(901234, 123456, 789012, 345678), 1445 Node(345678, 901234, 123456, 789012), 1446 ]; 1447 1448 static bool use_thousands = true; 1449 static bool dont_use_thousands = false; 1450 1451 foreach ( node; nodes ) 1452 { 1453 table.nextRow.set(Table.Cell.Integer(node.records1), 1454 Table.Cell.Integer(node.bytes1, use_thousands), 1455 Table.Cell.Integer(node.records2, dont_use_thousands), 1456 Table.Cell.Integer(node.bytes2, dont_use_thousands)); 1457 } 1458 1459 table.nextRow.setDivider(); 1460 1461 table.display(output); 1462 1463 // note: The string literal embeds escape characters, which are used by 1464 // the table functions to set foreground/background colors in the console. 1465 static immutable check = 1466 `------------------------------------------------------ 1467 0xdb6db6e4 .. 0xedb6db76 [39m[49m| 0xedb6db77 .. 0xffffffff [39m[49m| 1468 ------------------------------------------------------ 1469 Records [39m[49m| Bytes [39m[49m| Records [39m[49m| Bytes [39m[49m| 1470 ------------------------------------------------------ 1471 123,456 [39m[49m| 345,678 [39m[49m| 789012 [39m[49m| 901234 [39m[49m| 1472 901,234 [39m[49m| 789,012 [39m[49m| 123456 [39m[49m| 345678 [39m[49m| 1473 345,678 [39m[49m| 123,456 [39m[49m| 901234 [39m[49m| 789012 [39m[49m| 1474 ------------------------------------------------------ 1475 `; 1476 1477 auto result = cast(char[])buffer.slice(); 1478 1479 test(result == check, result); 1480 }