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 }