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