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