• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.unicode.cldr.tool;
2 
3 import java.io.PrintWriter;
4 import java.util.ArrayList;
5 import java.util.Arrays;
6 import java.util.BitSet;
7 import java.util.Collection;
8 import java.util.Comparator;
9 import java.util.List;
10 
11 import com.ibm.icu.text.Collator;
12 import com.ibm.icu.text.MessageFormat;
13 import com.ibm.icu.text.UnicodeSet;
14 import com.ibm.icu.util.ULocale;
15 
16 public class TablePrinter {
17 
main(String[] args)18     public static void main(String[] args) {
19         // quick test;
20         TablePrinter tablePrinter = new TablePrinter()
21             .setTableAttributes("style='border-collapse: collapse' border='1'")
22             .addColumn("Language").setSpanRows(true).setSortPriority(0).setBreakSpans(true)
23             .addColumn("Junk").setSpanRows(true)
24             .addColumn("Territory").setHeaderAttributes("bgcolor='green'").setCellAttributes("align='right'")
25             .setSpanRows(true)
26             .setSortPriority(1).setSortAscending(false);
27         Comparable<?>[][] data = {
28             { "German", 1.3d, 3 },
29             { "French", 1.3d, 2 },
30             { "English", 1.3d, 2 },
31             { "English", 1.3d, 4 },
32             { "English", 1.3d, 6 },
33             { "English", 1.3d, 8 },
34             { "Arabic", 1.3d, 5 },
35             { "Zebra", 1.3d, 10 }
36         };
37         tablePrinter.addRows(data);
38         tablePrinter.addRow().addCell("Foo").addCell(1.5d).addCell(99).finishRow();
39 
40         String s = tablePrinter.toTable();
41         System.out.println(s);
42     }
43 
44     private List<Column> columns = new ArrayList<>();
45     private String tableAttributes;
46     private transient Column[] columnsFlat;
47     private List<Comparable<Object>[]> rows = new ArrayList<>();
48     private String caption;
49 
getTableAttributes()50     public String getTableAttributes() {
51         return tableAttributes;
52     }
53 
setTableAttributes(String tableAttributes)54     public TablePrinter setTableAttributes(String tableAttributes) {
55         this.tableAttributes = tableAttributes;
56         return this;
57     }
58 
setCaption(String caption)59     public TablePrinter setCaption(String caption) {
60         this.caption = caption;
61         return this;
62     }
63 
setSortPriority(int priority)64     public TablePrinter setSortPriority(int priority) {
65         columnSorter.setSortPriority(columns.size() - 1, priority);
66         sort = true;
67         return this;
68     }
69 
setSortAscending(boolean ascending)70     public TablePrinter setSortAscending(boolean ascending) {
71         columnSorter.setSortAscending(columns.size() - 1, ascending);
72         return this;
73     }
74 
setBreakSpans(boolean breaks)75     public TablePrinter setBreakSpans(boolean breaks) {
76         breaksSpans.set(columns.size() - 1, breaks);
77         return this;
78     }
79 
80     private static class Column {
81         String header;
82         String headerAttributes;
83         MessageFormat cellAttributes;
84 
85         boolean spanRows;
86         MessageFormat cellPattern;
87         private boolean repeatHeader = false;
88         private boolean hidden = false;
89         private boolean isHeader = false;
90 //        private boolean divider = false;
91 
Column(String header)92         public Column(String header) {
93             this.header = header;
94         }
95 
setCellAttributes(String cellAttributes)96         public Column setCellAttributes(String cellAttributes) {
97             this.cellAttributes = new MessageFormat(MessageFormat.autoQuoteApostrophe(cellAttributes), ULocale.ENGLISH);
98             return this;
99         }
100 
setCellPattern(String cellPattern)101         public Column setCellPattern(String cellPattern) {
102             this.cellPattern = cellPattern == null ? null : new MessageFormat(
103                 MessageFormat.autoQuoteApostrophe(cellPattern), ULocale.ENGLISH);
104             return this;
105         }
106 
setHeaderAttributes(String headerAttributes)107         public Column setHeaderAttributes(String headerAttributes) {
108             this.headerAttributes = headerAttributes;
109             return this;
110         }
111 
setSpanRows(boolean spanRows)112         public Column setSpanRows(boolean spanRows) {
113             this.spanRows = spanRows;
114             return this;
115         }
116 
setRepeatHeader(boolean b)117         public void setRepeatHeader(boolean b) {
118             repeatHeader = b;
119         }
120 
setHidden(boolean b)121         public void setHidden(boolean b) {
122             hidden = b;
123         }
124 
setHeaderCell(boolean b)125         public void setHeaderCell(boolean b) {
126             isHeader = b;
127         }
128 
129 //        public void setDivider(boolean b) {
130 //            divider = b;
131 //        }
132     }
133 
addColumn(String header, String headerAttributes, String cellPattern, String cellAttributes, boolean spanRows)134     public TablePrinter addColumn(String header, String headerAttributes, String cellPattern, String cellAttributes,
135         boolean spanRows) {
136         columns.add(new Column(header).setHeaderAttributes(headerAttributes).setCellPattern(cellPattern)
137             .setCellAttributes(cellAttributes).setSpanRows(spanRows));
138         setSortAscending(true);
139         return this;
140     }
141 
addColumn(String header)142     public TablePrinter addColumn(String header) {
143         columns.add(new Column(header));
144         setSortAscending(true);
145         return this;
146     }
147 
addRow(Comparable<Object>[] data)148     public TablePrinter addRow(Comparable<Object>[] data) {
149         if (data.length != columns.size()) {
150             throw new IllegalArgumentException(String.format("Data size (%d) != column count (%d)", data.length,
151                 columns.size()));
152         }
153         // make sure we can compare; get exception early
154         if (rows.size() > 0) {
155             Comparable<Object>[] data2 = rows.get(0);
156             for (int i = 0; i < data.length; ++i) {
157                 try {
158                     data[i].compareTo(data2[i]);
159                 } catch (RuntimeException e) {
160                     throw new IllegalArgumentException("Can't compare column " + i + ", " + data[i] + ", " + data2[i]);
161                 }
162             }
163         }
164         rows.add(data);
165         return this;
166     }
167 
168     Collection<Comparable<Object>> partialRow;
169 
addRow()170     public TablePrinter addRow() {
171         if (partialRow != null) {
172             throw new IllegalArgumentException("Cannot add partial row before calling finishRow()");
173         }
174         partialRow = new ArrayList<>();
175         return this;
176     }
177 
178     @SuppressWarnings({ "rawtypes", "unchecked" })
addCell(Comparable cell)179     public TablePrinter addCell(Comparable cell) {
180         if (rows.size() > 0) {
181             int i = partialRow.size();
182             Comparable cell0 = rows.get(0)[i];
183             try {
184                 cell.compareTo(cell0);
185             } catch (RuntimeException e) {
186                 throw new IllegalArgumentException("Can't compare column " + i + ", " + cell + ", " + cell0);
187             }
188 
189         }
190         partialRow.add(cell);
191         return this;
192     }
193 
finishRow()194     public TablePrinter finishRow() {
195         if (partialRow.size() != columns.size()) {
196             throw new IllegalArgumentException("Items in row (" + partialRow.size()
197                 + " not same as number of columns" + columns.size());
198         }
199         addRow(partialRow);
200         partialRow = null;
201         return this;
202     }
203 
204     @SuppressWarnings("unchecked")
addRow(Collection<Comparable<Object>> data)205     public TablePrinter addRow(Collection<Comparable<Object>> data) {
206         addRow(data.toArray(new Comparable[data.size()]));
207         return this;
208     }
209 
210     @SuppressWarnings({ "rawtypes", "unchecked" })
addRows(Collection data)211     public TablePrinter addRows(Collection data) {
212         for (Object row : data) {
213             if (row instanceof Collection) {
214                 addRow((Collection) row);
215             } else {
216                 addRow((Comparable[]) row);
217             }
218         }
219         return this;
220     }
221 
222     @SuppressWarnings({ "rawtypes", "unchecked" })
addRows(Comparable[][] data)223     public TablePrinter addRows(Comparable[][] data) {
224         for (Comparable[] row : data) {
225             addRow(row);
226         }
227         return this;
228     }
229 
230     @Override
toString()231     public String toString() {
232         return toTable();
233     }
234 
toTsv(PrintWriter tsvFile)235     public void toTsv(PrintWriter tsvFile) {
236         Comparable[][] sortedFlat = (rows.toArray(new Comparable[rows.size()][]));
237         toTsvInternal(sortedFlat, tsvFile);
238     }
239 
240     @SuppressWarnings("rawtypes")
toTable()241     public String toTable() {
242         Comparable[][] sortedFlat = (rows.toArray(new Comparable[rows.size()][]));
243         return toTableInternal(sortedFlat);
244     }
245 
246     @SuppressWarnings("rawtypes")
247     static class ColumnSorter<T extends Comparable> implements Comparator<T[]> {
248         private int[] sortPriorities = new int[0];
249         private BitSet ascending = new BitSet();
250         Collator englishCollator = Collator.getInstance(ULocale.ENGLISH);
251 
252         @Override
253         @SuppressWarnings("unchecked")
compare(T[] o1, T[] o2)254         public int compare(T[] o1, T[] o2) {
255             int result;
256             for (int curr : sortPriorities) {
257                 result = o1[curr] instanceof String ? englishCollator.compare((String) o1[curr], (String) o2[curr])
258                     : o1[curr].compareTo(o2[curr]);
259                 if (0 != result) {
260                     if (ascending.get(curr)) {
261                         return result;
262                     }
263                     return -result;
264                 }
265             }
266             return 0;
267         }
268 
setSortPriority(int column, int priority)269         public void setSortPriority(int column, int priority) {
270             if (sortPriorities.length <= priority) {
271                 int[] temp = new int[priority + 1];
272                 System.arraycopy(sortPriorities, 0, temp, 0, sortPriorities.length);
273                 sortPriorities = temp;
274             }
275             sortPriorities[priority] = column;
276         }
277 
getSortPriorities()278         public int[] getSortPriorities() {
279             return sortPriorities;
280         }
281 
getSortAscending(int bitIndex)282         public boolean getSortAscending(int bitIndex) {
283             return ascending.get(bitIndex);
284         }
285 
setSortAscending(int bitIndex, boolean value)286         public void setSortAscending(int bitIndex, boolean value) {
287             ascending.set(bitIndex, value);
288         }
289     }
290 
291     @SuppressWarnings("rawtypes")
292     ColumnSorter<Comparable> columnSorter = new ColumnSorter<>();
293     private boolean sort;
294 
toTsvInternal(@uppressWarnings"rawtypes") Comparable[][] sortedFlat, PrintWriter tsvFile)295     public void toTsvInternal(@SuppressWarnings("rawtypes") Comparable[][] sortedFlat, PrintWriter tsvFile) {
296         Object[] patternArgs = new Object[columns.size() + 1];
297         if (sort) {
298             Arrays.sort(sortedFlat, columnSorter);
299         }
300         columnsFlat = columns.toArray(new Column[0]);
301         for (int i = 0; i < sortedFlat.length; ++i) {
302             System.arraycopy(sortedFlat[i], 0, patternArgs, 1, sortedFlat[i].length);
303 
304             String sep = "";
305             for (int j = 0; j < sortedFlat[i].length; ++j) {
306                 if (columnsFlat[j].hidden) {
307                     continue;
308                 }
309                 patternArgs[0] = sortedFlat[i][j];
310 
311                 if (false && columnsFlat[j].cellPattern != null) {
312                     try {
313                         patternArgs[0] = sortedFlat[i][j];
314                         System.arraycopy(sortedFlat[i], 0, patternArgs, 1, sortedFlat[i].length);
315                         tsvFile.append(sep).append(format(columnsFlat[j].cellPattern.format(patternArgs)).replace("<br>", " "));
316                     } catch (RuntimeException e) {
317                         throw (RuntimeException) new IllegalArgumentException("cellPattern<" + i + ", " + j + "> = "
318                             + sortedFlat[i][j]).initCause(e);
319                     }
320                 } else {
321                     tsvFile.append(sep).append(format(sortedFlat[i][j]).replace("<br>", " "));
322                 }
323                 sep = "\t";
324             }
325             tsvFile.println();
326         }
327 
328     }
329 
330     @SuppressWarnings("rawtypes")
toTableInternal(Comparable[][] sortedFlat)331     public String toTableInternal(Comparable[][] sortedFlat) {
332         // TreeSet<String[]> sorted = new TreeSet();
333         // sorted.addAll(data);
334         Object[] patternArgs = new Object[columns.size() + 1];
335 
336         if (sort) {
337             Arrays.sort(sortedFlat, columnSorter);
338         }
339 
340         columnsFlat = columns.toArray(new Column[0]);
341 
342         StringBuilder result = new StringBuilder();
343 
344         result.append("<table");
345         if (tableAttributes != null) {
346             result.append(' ').append(tableAttributes);
347         }
348         result.append(">" + System.lineSeparator());
349 
350         if (caption != null) {
351             result.append("<caption>").append(caption).append("</caption>");
352         }
353 
354         showHeader(result);
355         int visibleWidth = 0;
356         for (int j = 0; j < columns.size(); ++j) {
357             if (!columnsFlat[j].hidden) {
358                 ++visibleWidth;
359             }
360         }
361 
362         for (int i = 0; i < sortedFlat.length; ++i) {
363             System.arraycopy(sortedFlat[i], 0, patternArgs, 1, sortedFlat[i].length);
364             // check to see if we repeat the header
365             if (i != 0) {
366                 boolean divider = false;
367                 for (int j = 0; j < sortedFlat[i].length; ++j) {
368                     final Column column = columns.get(j);
369                     if (column.repeatHeader && !sortedFlat[i - 1][j].equals(sortedFlat[i][j])) {
370                         showHeader(result);
371                         break;
372 //                    } else if (column.divider && !sortedFlat[i - 1][j].equals(sortedFlat[i][j])) {
373 //                        divider = true;
374                     }
375                 }
376                 if (divider) {
377                     result.append("\t<tr><td class='divider' colspan='" + visibleWidth + "'></td></tr>");
378                 }
379             }
380             result.append("\t<tr>");
381             for (int j = 0; j < sortedFlat[i].length; ++j) {
382                 int identical = findIdentical(sortedFlat, i, j);
383                 if (identical == 0) continue;
384                 if (columnsFlat[j].hidden) {
385                     continue;
386                 }
387                 patternArgs[0] = sortedFlat[i][j];
388                 result.append(columnsFlat[j].isHeader ? "<th" : "<td");
389                 if (columnsFlat[j].cellAttributes != null) {
390                     try {
391                         result.append(' ').append(columnsFlat[j].cellAttributes.format(patternArgs));
392                     } catch (RuntimeException e) {
393                         throw (RuntimeException) new IllegalArgumentException("cellAttributes<" + i + ", " + j + "> = "
394                             + sortedFlat[i][j]).initCause(e);
395                     }
396                 }
397                 if (identical != 1) {
398                     result.append(" rowSpan='").append(identical).append('\'');
399                 }
400                 result.append('>');
401 
402                 if (columnsFlat[j].cellPattern != null) {
403                     try {
404                         patternArgs[0] = sortedFlat[i][j];
405                         System.arraycopy(sortedFlat[i], 0, patternArgs, 1, sortedFlat[i].length);
406                         result.append(format(columnsFlat[j].cellPattern.format(patternArgs)));
407                     } catch (RuntimeException e) {
408                         throw (RuntimeException) new IllegalArgumentException("cellPattern<" + i + ", " + j + "> = "
409                             + sortedFlat[i][j]).initCause(e);
410                     }
411                 } else {
412                     result.append(format(sortedFlat[i][j]));
413                 }
414                 result.append(columnsFlat[j].isHeader ? "</th>" : "</td>");
415             }
416             result.append("</tr>" + System.lineSeparator());
417         }
418         result.append("</table>");
419         return result.toString();
420     }
421 
422     static final UnicodeSet BIDI = new UnicodeSet("[[:bc=R:][:bc=AL:]]");
423     static final char RLE = '\u202B';
424     static final char PDF = '\u202C';
425 
426     @SuppressWarnings("rawtypes")
format(Comparable comparable)427     private String format(Comparable comparable) {
428         if (comparable == null) {
429             return null;
430         }
431         String s = comparable.toString().replace("\n", "<br>");
432         return BIDI.containsNone(s) ? s : RLE + s + PDF;
433     }
434 
showHeader(StringBuilder result)435     private void showHeader(StringBuilder result) {
436         result.append("\t<tr>");
437         for (int j = 0; j < columnsFlat.length; ++j) {
438             if (columnsFlat[j].hidden) {
439                 continue;
440             }
441             result.append("<th");
442             if (columnsFlat[j].headerAttributes != null) {
443                 result.append(' ').append(columnsFlat[j].headerAttributes);
444             }
445             result.append('>').append(columnsFlat[j].header).append("</th>");
446 
447         }
448         result.append("</tr>" + System.lineSeparator());
449     }
450 
451     /**
452      * Return 0 if the item is the same as in the row above, otherwise the rowSpan (of equal items)
453      *
454      * @param sortedFlat
455      * @param rowIndex
456      * @param colIndex
457      * @return
458      */
459     @SuppressWarnings("rawtypes")
findIdentical(Comparable[][] sortedFlat, int rowIndex, int colIndex)460     private int findIdentical(Comparable[][] sortedFlat, int rowIndex, int colIndex) {
461         if (!columnsFlat[colIndex].spanRows) return 1;
462         Comparable item = sortedFlat[rowIndex][colIndex];
463         if (rowIndex > 0 && item.equals(sortedFlat[rowIndex - 1][colIndex])) {
464             if (!breakSpans(sortedFlat, rowIndex, colIndex)) {
465                 return 0;
466             }
467         }
468         for (int k = rowIndex + 1; k < sortedFlat.length; ++k) {
469             if (!item.equals(sortedFlat[k][colIndex])
470                 || breakSpans(sortedFlat, k, colIndex)) {
471                 return k - rowIndex;
472             }
473         }
474         return sortedFlat.length - rowIndex;
475     }
476 
477     // to-do: prevent overlap when it would cause information to be lost.
478     private BitSet breaksSpans = new BitSet();
479 
480     /**
481      * Only called with rowIndex > 0
482      *
483      * @param rowIndex
484      * @param colIndex2
485      * @return
486      */
487     @SuppressWarnings({ "rawtypes", "unchecked" })
breakSpans(Comparable[][] sortedFlat, int rowIndex, int colIndex2)488     private boolean breakSpans(Comparable[][] sortedFlat, int rowIndex, int colIndex2) {
489         final int limit = Math.min(breaksSpans.length(), colIndex2);
490         for (int colIndex = 0; colIndex < limit; ++colIndex) {
491             if (breaksSpans.get(colIndex)
492                 && sortedFlat[rowIndex][colIndex].compareTo(sortedFlat[rowIndex - 1][colIndex]) != 0) {
493                 return true;
494             }
495         }
496         return false;
497     }
498 
setCellAttributes(String cellAttributes)499     public TablePrinter setCellAttributes(String cellAttributes) {
500         columns.get(columns.size() - 1).setCellAttributes(cellAttributes);
501         return this;
502     }
503 
setCellPattern(String cellPattern)504     public TablePrinter setCellPattern(String cellPattern) {
505         columns.get(columns.size() - 1).setCellPattern(cellPattern);
506         return this;
507     }
508 
setHeaderAttributes(String headerAttributes)509     public TablePrinter setHeaderAttributes(String headerAttributes) {
510         columns.get(columns.size() - 1).setHeaderAttributes(headerAttributes);
511         return this;
512     }
513 
setSpanRows(boolean spanRows)514     public TablePrinter setSpanRows(boolean spanRows) {
515         columns.get(columns.size() - 1).setSpanRows(spanRows);
516         return this;
517     }
518 
setRepeatHeader(boolean b)519     public TablePrinter setRepeatHeader(boolean b) {
520         columns.get(columns.size() - 1).setRepeatHeader(b);
521         if (b) {
522             breaksSpans.set(columns.size() - 1, true);
523         }
524         return this;
525     }
526 
527     /**
528      * In the style section, have something like:
529      * <style>
530      * <!--
531      * .redbar { border-style: solid; border-width: 1px; padding: 0; background-color:red; border-collapse: collapse}
532      * -->
533      * </style>
534      *
535      * @param color
536      * @return
537      */
bar(String htmlClass, double value, double max, boolean log)538     public static String bar(String htmlClass, double value, double max, boolean log) {
539         double width = 100 * (log ? Math.log(value) / Math.log(max) : value / max);
540         if (!(width >= 0.5)) return ""; // do the comparison this way to catch NaN
541         return "<table class='" + htmlClass + "' width='" + width + "%'><tr><td>\u200B</td></tr></table>";
542     }
543 
setHidden(boolean b)544     public TablePrinter setHidden(boolean b) {
545         columns.get(columns.size() - 1).setHidden(b);
546         return this;
547     }
548 
setHeaderCell(boolean b)549     public TablePrinter setHeaderCell(boolean b) {
550         columns.get(columns.size() - 1).setHeaderCell(b);
551         return this;
552     }
553 
554 //    public TablePrinter setRepeatDivider(boolean b) {
555 //        //columns.get(columns.size() - 1).setDivider(b);
556 //        return this;
557 //    }
558 
clearRows()559     public void clearRows() {
560         rows.clear();
561     }
562 }