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 | 0xedb6db77 .. 0xffffffff |
1472 ------------------------------------------------------
1473      Records |      Bytes |     Records |      Bytes |
1474 ------------------------------------------------------
1475      123,456 |    345,678 |      789012 |     901234 |
1476      901,234 |    789,012 |      123456 |     345678 |
1477      345,678 |    123,456 |      901234 |     789012 |
1478 ------------------------------------------------------
1479 `;
1480 
1481     auto result = cast(char[])buffer.slice();
1482 
1483     test(result == check, result);
1484 }