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