• 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 = 0;
256             for (int curr : sortPriorities) {
257                 final T c1 = o1[curr];
258                 final T c2 = o2[curr];
259                 result = c1 instanceof String
260                     ? englishCollator.compare((String) c1, (String) c2)
261                     : c1.compareTo(c2);
262                 if (0 != result) {
263                     if (ascending.get(curr)) {
264                         return result;
265                     }
266                     return -result;
267                 }
268             }
269             return 0;
270         }
271 
setSortPriority(int column, int priority)272         public void setSortPriority(int column, int priority) {
273             if (sortPriorities.length <= priority) {
274                 int[] temp = new int[priority + 1];
275                 System.arraycopy(sortPriorities, 0, temp, 0, sortPriorities.length);
276                 sortPriorities = temp;
277             }
278             sortPriorities[priority] = column;
279         }
280 
getSortPriorities()281         public int[] getSortPriorities() {
282             return sortPriorities;
283         }
284 
getSortAscending(int bitIndex)285         public boolean getSortAscending(int bitIndex) {
286             return ascending.get(bitIndex);
287         }
288 
setSortAscending(int bitIndex, boolean value)289         public void setSortAscending(int bitIndex, boolean value) {
290             ascending.set(bitIndex, value);
291         }
292     }
293 
294     @SuppressWarnings("rawtypes")
295     ColumnSorter<Comparable> columnSorter = new ColumnSorter<>();
296     private boolean sort;
297 
toTsvInternal(@uppressWarnings"rawtypes") Comparable[][] sortedFlat, PrintWriter tsvFile)298     public void toTsvInternal(@SuppressWarnings("rawtypes") Comparable[][] sortedFlat, PrintWriter tsvFile) {
299         String sep0 = "#";
300         for (Column column : columns) {
301             if (column.hidden) {
302                 continue;
303             }
304             tsvFile.print(sep0);
305             tsvFile.print(column.header);
306             sep0 = "\t";
307         }
308         tsvFile.println();
309 
310         Object[] patternArgs = new Object[columns.size() + 1];
311         if (sort) {
312             Arrays.sort(sortedFlat, columnSorter);
313         }
314         columnsFlat = columns.toArray(new Column[0]);
315         for (int i = 0; i < sortedFlat.length; ++i) {
316             System.arraycopy(sortedFlat[i], 0, patternArgs, 1, sortedFlat[i].length);
317 
318             String sep = "";
319             for (int j = 0; j < sortedFlat[i].length; ++j) {
320                 if (columnsFlat[j].hidden) {
321                     continue;
322                 }
323                 final Comparable value = sortedFlat[i][j];
324                 patternArgs[0] = value;
325 
326 //                if (false && columnsFlat[j].cellPattern != null) {
327 //                    try {
328 //                        patternArgs[0] = value;
329 //                        System.arraycopy(sortedFlat[i], 0, patternArgs, 1, sortedFlat[i].length);
330 //                        tsvFile.append(sep).append(format(columnsFlat[j].cellPattern.format(patternArgs)).replace("<br>", " "));
331 //                    } catch (RuntimeException e) {
332 //                        throw (RuntimeException) new IllegalArgumentException("cellPattern<" + i + ", " + j + "> = "
333 //                            + value).initCause(e);
334 //                    }
335 //                } else
336                 {
337                     tsvFile.append(sep).append(tsvFormat(value));
338                 }
339                 sep = "\t";
340             }
341             tsvFile.println();
342         }
343 
344     }
345 
tsvFormat(Comparable value)346     private String tsvFormat(Comparable value) {
347         if (value == null) {
348             return "n/a";
349         }
350         if (value instanceof Number) {
351             int debug = 0;
352         }
353         String s = value.toString().replace("\n", " • ");
354         return BIDI.containsNone(s) ? s : RLE + s + PDF;
355     }
356 
357     @SuppressWarnings("rawtypes")
toTableInternal(Comparable[][] sortedFlat)358     public String toTableInternal(Comparable[][] sortedFlat) {
359         // TreeSet<String[]> sorted = new TreeSet();
360         // sorted.addAll(data);
361         Object[] patternArgs = new Object[columns.size() + 1];
362 
363         if (sort) {
364             Arrays.sort(sortedFlat, columnSorter);
365         }
366 
367         columnsFlat = columns.toArray(new Column[0]);
368 
369         StringBuilder result = new StringBuilder();
370 
371         result.append("<table");
372         if (tableAttributes != null) {
373             result.append(' ').append(tableAttributes);
374         }
375         result.append(">" + System.lineSeparator());
376 
377         if (caption != null) {
378             result.append("<caption>").append(caption).append("</caption>");
379         }
380 
381         showHeader(result);
382         int visibleWidth = 0;
383         for (int j = 0; j < columns.size(); ++j) {
384             if (!columnsFlat[j].hidden) {
385                 ++visibleWidth;
386             }
387         }
388 
389         for (int i = 0; i < sortedFlat.length; ++i) {
390             System.arraycopy(sortedFlat[i], 0, patternArgs, 1, sortedFlat[i].length);
391             // check to see if we repeat the header
392             if (i != 0) {
393                 boolean divider = false;
394                 for (int j = 0; j < sortedFlat[i].length; ++j) {
395                     final Column column = columns.get(j);
396                     if (column.repeatHeader && !sortedFlat[i - 1][j].equals(sortedFlat[i][j])) {
397                         showHeader(result);
398                         break;
399 //                    } else if (column.divider && !sortedFlat[i - 1][j].equals(sortedFlat[i][j])) {
400 //                        divider = true;
401                     }
402                 }
403                 if (divider) {
404                     result.append("\t<tr><td class='divider' colspan='" + visibleWidth + "'></td></tr>");
405                 }
406             }
407             result.append("\t<tr>");
408             for (int j = 0; j < sortedFlat[i].length; ++j) {
409                 int identical = findIdentical(sortedFlat, i, j);
410                 if (identical == 0) continue;
411                 if (columnsFlat[j].hidden) {
412                     continue;
413                 }
414                 patternArgs[0] = sortedFlat[i][j];
415                 result.append(columnsFlat[j].isHeader ? "<th" : "<td");
416                 if (columnsFlat[j].cellAttributes != null) {
417                     try {
418                         result.append(' ').append(columnsFlat[j].cellAttributes.format(patternArgs));
419                     } catch (RuntimeException e) {
420                         throw (RuntimeException) new IllegalArgumentException("cellAttributes<" + i + ", " + j + "> = "
421                             + sortedFlat[i][j]).initCause(e);
422                     }
423                 }
424                 if (identical != 1) {
425                     result.append(" rowSpan='").append(identical).append('\'');
426                 }
427                 result.append('>');
428 
429                 if (columnsFlat[j].cellPattern != null) {
430                     try {
431                         patternArgs[0] = sortedFlat[i][j];
432                         System.arraycopy(sortedFlat[i], 0, patternArgs, 1, sortedFlat[i].length);
433                         result.append(format(columnsFlat[j].cellPattern.format(patternArgs)));
434                     } catch (RuntimeException e) {
435                         throw (RuntimeException) new IllegalArgumentException("cellPattern<" + i + ", " + j + "> = "
436                             + sortedFlat[i][j]).initCause(e);
437                     }
438                 } else {
439                     result.append(format(sortedFlat[i][j]));
440                 }
441                 result.append(columnsFlat[j].isHeader ? "</th>" : "</td>");
442             }
443             result.append("</tr>" + System.lineSeparator());
444         }
445         result.append("</table>");
446         return result.toString();
447     }
448 
449     static final UnicodeSet BIDI = new UnicodeSet("[[:bc=R:][:bc=AL:]]");
450     static final char RLE = '\u202B';
451     static final char PDF = '\u202C';
452 
453     @SuppressWarnings("rawtypes")
format(Comparable comparable)454     private String format(Comparable comparable) {
455         if (comparable == null) {
456             return null;
457         }
458         String s = comparable.toString().replace("\n", "<br>");
459         return BIDI.containsNone(s) ? s : RLE + s + PDF;
460     }
461 
showHeader(StringBuilder result)462     private void showHeader(StringBuilder result) {
463         result.append("\t<tr>");
464         for (int j = 0; j < columnsFlat.length; ++j) {
465             if (columnsFlat[j].hidden) {
466                 continue;
467             }
468             result.append("<th");
469             if (columnsFlat[j].headerAttributes != null) {
470                 result.append(' ').append(columnsFlat[j].headerAttributes);
471             }
472             result.append('>').append(columnsFlat[j].header).append("</th>");
473 
474         }
475         result.append("</tr>" + System.lineSeparator());
476     }
477 
478     /**
479      * Return 0 if the item is the same as in the row above, otherwise the rowSpan (of equal items)
480      *
481      * @param sortedFlat
482      * @param rowIndex
483      * @param colIndex
484      * @return
485      */
486     @SuppressWarnings("rawtypes")
findIdentical(Comparable[][] sortedFlat, int rowIndex, int colIndex)487     private int findIdentical(Comparable[][] sortedFlat, int rowIndex, int colIndex) {
488         if (!columnsFlat[colIndex].spanRows) return 1;
489         Comparable item = sortedFlat[rowIndex][colIndex];
490         if (rowIndex > 0 && item.equals(sortedFlat[rowIndex - 1][colIndex])) {
491             if (!breakSpans(sortedFlat, rowIndex, colIndex)) {
492                 return 0;
493             }
494         }
495         for (int k = rowIndex + 1; k < sortedFlat.length; ++k) {
496             if (!item.equals(sortedFlat[k][colIndex])
497                 || breakSpans(sortedFlat, k, colIndex)) {
498                 return k - rowIndex;
499             }
500         }
501         return sortedFlat.length - rowIndex;
502     }
503 
504     // to-do: prevent overlap when it would cause information to be lost.
505     private BitSet breaksSpans = new BitSet();
506 
507     /**
508      * Only called with rowIndex > 0
509      *
510      * @param rowIndex
511      * @param colIndex2
512      * @return
513      */
514     @SuppressWarnings({ "rawtypes", "unchecked" })
breakSpans(Comparable[][] sortedFlat, int rowIndex, int colIndex2)515     private boolean breakSpans(Comparable[][] sortedFlat, int rowIndex, int colIndex2) {
516         final int limit = Math.min(breaksSpans.length(), colIndex2);
517         for (int colIndex = 0; colIndex < limit; ++colIndex) {
518             if (breaksSpans.get(colIndex)
519                 && sortedFlat[rowIndex][colIndex].compareTo(sortedFlat[rowIndex - 1][colIndex]) != 0) {
520                 return true;
521             }
522         }
523         return false;
524     }
525 
setCellAttributes(String cellAttributes)526     public TablePrinter setCellAttributes(String cellAttributes) {
527         columns.get(columns.size() - 1).setCellAttributes(cellAttributes);
528         return this;
529     }
530 
setCellPattern(String cellPattern)531     public TablePrinter setCellPattern(String cellPattern) {
532         columns.get(columns.size() - 1).setCellPattern(cellPattern);
533         return this;
534     }
535 
setHeaderAttributes(String headerAttributes)536     public TablePrinter setHeaderAttributes(String headerAttributes) {
537         columns.get(columns.size() - 1).setHeaderAttributes(headerAttributes);
538         return this;
539     }
540 
setSpanRows(boolean spanRows)541     public TablePrinter setSpanRows(boolean spanRows) {
542         columns.get(columns.size() - 1).setSpanRows(spanRows);
543         return this;
544     }
545 
setRepeatHeader(boolean b)546     public TablePrinter setRepeatHeader(boolean b) {
547         columns.get(columns.size() - 1).setRepeatHeader(b);
548         if (b) {
549             breaksSpans.set(columns.size() - 1, true);
550         }
551         return this;
552     }
553 
554     /**
555      * In the style section, have something like:
556      * <style>
557      * <!--
558      * .redbar { border-style: solid; border-width: 1px; padding: 0; background-color:red; border-collapse: collapse}
559      * -->
560      * </style>
561      *
562      * @param color
563      * @return
564      */
bar(String htmlClass, double value, double max, boolean log)565     public static String bar(String htmlClass, double value, double max, boolean log) {
566         double width = 100 * (log ? Math.log(value) / Math.log(max) : value / max);
567         if (!(width >= 0.5)) return ""; // do the comparison this way to catch NaN
568         return "<table class='" + htmlClass + "' width='" + width + "%'><tr><td>\u200B</td></tr></table>";
569     }
570 
setHidden(boolean b)571     public TablePrinter setHidden(boolean b) {
572         columns.get(columns.size() - 1).setHidden(b);
573         return this;
574     }
575 
setHeaderCell(boolean b)576     public TablePrinter setHeaderCell(boolean b) {
577         columns.get(columns.size() - 1).setHeaderCell(b);
578         return this;
579     }
580 
581 //    public TablePrinter setRepeatDivider(boolean b) {
582 //        //columns.get(columns.size() - 1).setDivider(b);
583 //        return this;
584 //    }
585 
clearRows()586     public void clearRows() {
587         rows.clear();
588     }
589 }