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