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.meta.types.Qualifiers; 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 ) return 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 ) return 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, 461 cstring metric_string = "" ) return 462 { 463 this.type = Type.BinaryMetric; 464 this.contents.integer = num; 465 this.metric_string.copy(metric_string); 466 467 return &this; 468 } 469 470 471 /******************************************************************* 472 473 Sets the cell to contain an integer scaled into a decimal metric 474 representation (K, M, G, T, etc). 475 476 Params: 477 num = integer to set 478 metric_string = metric identifier (eg bytes, Kbytes, Mbytes, 479 etc.) 480 481 Returns: 482 this instance for method chaining 483 484 *******************************************************************/ 485 486 public typeof(&this) setDecimalMetric ( ulong num, 487 cstring metric_string = "" ) return 488 { 489 this.type = Type.DecimalMetric; 490 this.contents.integer = num; 491 this.metric_string.copy(metric_string); 492 493 return &this; 494 } 495 496 497 /******************************************************************* 498 499 Sets the cell to contain a float. 500 501 Params: 502 num = float to set 503 504 Returns: 505 this instance for method chaining 506 507 *******************************************************************/ 508 509 public typeof(&this) setFloat ( double num ) return 510 { 511 this.type = Type.Float; 512 this.contents.floating = num; 513 514 return &this; 515 } 516 517 518 /******************************************************************* 519 520 Sets the cell to contain nothing. 521 522 Returns: 523 this instance for method chaining 524 525 *******************************************************************/ 526 527 public typeof(&this) setEmpty ( ) return 528 { 529 this.type = Type.Empty; 530 531 return &this; 532 } 533 534 535 /******************************************************************* 536 537 Sets the cell to contain a horizontal divider. 538 539 Returns: 540 this instance for method chaining 541 542 *******************************************************************/ 543 544 public typeof(&this) setDivider ( ) return 545 { 546 this.type = Type.Divider; 547 548 return &this; 549 } 550 551 552 /******************************************************************* 553 554 Sets the cell to be merged with the cell to its right. 555 556 Returns: 557 this instance for method chaining 558 559 *******************************************************************/ 560 561 public typeof(&this) setMerged ( ) return 562 { 563 this.type = Type.Merged; 564 565 return &this; 566 } 567 568 569 /******************************************************************* 570 571 Sets the foreground colour of this cell 572 573 Params: 574 colour = The colour to use 575 576 Returns: 577 this instance for method chaining 578 579 *******************************************************************/ 580 581 public typeof(&this) setForegroundColour ( Terminal.Colour colour ) 582 return 583 { 584 auto colour_str = Terminal.fg_colour_codes[colour]; 585 this.fg_colour_string.concat(Terminal.CSI, colour_str); 586 587 return &this; 588 } 589 590 591 /******************************************************************* 592 593 Sets the background colour of this cell 594 595 Params: 596 colour = The colour to use 597 598 Returns: 599 this instance for method chaining 600 601 *******************************************************************/ 602 603 public typeof(&this) setBackgroundColour ( Terminal.Colour colour ) 604 return 605 { 606 auto colour_str = Terminal.bg_colour_codes[colour]; 607 this.bg_colour_string.concat(Terminal.CSI, colour_str); 608 609 return &this; 610 } 611 612 613 /******************************************************************* 614 615 Returns: 616 the width of cell's contents, in characters 617 618 *******************************************************************/ 619 620 public size_t width ( ) 621 { 622 switch ( this.type ) 623 { 624 case Cell.Type.Merged: 625 case Cell.Type.Empty: 626 case Cell.Type.Divider: 627 return 0; 628 case Cell.Type.BinaryMetric: 629 MetricPrefix metric; 630 metric.bin(this.contents.integer); 631 return this.floatWidth(metric.scaled) + 3 + 632 this.metric_string.length; 633 case Cell.Type.DecimalMetric: 634 MetricPrefix metric; 635 metric.dec(this.contents.integer); 636 return this.floatWidth(metric.scaled) + 2 + 637 this.metric_string.length; 638 case Cell.Type.Integer: 639 return this.integerWidth(this.contents.integer); 640 case Cell.Type.Float: 641 return this.floatWidth(this.contents.floating); 642 case Cell.Type.String: 643 return utf8Length(this.contents.utf8); 644 645 default: 646 assert(0); 647 } 648 } 649 650 651 /******************************************************************* 652 653 Displays the cell to the specified output. 654 655 Params: 656 output = output to send cell to 657 width = display width of cell 658 content_buf = string buffer to use for formatting cell 659 contents 660 spacing_buf = string buffer to use for formatting spacing to 661 the left of the cell's contents 662 663 *******************************************************************/ 664 665 public void display ( Output output, size_t width, ref mstring 666 content_buf, ref mstring spacing_buf ) 667 { 668 // sequence of control characters to reset output colors to default 669 istring default_colours = 670 Terminal.CSI ~ Terminal.fg_colour_codes[Terminal.Colour.Default] ~ 671 Terminal.CSI ~ Terminal.bg_colour_codes[Terminal.Colour.Default]; 672 673 // set the colour of this cell 674 if ( this.fg_colour_string.length > 0 || 675 this.bg_colour_string.length > 0 ) 676 { 677 output.format("{}{}", this.fg_colour_string, this.bg_colour_string); 678 } 679 680 if ( this.type == Type.Divider ) 681 { 682 content_buf.length = width + inter_cell_spacing; 683 assumeSafeAppend(content_buf); 684 content_buf[] = '-'; 685 686 output.format("{}", content_buf); 687 688 // reset colour to default 689 if ( this.fg_colour_string.length > 0 || 690 this.bg_colour_string.length > 0 ) 691 { 692 output.format("{}", default_colours); 693 } 694 } 695 else 696 { 697 switch ( this.type ) 698 { 699 case Type.Empty: 700 content_buf.length = 0; 701 assumeSafeAppend(content_buf); 702 break; 703 case Type.BinaryMetric: 704 content_buf.length = 0; 705 assumeSafeAppend(content_buf); 706 707 MetricPrefix metric; 708 metric.bin(this.contents.integer); 709 710 if ( metric.prefix == ' ' ) 711 { 712 sformat(content_buf, 713 "{} {}", cast(uint)metric.scaled, 714 this.metric_string); 715 } 716 else 717 { 718 sformat(content_buf, 719 "{} {}i{}", metric.scaled, metric.prefix, 720 this.metric_string); 721 } 722 break; 723 case Type.DecimalMetric: 724 content_buf.length = 0; 725 assumeSafeAppend(content_buf); 726 727 MetricPrefix metric; 728 metric.dec(this.contents.integer); 729 730 if ( metric.prefix == ' ' ) 731 { 732 sformat(content_buf, 733 "{} {}", cast(uint)metric.scaled, 734 this.metric_string); 735 } 736 else 737 { 738 sformat(content_buf, 739 "{} {}{}", metric.scaled, metric.prefix, 740 this.metric_string); 741 } 742 break; 743 case Type.Integer: 744 if ( this.use_thousands_separator ) 745 { 746 DigitGrouping.format(this.contents.integer, content_buf); 747 } 748 else 749 { 750 content_buf.length = 0; 751 assumeSafeAppend(content_buf); 752 sformat(content_buf, 753 "{}", this.contents.integer); 754 } 755 break; 756 case Type.Float: 757 content_buf.length = 0; 758 assumeSafeAppend(content_buf); 759 sformat(content_buf, 760 "{}", this.contents.floating); 761 break; 762 case Type.String: 763 content_buf = this.contents.utf8; 764 break; 765 default: 766 return; 767 } 768 769 verify(width >= utf8Length(content_buf), "column not wide enough"); 770 771 spacing_buf.length = width - utf8Length(content_buf); 772 assumeSafeAppend(spacing_buf); 773 spacing_buf[] = ' '; 774 output.format(" {}{} ", spacing_buf, content_buf); 775 776 // reset colour to default 777 output.format("{}", default_colours); 778 779 output.format("|"); 780 } 781 } 782 783 784 /******************************************************************* 785 786 Calculates the number of characters required to display 787 an integer (the number of digits) 788 789 Params: 790 i = integer to calculate width of 791 792 Returns: 793 number of characters required to display i 794 795 *******************************************************************/ 796 797 private size_t integerWidth ( ulong i ) 798 { 799 if ( this.use_thousands_separator ) 800 { 801 return DigitGrouping.length(i); 802 } 803 804 size_t digits; 805 while (i) 806 { 807 i /= 10; 808 ++digits; 809 } 810 811 return digits; 812 } 813 814 815 /******************************************************************* 816 817 Calculates the number of characters required to display a float. 818 819 Params: 820 f = float to calculate width of 821 822 Returns: 823 number of character required to display f 824 825 *******************************************************************/ 826 827 private size_t floatWidth ( double f ) 828 { 829 size_t width = 4; // 0.00 830 if ( f < 0 ) 831 { 832 f = -f; 833 width++; // minus symbol 834 } 835 836 double dec = 10; 837 while ( f >= dec ) 838 { 839 width++; 840 dec *= 10; 841 } 842 843 return width; 844 } 845 } 846 847 848 /*********************************************************************** 849 850 List of cells in row 851 852 ***********************************************************************/ 853 854 public Cell[] cells; 855 856 857 /*********************************************************************** 858 859 Returns: 860 the number of cells in this row 861 862 ***********************************************************************/ 863 864 public size_t length ( ) 865 { 866 return this.cells.length; 867 } 868 869 870 /*********************************************************************** 871 872 Sets the number of cells in this row. 873 874 Params: 875 width = numebr of cells 876 877 ***********************************************************************/ 878 879 public void setWidth ( size_t width ) 880 { 881 this.cells.length = width; 882 assumeSafeAppend(this.cells); 883 } 884 885 886 /*********************************************************************** 887 888 Gets the cell in this row at the specified column. 889 890 Params: 891 col = column number 892 893 Returns: 894 pointer to cell in specified column, null if out of range 895 896 ***********************************************************************/ 897 898 public Cell* opIndex ( size_t col ) 899 { 900 Cell* c; 901 902 if ( col < this.cells.length ) 903 { 904 return &this.cells[col]; 905 } 906 907 return c; 908 } 909 910 911 /*********************************************************************** 912 913 foreach iterator over the cells in this row. 914 915 ***********************************************************************/ 916 917 public int opApply ( scope int delegate ( ref Cell cell ) dg ) 918 { 919 int res; 920 foreach ( cell; this.cells ) 921 { 922 res = dg(cell); 923 if ( !res ) break; 924 } 925 926 return res; 927 } 928 929 930 /*********************************************************************** 931 932 foreach iterator over the cells in this row and their indices. 933 934 ***********************************************************************/ 935 936 public int opApply ( scope int delegate ( ref size_t i, ref Cell cell ) dg ) 937 { 938 int res; 939 foreach ( i, cell; this.cells ) 940 { 941 res = dg(i, cell); 942 if ( res ) break; 943 } 944 945 return res; 946 } 947 948 949 /*********************************************************************** 950 951 Sets the cells in this row. The passed list must be of equal length 952 to the length of this row. 953 954 Params: 955 cells = variadic list of cells 956 957 ***********************************************************************/ 958 959 public void set ( Cell[] cells ... ) 960 { 961 verify(cells.length == this.cells.length, "row length mismatch"); 962 963 foreach ( i, cell; cells ) 964 { 965 this.cells[i] = cell; 966 } 967 } 968 969 970 /*********************************************************************** 971 972 Sets all the cells in this row to be dividers, optionally with some 973 empty cells at the left. 974 975 Params: 976 empty_cells_at_left = number of empty cells to leave at the left 977 of the row (all others will be dividers) 978 979 ***********************************************************************/ 980 981 public void setDivider ( size_t empty_cells_at_left = 0 ) 982 { 983 foreach ( i, ref cell; this.cells ) 984 { 985 if ( i < empty_cells_at_left ) 986 { 987 cell.setEmpty(); 988 } 989 else 990 { 991 cell.setDivider(); 992 } 993 } 994 } 995 996 997 /*********************************************************************** 998 999 Displays this row to the specified output, terminated with a 1000 newline. 1001 1002 Params: 1003 output = output to send cell to 1004 column_widths = display width of cells 1005 content_buf = string buffer to use for formatting cell 1006 contents 1007 spacing_buf = string buffer to use for formatting spacing to 1008 the left of the cells' contents 1009 1010 ***********************************************************************/ 1011 1012 public void display ( Output output, size_t[] column_widths, ref mstring 1013 content_buf, ref mstring spacing_buf ) 1014 { 1015 verify(column_widths.length == this.length); 1016 1017 uint merged; 1018 size_t merged_width; 1019 1020 foreach ( i, cell; this.cells ) 1021 { 1022 if ( cell.type == Cell.Type.Merged ) 1023 { 1024 merged++; 1025 merged_width += column_widths[i] + Cell.inter_cell_spacing; 1026 } 1027 else 1028 { 1029 cell.display(output, merged_width + column_widths[i], content_buf, spacing_buf); 1030 1031 merged = 0; 1032 merged_width = 0; 1033 } 1034 } 1035 1036 output.formatln(""); 1037 } 1038 } 1039 1040 1041 /*************************************************************************** 1042 1043 Convenience alias, allows the Cell struct to be accessed from the 1044 outside as Table.Cell. 1045 1046 ***************************************************************************/ 1047 1048 public alias Row.Cell Cell; 1049 1050 1051 /*************************************************************************** 1052 1053 Number of columns in the table 1054 1055 ***************************************************************************/ 1056 1057 private size_t num_columns; 1058 1059 1060 /*************************************************************************** 1061 1062 Number of characters in each column (auto calculated by the 1063 calculateColumnWidths() method) 1064 1065 ***************************************************************************/ 1066 1067 private size_t[] column_widths; 1068 1069 1070 /*************************************************************************** 1071 1072 List of table rows 1073 1074 ***************************************************************************/ 1075 1076 private Row[] rows; 1077 1078 1079 /*************************************************************************** 1080 1081 Index of the current row 1082 1083 ***************************************************************************/ 1084 1085 private size_t row_index; 1086 1087 1088 /*************************************************************************** 1089 1090 String buffers used for formatting. 1091 1092 ***************************************************************************/ 1093 1094 private char[] content_buf, spacing_buf; 1095 1096 1097 /*************************************************************************** 1098 1099 Information on merged cells -- used by scanMergedCells(). 1100 1101 ***************************************************************************/ 1102 1103 private struct MergeInfo 1104 { 1105 size_t total_width; 1106 size_t first_column; 1107 size_t last_column; 1108 } 1109 1110 private MergeInfo[] merged; 1111 1112 1113 /*************************************************************************** 1114 1115 Constructor. 1116 1117 Note: if you create a Table with this default constructor, you must call 1118 init() when you're ready to use it. 1119 1120 ***************************************************************************/ 1121 1122 public this ( ) 1123 { 1124 } 1125 1126 1127 /*************************************************************************** 1128 1129 Constructor. Sets the number of columns in the table. 1130 1131 Params: 1132 num_columns = number of columns in the table 1133 1134 ***************************************************************************/ 1135 1136 public this ( size_t num_columns ) 1137 { 1138 this.init(num_columns); 1139 } 1140 1141 1142 /*************************************************************************** 1143 1144 Init method. Must be called before any other methods are used. 1145 1146 Params: 1147 num_columns = number of columns in the table 1148 1149 ***************************************************************************/ 1150 1151 public void init ( size_t num_columns ) 1152 { 1153 this.num_columns = num_columns; 1154 this.rows.length = 0; 1155 this.row_index = 0; 1156 } 1157 1158 1159 /*************************************************************************** 1160 1161 Gets the first row in the table. 1162 1163 Returns: 1164 reference to the table's first row 1165 1166 ***************************************************************************/ 1167 1168 public Row firstRow ( ) 1169 { 1170 this.rows.length = 0; 1171 this.row_index = 0; 1172 return this.currentRow(); 1173 } 1174 1175 1176 /*************************************************************************** 1177 1178 Gets the current row in the table. 1179 1180 Returns: 1181 reference to the table's current row 1182 1183 ***************************************************************************/ 1184 1185 public Row currentRow ( ) 1186 { 1187 this.ensureRowExists(); 1188 return this.rows[this.row_index]; 1189 } 1190 1191 1192 /*************************************************************************** 1193 1194 Gets the next row in the table, adding a new row if the current row is 1195 currently the last. 1196 1197 Returns: 1198 reference to the table's next row 1199 1200 ***************************************************************************/ 1201 1202 public Row nextRow ( ) 1203 { 1204 this.row_index++; 1205 this.ensureRowExists(); 1206 return this.rows[this.row_index]; 1207 } 1208 1209 1210 /*************************************************************************** 1211 1212 Displays the table to the specified output. 1213 1214 Returns: 1215 output = output to display table to 1216 1217 ***************************************************************************/ 1218 1219 public void display ( Output output = Stdout ) 1220 { 1221 this.calculateColumnWidths(); 1222 1223 foreach ( row; this.rows ) 1224 { 1225 row.display(output, this.column_widths, this.content_buf, this.spacing_buf); 1226 } 1227 } 1228 1229 1230 /*************************************************************************** 1231 1232 Checks whether the current row already exists, and creates it if it 1233 doesn't. 1234 1235 ***************************************************************************/ 1236 1237 private void ensureRowExists ( ) 1238 { 1239 verify(this.num_columns > 0, "num_columns not set, please call init()"); 1240 1241 if ( this.rows.length <= this.row_index ) 1242 { 1243 this.rows.length = this.row_index + 1; 1244 foreach ( ref row; this.rows ) 1245 { 1246 if ( !row ) 1247 { 1248 // TODO: repeatedly calling init() will cause a memory leak 1249 row = new Row; 1250 } 1251 row.setWidth(this.num_columns); 1252 } 1253 } 1254 } 1255 1256 1257 /*************************************************************************** 1258 1259 Calculates the optimal width for each column, setting the column_widths 1260 member. 1261 1262 ***************************************************************************/ 1263 1264 private void calculateColumnWidths ( ) 1265 { 1266 this.column_widths.length = this.num_columns; 1267 this.column_widths[] = 0; 1268 1269 if ( !this.rows.length ) 1270 { 1271 return; 1272 } 1273 1274 // Find basic column widths, excluding merged cells 1275 bool in_merge; 1276 foreach ( row; this.rows ) 1277 { 1278 in_merge = false; 1279 1280 foreach ( i, cell; row ) 1281 { 1282 if ( in_merge ) 1283 { 1284 if ( cell.type != Row.Cell.Type.Merged ) 1285 { 1286 in_merge = false; 1287 } 1288 } 1289 else 1290 { 1291 if ( cell.type == Row.Cell.Type.Merged ) 1292 { 1293 in_merge = true; 1294 } 1295 else 1296 { 1297 this.column_widths[i] = 1298 cell.width > this.column_widths[i] 1299 ? cell.width 1300 : this.column_widths[i]; 1301 } 1302 } 1303 } 1304 } 1305 1306 // Find merged columns and work out how many cells they span 1307 auto merged = this.scanMergedCells(); 1308 1309 // Adjust widths of non-merged columns to fit merged columns 1310 foreach ( i, row; this.rows ) 1311 { 1312 foreach ( merge; merged ) 1313 { 1314 if ( row.cells[merge.first_column].type != Row.Cell.Type.Merged ) 1315 { 1316 // Calculate current width of all columns which merged cells 1317 // cover 1318 size_t width; 1319 foreach ( w; this.column_widths[merge.first_column..merge.last_column + 1] ) 1320 { 1321 width += w; 1322 } 1323 1324 // Add extra width to columns if the merged cells are larger 1325 // than the currently set column widths. 1326 if ( merge.total_width > width ) 1327 { 1328 auto num_merged = merge.last_column - merge.first_column; 1329 ptrdiff_t difference = merge.total_width - width - num_merged 1330 * Row.Cell.inter_cell_spacing; 1331 1332 if ( difference > 0 ) 1333 { 1334 this.expandColumns(merge.first_column, 1335 merge.last_column, difference); 1336 } 1337 } 1338 } 1339 } 1340 } 1341 } 1342 1343 1344 /*************************************************************************** 1345 1346 Find sets of merged cells. 1347 1348 Returns: 1349 list of merged cells sets 1350 1351 ***************************************************************************/ 1352 1353 private MergeInfo[] scanMergedCells ( ) 1354 { 1355 this.merged.length = 0; 1356 1357 foreach ( row; this.rows ) 1358 { 1359 bool in_merge = false; 1360 1361 foreach ( i, cell; row ) 1362 { 1363 if ( in_merge ) 1364 { 1365 this.merged[$-1].last_column = i; 1366 this.merged[$-1].total_width += cell.width; 1367 1368 if ( cell.type != Row.Cell.Type.Merged ) 1369 { 1370 in_merge = false; 1371 } 1372 } 1373 else 1374 { 1375 if ( cell.type == Row.Cell.Type.Merged ) 1376 { 1377 in_merge = true; 1378 this.merged.length = this.merged.length + 1; 1379 this.merged[$-1].first_column = i; 1380 this.merged[$-1].total_width += cell.width; 1381 } 1382 } 1383 } 1384 } 1385 1386 return this.merged; 1387 } 1388 1389 1390 /*************************************************************************** 1391 1392 Adds extra width to a specified range of columns. Extra width is 1393 distriubuted evenly between all columns in the specified range. 1394 1395 Params: 1396 first_column = index of first column in range to add extra width to 1397 last_column = index of last column in range to add extra width to 1398 extra_width = characters of extra width to distribute between all 1399 columns in the specified range 1400 1401 ***************************************************************************/ 1402 1403 private void expandColumns ( size_t first_column, size_t last_column, size_t extra_width ) 1404 { 1405 size_t column = first_column; 1406 while ( extra_width > 0 ) 1407 { 1408 this.column_widths[column]++; 1409 if ( ++column > last_column ) 1410 { 1411 column = first_column; 1412 } 1413 extra_width--; 1414 } 1415 } 1416 } 1417 1418 version (unittest) import ocean.io.device.Array : Array; 1419 1420 unittest 1421 { 1422 auto buffer = new Array(1024, 1024); 1423 1424 scope output = new FormatOutput(buffer); 1425 1426 scope table = new Table(4); 1427 1428 table.firstRow.setDivider(); 1429 1430 table.nextRow.set(Table.Cell.Merged, Table.Cell.String("0xdb6db6e4 .. 0xedb6db76"), 1431 Table.Cell.Merged, Table.Cell.String("0xedb6db77 .. 0xffffffff")); 1432 1433 table.nextRow.setDivider(); 1434 1435 table.nextRow.set(Table.Cell.String("Records"), Table.Cell.String("Bytes"), 1436 Table.Cell.String("Records"), Table.Cell.String("Bytes")); 1437 1438 table.nextRow.setDivider(); 1439 1440 struct Node 1441 { 1442 int records1, records2, bytes1, bytes2; 1443 } 1444 1445 static immutable nodes = 1446 [ 1447 Node(123456, 789012, 345678, 901234), 1448 Node(901234, 123456, 789012, 345678), 1449 Node(345678, 901234, 123456, 789012), 1450 ]; 1451 1452 static bool use_thousands = true; 1453 static bool dont_use_thousands = false; 1454 1455 foreach ( node; nodes ) 1456 { 1457 table.nextRow.set(Table.Cell.Integer(node.records1), 1458 Table.Cell.Integer(node.bytes1, use_thousands), 1459 Table.Cell.Integer(node.records2, dont_use_thousands), 1460 Table.Cell.Integer(node.bytes2, dont_use_thousands)); 1461 } 1462 1463 table.nextRow.setDivider(); 1464 1465 table.display(output); 1466 1467 // note: The string literal embeds escape characters, which are used by 1468 // the table functions to set foreground/background colors in the console. 1469 static immutable check = 1470 `------------------------------------------------------ 1471 0xdb6db6e4 .. 0xedb6db76 [39m[49m| 0xedb6db77 .. 0xffffffff [39m[49m| 1472 ------------------------------------------------------ 1473 Records [39m[49m| Bytes [39m[49m| Records [39m[49m| Bytes [39m[49m| 1474 ------------------------------------------------------ 1475 123,456 [39m[49m| 345,678 [39m[49m| 789012 [39m[49m| 901234 [39m[49m| 1476 901,234 [39m[49m| 789,012 [39m[49m| 123456 [39m[49m| 345678 [39m[49m| 1477 345,678 [39m[49m| 123,456 [39m[49m| 901234 [39m[49m| 789012 [39m[49m| 1478 ------------------------------------------------------ 1479 `; 1480 1481 auto result = cast(char[])buffer.slice(); 1482 1483 test(result == check, result); 1484 }