package water.util; import water.AutoBuffer; import water.Iced; import water.IcedWrapper; import java.util.Arrays; /** * Serializable 2D Table containing Strings or doubles * Table can be named * Columns and Rows can be named * Fields can be empty */ public class TwoDimTable extends Iced { private String tableHeader; private String tableDescription; private String[] rowHeaders; private String[] colHeaders; private String[] colTypes; private String[] colFormats; private IcedWrapper[][] cellValues; private String colHeaderForRowHeaders; //public static final double emptyDouble = Double.longBitsToDouble(0x7ff8000000000100L); //also a NaN, but not Double.NaN (0x7ff8000000000000) public static final double emptyDouble = Double.MIN_VALUE*2; //Some unlikely value /** * Check whether a double value is considered an "empty field". * @param d a double value * @return true iff d represents an empty field */ public static boolean isEmpty(final double d) { return Double.doubleToRawLongBits(d) == Double.doubleToRawLongBits(emptyDouble); } /** * Constructor for TwoDimTable (R rows, C columns) * @param tableHeader the table header * @param tableDescription the table description * @param rowHeaders R-dim array for row headers * @param colHeaders C-dim array for column headers * @param colTypes C-dim array for column types * @param colFormats C-dim array with printf format strings for each column * @param colHeaderForRowHeaders column header for row headers */ public TwoDimTable(String tableHeader, String tableDescription, String[] rowHeaders, String[] colHeaders, String[] colTypes, String[] colFormats, String colHeaderForRowHeaders) { if (tableHeader == null) tableHeader = ""; if (tableDescription == null) tableDescription = ""; this.colHeaderForRowHeaders = colHeaderForRowHeaders; if (rowHeaders == null) throw new IllegalArgumentException("rowHeaders is null"); else { for (int r = 0; r < rowHeaders.length; ++r) if (rowHeaders[r] == null) rowHeaders[r] = ""; } if (colHeaders == null) throw new IllegalArgumentException("colHeaders is null"); else { for (int c = 0; c < colHeaders.length; ++c) if (colHeaders[c] == null) colHeaders[c] = ""; } final int rowDim = rowHeaders.length; final int colDim = colHeaders.length; if (colTypes == null) { colTypes = new String[colDim]; Arrays.fill(colTypes, "string"); } else if (colTypes.length != colDim) throw new IllegalArgumentException("colTypes must have the same length as colHeaders"); else { for (int c = 0; c < colDim; ++c) { colTypes[c] = colTypes[c].toLowerCase(); if (!(colTypes[c].equals("double") || colTypes[c].equals("float") || colTypes[c].equals("int") || colTypes[c].equals("long") || colTypes[c].equals("string"))) throw new IllegalArgumentException("colTypes values must be one of \"double\", \"float\", \"int\", \"long\", or \"string\""); } } if (colFormats == null) { colFormats = new String[colDim]; Arrays.fill(colFormats, "%s"); } else if (colFormats.length != colDim) throw new IllegalArgumentException("colFormats must have the same length as colHeaders"); this.tableHeader = tableHeader; this.tableDescription = tableDescription; this.rowHeaders = rowHeaders; this.colHeaders = colHeaders; this.colTypes = colTypes; this.colFormats = colFormats; this.cellValues = new IcedWrapper[rowDim][colDim]; } /** * Constructor for TwoDimTable (R rows, C columns) * @param tableHeader the table header * @param tableDescription the table description * @param rowHeaders R-dim array for row headers * @param colHeaders C-dim array for column headers * @param colTypes C-dim array for column types * @param colFormats C-dim array with printf format strings for each column * @param colHeaderForRowHeaders column header for row headers * @param strCellValues String[R][C] array for string cell values, can be null (can provide String[R][], for example) * @param dblCellValues double[R][C] array for double cell values, can be empty (marked with emptyDouble - happens when initialized with double[R][]) */ public TwoDimTable(String tableHeader, String tableDescription, String[] rowHeaders, String[] colHeaders, String[] colTypes, String[] colFormats, String colHeaderForRowHeaders, String[][] strCellValues, double[][] dblCellValues) { this(tableHeader, tableDescription, rowHeaders, colHeaders, colTypes, colFormats, colHeaderForRowHeaders); assert (isEmpty(emptyDouble)); assert (!Arrays.equals(new AutoBuffer().put8d(emptyDouble).buf(), new AutoBuffer().put8d(Double.NaN).buf())); final int rowDim = rowHeaders.length; final int colDim = colHeaders.length; for (int c = 0; c < colDim; ++c) { if (colTypes[c].equalsIgnoreCase("string")) { for (String[] vec : strCellValues) { if (vec == null) throw new IllegalArgumentException("Null string in strCellValues"); if (vec.length != colDim) throw new IllegalArgumentException("Each row in strCellValues must have the same length as colHeaders"); } break; } } for (int c = 0; c < colDim; ++c) { if (!colTypes[c].equalsIgnoreCase("string")) { for (double[] vec : dblCellValues) { if (vec.length != colDim) throw new IllegalArgumentException("Each row in dblCellValues must have the same length as colHeaders"); } break; } } for (int r = 0; r < rowDim; ++r) { for (int c = 0; c < colDim; ++c) { if (strCellValues[r] != null && strCellValues[r][c] != null && dblCellValues[r] != null && !isEmpty(dblCellValues[r][c])) throw new IllegalArgumentException("Cannot provide both a String and a Double at row " + r + " and column " + c + "."); } } for (int c = 0; c < colDim; ++c) { switch (colTypes[c]) { case "double": case "float": for (int r = 0; r < rowDim; ++r) set(r, c, dblCellValues[r][c]); break; case "int": case "long": for (int r = 0; r < rowDim; ++r) { double val = dblCellValues[r][c]; if (isEmpty(val)) set(r, c, Double.NaN); else if ((long)val==val) set(r, c, (long)val); else set(r, c, val); } break; case "string": for (int r = 0; r < rowDim; ++r) set(r, c, strCellValues[r][c]); break; default: throw new IllegalArgumentException("Column type " + colTypes[c] + " is not supported."); } } } /** * Accessor for table cells * @param row a row index * @param col a column index * @return Object (either String or Double or Float or Integer or Long) */ public Object get(final int row, final int col) { return cellValues[row][col] == null ? null : cellValues[row][col].get(); } public String getTableHeader() { return tableHeader; } public String getTableDescription() { return tableDescription; } public String[] getRowHeaders() { return rowHeaders; } public String[] getColHeaders() { return colHeaders; } public String getColHeaderForRowHeaders() { return colHeaderForRowHeaders; } public String[] getColTypes() { return colTypes; } public String[] getColFormats() { return colFormats; } public IcedWrapper[][] getCellValues() { return cellValues; } /** * Get row dimension * @return int */ public int getRowDim() { return rowHeaders.length; } /** * Get col dimension * @return int */ public int getColDim() { return colHeaders.length; } /** * Need to change table header when we are calling GLRM from PCA. * * @param newHeader: String containing new table header. */ public void setTableHeader(String newHeader) { if (!StringUtils.isNullOrEmpty(newHeader)) { this.tableHeader = newHeader; } } /** * Setter for table cells * @param row a row index * @param col a column index * @param o Object value */ public void set(final int row, final int col, final Object o) { if (o == null) cellValues[row][col] = new IcedWrapper(null); else if (o instanceof Double && Double.isNaN((double)o)) cellValues[row][col] = new IcedWrapper(Double.NaN); else if (o instanceof int[]) cellValues[row][col] = new IcedWrapper(Arrays.toString((int[])o)); else if (o instanceof long[]) cellValues[row][col] = new IcedWrapper(Arrays.toString((long[])o)); else if (o instanceof float[]) cellValues[row][col] = new IcedWrapper(Arrays.toString((float[])o)); else if (o instanceof double[]) cellValues[row][col] = new IcedWrapper(Arrays.toString((double[])o)); else if (colTypes[col]=="string") cellValues[row][col] = new IcedWrapper(o.toString()); else cellValues[row][col] = new IcedWrapper(o); } /** * Print table to String, using 2 spaces for padding between columns * @return String containing the ASCII version of the table */ public String toString() { return toString(2, true); } /** * Print table to String, using user-given padding * @param pad number of spaces for padding between columns * @return String containing the ASCII version of the table */ public String toString(final int pad) { return toString(pad, true); } private static int PRINTOUT_ROW_LIMIT = 20; private boolean skip(int row) { assert(PRINTOUT_ROW_LIMIT % 2 == 0); if (getRowDim() <= PRINTOUT_ROW_LIMIT) return false; if (row <= PRINTOUT_ROW_LIMIT/2) return false; if (row >= getRowDim()-PRINTOUT_ROW_LIMIT/2) return false; return true; } /** * Print table to String, using user-given padding * @param pad number of spaces for padding between columns * @param full whether to print the full table (otherwise top 5 and bottom 5 rows only) * @return String containing the ASCII version of the table */ public String toString(final int pad, boolean full) { if (pad < 0) throw new IllegalArgumentException("pad must be a non-negative integer"); final int rowDim = getRowDim(); final int colDim = getColDim(); final int actualRowDim = full ? rowDim : Math.min(PRINTOUT_ROW_LIMIT+1, rowDim); final String[][] cellStrings = new String[actualRowDim + 1][colDim + 1]; for (String[] row: cellStrings) Arrays.fill(row, ""); cellStrings[0][0] = colHeaderForRowHeaders != null ? colHeaderForRowHeaders : ""; int row = 0; for (int r = 0; r < rowDim; ++r) { if (!full && skip(r)) continue; cellStrings[row+1][0] = rowHeaders[r]; row++; } for (int c = 0; c < colDim; ++c) cellStrings[0][c+1] = colHeaders[c]; for (int c = 0; c < colDim; ++c) { final String formatString = colFormats[c]; row = 0; for (int r = 0; r < rowDim; ++r) { if (!full && skip(r)) continue; Object o = get(r,c); if ((o == null) || o instanceof Double && isEmpty((double)o)){ cellStrings[row + 1][c + 1] = ""; row++; continue; } else if (o instanceof Double && Double.isNaN((double)o)) { cellStrings[row + 1][c + 1] = "NaN"; row++; continue; } try { if (o instanceof Double) cellStrings[row + 1][c + 1] = String.format(formatString, (Double) o); else if (o instanceof Float) cellStrings[row + 1][c + 1] = String.format(formatString, (Float) o); else if (o instanceof Integer) cellStrings[row + 1][c + 1] = String.format(formatString, (Integer) o); else if (o instanceof Long) cellStrings[row + 1][c + 1] = String.format(formatString, (Long) o); else if (o instanceof String) cellStrings[row + 1][c + 1] = (String)o; else cellStrings[row + 1][c + 1] = String.format(formatString, cellValues[r][c]); } catch(Throwable t) { cellStrings[row + 1][c + 1] = o.toString(); } row++; } } final int[] colLen = new int[colDim + 1]; for (int c = 0; c <= colDim; ++c) { for (int r = 0; r <= actualRowDim; ++r) { colLen[c] = Math.max(colLen[c], cellStrings[r][c].length()); } } final StringBuilder sb = new StringBuilder(); if (tableHeader.length() > 0) { sb.append(tableHeader); } if (tableDescription.length() > 0) { sb.append(" (").append(tableDescription).append(")"); } sb.append(":\n"); for (int r = 0; r <= actualRowDim; ++r) { int len = colLen[0]; if (actualRowDim != rowDim && r - 1 == PRINTOUT_ROW_LIMIT/2) { assert(!full); sb.append("---"); } else { if (len > 0) sb.append(String.format("%" + colLen[0] + "s", cellStrings[r][0])); for (int c = 1; c <= colDim; ++c) { len = colLen[c]; if (len > 0) sb.append(String.format("%" + (len + pad) + "s", cellStrings[r][c].equals("null") ? "" : cellStrings[r][c])); } } sb.append("\n"); } return sb.toString(); } }