Home | History | Annotate | Download | only in tool
      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 
     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 
     50     public String getTableAttributes() {
     51         return tableAttributes;
     52     }
     53 
     54     public TablePrinter setTableAttributes(String tableAttributes) {
     55         this.tableAttributes = tableAttributes;
     56         return this;
     57     }
     58 
     59     public TablePrinter setCaption(String caption) {
     60         this.caption = caption;
     61         return this;
     62     }
     63 
     64     public TablePrinter setSortPriority(int priority) {
     65         columnSorter.setSortPriority(columns.size() - 1, priority);
     66         sort = true;
     67         return this;
     68     }
     69 
     70     public TablePrinter setSortAscending(boolean ascending) {
     71         columnSorter.setSortAscending(columns.size() - 1, ascending);
     72         return this;
     73     }
     74 
     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 
     92         public Column(String header) {
     93             this.header = header;
     94         }
     95 
     96         public Column setCellAttributes(String cellAttributes) {
     97             this.cellAttributes = new MessageFormat(MessageFormat.autoQuoteApostrophe(cellAttributes), ULocale.ENGLISH);
     98             return this;
     99         }
    100 
    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 
    107         public Column setHeaderAttributes(String headerAttributes) {
    108             this.headerAttributes = headerAttributes;
    109             return this;
    110         }
    111 
    112         public Column setSpanRows(boolean spanRows) {
    113             this.spanRows = spanRows;
    114             return this;
    115         }
    116 
    117         public void setRepeatHeader(boolean b) {
    118             repeatHeader = b;
    119         }
    120 
    121         public void setHidden(boolean b) {
    122             hidden = b;
    123         }
    124 
    125         public void setHeaderCell(boolean b) {
    126             isHeader = b;
    127         }
    128 
    129 //        public void setDivider(boolean b) {
    130 //            divider = b;
    131 //        }
    132     }
    133 
    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 
    142     public TablePrinter addColumn(String header) {
    143         columns.add(new Column(header));
    144         setSortAscending(true);
    145         return this;
    146     }
    147 
    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 
    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" })
    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 
    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")
    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" })
    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" })
    223     public TablePrinter addRows(Comparable[][] data) {
    224         for (Comparable[] row : data) {
    225             addRow(row);
    226         }
    227         return this;
    228     }
    229 
    230     public String toString() {
    231         return toTable();
    232     }
    233 
    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")
    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")
    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 
    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 
    276         public int[] getSortPriorities() {
    277             return sortPriorities;
    278         }
    279 
    280         public boolean getSortAscending(int bitIndex) {
    281             return ascending.get(bitIndex);
    282         }
    283 
    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 
    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")
    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")
    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 
    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")
    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" })
    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 
    497     public TablePrinter setCellAttributes(String cellAttributes) {
    498         columns.get(columns.size() - 1).setCellAttributes(cellAttributes);
    499         return this;
    500     }
    501 
    502     public TablePrinter setCellPattern(String cellPattern) {
    503         columns.get(columns.size() - 1).setCellPattern(cellPattern);
    504         return this;
    505     }
    506 
    507     public TablePrinter setHeaderAttributes(String headerAttributes) {
    508         columns.get(columns.size() - 1).setHeaderAttributes(headerAttributes);
    509         return this;
    510     }
    511 
    512     public TablePrinter setSpanRows(boolean spanRows) {
    513         columns.get(columns.size() - 1).setSpanRows(spanRows);
    514         return this;
    515     }
    516 
    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      */
    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 
    542     public TablePrinter setHidden(boolean b) {
    543         columns.get(columns.size() - 1).setHidden(b);
    544         return this;
    545     }
    546 
    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 
    557     public void clearRows() {
    558         rows.clear();
    559     }
    560 }