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