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