1 module lantern.table;
2 
3 import std.meta;
4 import std.traits;
5 
6 ///
7 void printTable(Results)(Results results)
8 {
9     import std.array : appender;
10     import std.stdio : writeln;
11 
12     auto table = TablePrinter!Results(results);
13     auto buffer = appender!string;
14     table.writeTo(buffer);
15 
16     writeln(buffer.data);
17 }
18 
19 unittest
20 {
21     struct X
22     {
23         int a;
24         int b;
25     }
26 
27     struct Y
28     {
29         int a;
30         int c;
31     }
32 
33     struct Z
34     {
35         int d;
36     }
37 
38     static assert(MergeFieldNames!(X, Y) == AliasSeq!("a", "b", "c"));
39     static assert(MergeFieldNames!(X, Y, Z) == AliasSeq!("a", "b", "c", "d"));
40 }
41 
42 private alias MergeFieldNames(Ts...) = NoDuplicates!(staticMap!(FieldNameTuple, Ts));
43 
44 unittest
45 {
46     struct NumericResult
47     {
48         size_t count;
49         double min;
50         double max;
51     }
52 
53     struct CategoricalResult
54     {
55         size_t count;
56         string top;
57         size_t freq;
58     }
59 
60     struct Results
61     {
62         NumericResult num;
63         CategoricalResult text;
64     }
65 
66     alias B = MergeFieldNames!(Fields!Results);
67     static assert(B == AliasSeq!("count", "min", "max", "top", "freq"));
68 }
69 
70 struct TablePrinter(T)
71 {
72     T result;
73 
74     alias ColumnNames = FieldNameTuple!T;
75     alias RowNames = MergeFieldNames!(Fields!T);
76 
77     void writeTo(R)(R buffer)
78     {
79         import std.array : array;
80         import std.algorithm : map, max, joiner, reduce;
81         import std.range : put;
82         import std.range : repeat;
83         import eastasianwidth : displayWidth;
84 
85         enum rowNameLength = [RowNames].map!(name => displayWidth(name)).array();
86         enum maxNameLength = rowNameLength.reduce!max;
87         enum paddingHeaderText = "|" ~ repeatText(maxNameLength + 2, ' ');
88         enum splitHeaderText = "|" ~ repeatText(maxNameLength + 2, '-');
89 
90         put(buffer, paddingHeaderText);
91 
92         import std.conv;
93 
94         string[][string] cells;
95         static foreach (colName; ColumnNames)
96         {
97             static foreach (rowName; RowNames)
98             {
99                 static if (__traits(compiles, {
100                         auto s = __traits(getMember, __traits(getMember,
101                         result, colName), rowName);
102                     }))
103                 {
104                     cells[colName] ~= to!string(__traits(getMember,
105                             __traits(getMember, result, colName), rowName));
106                 }
107                 else
108                 {
109                     cells[colName] ~= "";
110                 }
111             }
112         }
113 
114         size_t[string] columnWidths;
115         static foreach (colName; ColumnNames)
116         {
117             columnWidths[colName] = max(cells[colName].map!(c => displayWidth(c))
118                     .reduce!max(), displayWidth(colName));
119         }
120 
121         static foreach (colName; ColumnNames)
122         {
123             put(buffer, "| ");
124             put(buffer, colName);
125             put(buffer, repeatText(columnWidths[colName] - displayWidth(colName) + 1));
126         }
127         put(buffer, "|\n");
128 
129         put(buffer, splitHeaderText);
130         static foreach (colName; ColumnNames)
131         {
132             put(buffer, '|');
133             put(buffer, repeatText(columnWidths[colName] + 2, '-'));
134         }
135         put(buffer, "|\n");
136 
137         static foreach (j, rowName; RowNames)
138         {
139             put(buffer, "| ");
140             put(buffer, rowName);
141             put(buffer, repeatText(maxNameLength - displayWidth(rowName) + 1));
142 
143             static foreach (i, colName; ColumnNames)
144             {
145                 put(buffer, '|');
146                 put(buffer, ' ');
147                 put(buffer, cells[colName][j]);
148                 put(buffer, repeatText(columnWidths[colName] - displayWidth(cells[colName][j]) + 1));
149             }
150             put(buffer, "|\n");
151         }
152     }
153 }
154 
155 unittest
156 {
157     struct NumericResult
158     {
159         size_t count;
160         double min;
161         double max;
162     }
163 
164     struct CategoricalResult
165     {
166         size_t count;
167         string top;
168         size_t freq;
169         size_t サイズ;
170     }
171 
172     struct Results
173     {
174         NumericResult num;
175         CategoricalResult text;
176     }
177 
178     TablePrinter!Results table;
179     table.result.num.min = -1000;
180     table.result.text.top = "lempiji";
181 
182     import std.array : appender;
183 
184     auto buffer = appender!string();
185     table.writeTo(buffer);
186 
187     // import std.stdio : writeln;
188 
189     // writeln(buffer.data);
190 }
191 
192 private string[] tablePadRight(string[] texts)
193 {
194     import std.algorithm : map, max, reduce;
195     import eastasianwidth : displayWidth;
196 
197     auto displayWidthList = texts.map!(s => displayWidth(s));
198     const maxDisplayWidth = displayWidthList.reduce!max();
199 
200     auto results = new string[texts.length];
201     foreach (i, ref result; results)
202     {
203         result = ' ' ~ texts[i] ~ repeatText(maxDisplayWidth - displayWidthList[i] + 1);
204     }
205     return results;
206 }
207 
208 unittest
209 {
210     enum texts = tablePadRight(["text", "value", "a", "b"]);
211     assert(texts[0] == " text  ");
212     assert(texts[1] == " value ");
213     assert(texts[2] == " a     ");
214     assert(texts[3] == " b     ");
215 }
216 
217 private string repeatText(size_t n, char c = ' ')
218 in(n > 0)
219 in(n < 1000)
220 {
221     auto text = new char[n];
222     text[] = c;
223     import std : assumeUnique;
224 
225     return text.assumeUnique();
226 }
227 
228 unittest
229 {
230     enum a = repeatText(4);
231     assert(a == "    ");
232 }
233 
234 private size_t calculateColumnWidth(size_t dataWidth, size_t columnNameLength)
235 {
236     if (dataWidth > columnNameLength)
237     {
238         return dataWidth + 2;
239     }
240     return columnNameLength + 2;
241 }