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<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 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<Comparable<Object>>(); 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 toString()230 public String toString() { 231 return toTable(); 232 } 233 toTsv(PrintWriter tsvFile)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") toTable()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") compare(T[] o1, T[] o2)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 setSortPriority(int column, int priority)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 getSortPriorities()276 public int[] getSortPriorities() { 277 return sortPriorities; 278 } 279 getSortAscending(int bitIndex)280 public boolean getSortAscending(int bitIndex) { 281 return ascending.get(bitIndex); 282 } 283 setSortAscending(int bitIndex, boolean value)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 toTsvInternal(@uppressWarnings"rawtypes") Comparable[][] sortedFlat, PrintWriter tsvFile)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") toTableInternal(Comparable[][] sortedFlat)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") format(Comparable comparable)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 showHeader(StringBuilder result)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") findIdentical(Comparable[][] sortedFlat, int rowIndex, int colIndex)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" }) breakSpans(Comparable[][] sortedFlat, int rowIndex, int colIndex2)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 setCellAttributes(String cellAttributes)497 public TablePrinter setCellAttributes(String cellAttributes) { 498 columns.get(columns.size() - 1).setCellAttributes(cellAttributes); 499 return this; 500 } 501 setCellPattern(String cellPattern)502 public TablePrinter setCellPattern(String cellPattern) { 503 columns.get(columns.size() - 1).setCellPattern(cellPattern); 504 return this; 505 } 506 setHeaderAttributes(String headerAttributes)507 public TablePrinter setHeaderAttributes(String headerAttributes) { 508 columns.get(columns.size() - 1).setHeaderAttributes(headerAttributes); 509 return this; 510 } 511 setSpanRows(boolean spanRows)512 public TablePrinter setSpanRows(boolean spanRows) { 513 columns.get(columns.size() - 1).setSpanRows(spanRows); 514 return this; 515 } 516 setRepeatHeader(boolean b)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 */ bar(String htmlClass, double value, double max, boolean log)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 setHidden(boolean b)542 public TablePrinter setHidden(boolean b) { 543 columns.get(columns.size() - 1).setHidden(b); 544 return this; 545 } 546 setHeaderCell(boolean b)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 clearRows()557 public void clearRows() { 558 rows.clear(); 559 } 560 }