diff --git a/android/core/src/processing/core/PApplet.java b/android/core/src/processing/core/PApplet.java index 4edca955b..56a5ad120 100644 --- a/android/core/src/processing/core/PApplet.java +++ b/android/core/src/processing/core/PApplet.java @@ -46,7 +46,6 @@ import android.text.format.Time; import android.util.*; import android.view.*; import android.widget.*; -import android.widget.RelativeLayout; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; @@ -3838,22 +3837,20 @@ public class PApplet extends Activity implements PConstants, Runnable { } + ////////////////////////////////////////////////////////////// - // NODE I/O (XML, JSON, etc.) + // DATA I/O - public XML loadNode(String filename) { + + public XML loadXML(String filename) { return new XML(this, filename); } -// public PData loadData(String filename) { -// if (filename.toLowerCase().endsWith(".json")) { -// return new PData(this, filename); -// } else { -// throw new RuntimeException("filename used for loadNode() must end with XML"); -// } -// } + public Table loadTable(String filename) { + return new Table(this, filename); + } diff --git a/android/core/src/processing/data/Table.java b/android/core/src/processing/data/Table.java new file mode 100644 index 000000000..8318b5a5c --- /dev/null +++ b/android/core/src/processing/data/Table.java @@ -0,0 +1,2632 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2011- Ben Fry and Casey Reas + Copyright (c) 2006-2011 Ben Fry + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License version 2.1 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA + */ + +package processing.data; + +import java.io.*; +import java.lang.reflect.Array; +import java.sql.*; +import java.util.*; + +import processing.core.PApplet; +import processing.core.PConstants; + +// function that will convert awful CSV to TSV.. or something else? +// maybe to write binary instead? then read the binary file once it's ok? + +// if loading from a File object (or PApplet is passed in and we can check online) +// then check the (probable) size of the file before loading + +// implement binary tables + +// no column max/min functions since it needs to be per-datatype +// better to use float mx = max(float(getColumn(3))); +// *** but what to do with null entries? + +// todo: need a method to reset the row/column indices after add/remove +// or just make sure that it's covered for all cases + +// no longer the case, ja? +//

By default, empty rows are skipped and so are lines that start with the +// # character. Using # at the beginning of a line indicates a comment.

+ + +/** + *

Generic class for handling tabular data, typically from a CSV, TSV, or + * other sort of spreadsheet file.

+ *

CSV files are + * comma separated values, + * often with the data in quotes. TSV files use tabs as separators, and usually + * don't bother with the quotes.

+ *

File names should end with .csv if they're comma separated.

+ */ +public class Table implements Iterable { + protected int rowCount; + +// protected boolean skipEmptyRows = true; +// protected boolean skipCommentLines = true; + protected boolean commaSeparatedValues = false; + protected boolean awfulCSV = false; + + protected String missingString = null; + protected int missingInt = 0; + protected long missingLong = 0; + protected float missingFloat = Float.NaN; + protected double missingDouble = Double.NaN; + protected int missingCategory = -1; + + String[] columnTitles; + HashMapBlows[] columnCategories; + HashMap columnIndices; + +// static final int TSV = 1; +// static final int CSV = 2; +// static final int AWFUL_CSV = 3; + +// boolean typed; + + // untyped data +// protected String[][] data; +// protected Object[] data; // [row][column] + protected Object[] columns; // [column] + + // typed data + static final int STRING = 0; + static final int INT = 1; + static final int LONG = 2; + static final int FLOAT = 3; + static final int DOUBLE = 4; + static final int CATEGORICAL = 5; +// static final int TIME = 5; + int[] columnTypes; + +// int[][] intData; // [column][row] +// long[][] longData; +// float[][] floatData; +// double[][] doubleData; +// Object[][] objectData; + + + /** + * Creates a new, empty table. Use addRow() to add additional rows. + */ + public Table() { + columns = new Object[0]; + columnTypes = new int[0]; + columnCategories = new HashMapBlows[0]; + } + + + public Table(File file) { + this(PApplet.createReader(file)); + } + + + /** + * Can handle TSV or CSV files. + * @param parent + * @param filename + */ + public Table(PApplet parent, String filename) { + this(parent.createReader(filename)); + } + + + public Table(BufferedReader reader) { + columns = new Object[0]; + columnTypes = new int[0]; + columnCategories = new HashMapBlows[0]; + try { + boolean csv = peekCSV(reader); + if (csv) { + parseCSV(reader); + } else { + parseTSV(reader); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + + public Table(ResultSet rs) { + this(); + try { + ResultSetMetaData rsmd = rs.getMetaData(); + + int columnCount = rsmd.getColumnCount(); + setColumnCount(columnCount); + + for (int col = 0; col < columnCount; col++) { + setColumnTitle(col, rsmd.getColumnName(col + 1)); + + int type = rsmd.getColumnType(col + 1); + switch (type) { // TODO these aren't tested. nor are they complete. + case Types.INTEGER: + case Types.TINYINT: + case Types.SMALLINT: + setColumnType(col, INT); + break; + case Types.BIGINT: + setColumnType(col, LONG); + break; + case Types.FLOAT: + setColumnType(col, FLOAT); + break; + case Types.DECIMAL: + case Types.DOUBLE: + case Types.REAL: + setColumnType(col, DOUBLE); + break; + } + } + + int row = 0; + while (rs.next()) { + for (int col = 0; col < columnCount; col++) { + switch (columnTypes[col]) { + case STRING: setString(row, col, rs.getString(col+1)); break; + case INT: setInt(row, col, rs.getInt(col+1)); break; + case LONG: setLong(row, col, rs.getLong(col+1)); break; + case FLOAT: setFloat(row, col, rs.getFloat(col+1)); break; + case DOUBLE: setDouble(row, col, rs.getDouble(col+1)); break; + default: throw new IllegalArgumentException("column type " + columnTypes[col] + " not supported."); + } + } + row++; +// String[] row = new String[columnCount]; +// for (int col = 0; col < columnCount; col++) { +// row[col] = rs.getString(col + 1); +// } +// addRow(row); + } + + } catch (SQLException s) { + throw new RuntimeException(s); + } + } + + + /** + * Guess whether this file is tab separated or comma separated by checking + * whether there are more tabs or commas in the first 100 characters. + */ + protected boolean peekCSV(BufferedReader reader) throws IOException { + char[] buffer = new char[100]; + int remaining = buffer.length; + reader.mark(remaining); +// int count = 0; + int commas = 0; + int tabs = 0; + for (int i = 0; i < remaining; i++) { + int c = reader.read(); + if (c == -1) break; + if (c == ',') commas++; + if (c == '\t') tabs++; + } + reader.reset(); + return (commas > tabs); + } + + + public void parse(BufferedReader reader) throws IOException { + if (commaSeparatedValues) { + if (awfulCSV) { + parseAwfulCSV(reader); + } else { + parseCSV(reader); + } + } else { + parseTSV(reader); + } + } + + + public void parseTSV(BufferedReader reader) throws IOException { + parseBasic(reader, true); +// String line = null; +// int row = 0; +// if (rowCount == 0) { +// setRowCount(10); +// } +// while ((line = reader.readLine()) != null) { +// if (row == getRowCount()) { +// setRowCount(row << 1); +// } +// setRow(row, PApplet.split(line, '\t')); +// row++; +// } +// // shorten or lengthen based on what's left +// if (row != getRowCount()) { +// setRowCount(row); +// } + } + + + public void parseCSV(BufferedReader reader) throws IOException { + parseBasic(reader, false); +// String line = null; +// int row = 0; +// if (rowCount == 0) { +// setRowCount(10); +// } +// while ((line = reader.readLine()) != null) { +// if (row == getRowCount()) { +// setRowCount(row << 1); +// } +// setRow(row, splitLineCSV(line)); +// row++; +// } +// // shorten or lengthen based on what's left +// if (row != getRowCount()) { +// setRowCount(row); +// } + } + + + protected void parseBasic(BufferedReader reader, boolean tsv) throws IOException { + String line = null; + int row = 0; + if (rowCount == 0) { + setRowCount(10); + } + int prev = 0; //-1; + while ((line = reader.readLine()) != null) { + if (row == getRowCount()) { + setRowCount(row << 1); + } + setRow(row, tsv ? PApplet.split(line, '\t') : splitLineCSV(line)); + row++; + + if (row % 10000 == 0) { + if (row < rowCount) { + int pct = (100 * row) / rowCount; + if (pct != prev) { // also prevents "0%" from showing up + System.out.println(pct + "%"); + prev = pct; + } + } + try { + Thread.sleep(5); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + // shorten or lengthen based on what's left + if (row != getRowCount()) { + setRowCount(row); + } + } + + + public void convertTSV(BufferedReader reader, File outputFile) throws IOException { + convertBasic(reader, true, outputFile); + } + + + protected void convertBasic(BufferedReader reader, boolean tsv, + File outputFile) throws IOException { + FileOutputStream fos = new FileOutputStream(outputFile); + BufferedOutputStream bos = new BufferedOutputStream(fos, 16384); + DataOutputStream output = new DataOutputStream(bos); + output.writeInt(0); // come back for row count + output.writeInt(getColumnCount()); + if (columnTitles != null) { + output.writeBoolean(true); + for (String title : columnTitles) { + output.writeUTF(title); + } + } else { + output.writeBoolean(false); + } + for (int type : columnTypes) { + output.writeInt(type); + } + + String line = null; + //setRowCount(1); + int prev = -1; + int row = 0; + while ((line = reader.readLine()) != null) { + convertRow(output, tsv ? PApplet.split(line, '\t') : splitLineCSV(line)); + row++; + + if (row % 10000 == 0) { + if (row < rowCount) { + int pct = (100 * row) / rowCount; + if (pct != prev) { + System.out.println(pct + "%"); + prev = pct; + } + } +// try { +// Thread.sleep(5); +// } catch (InterruptedException e) { +// e.printStackTrace(); +// } + } + } + // shorten or lengthen based on what's left +// if (row != getRowCount()) { +// setRowCount(row); +// } + + // has to come afterwards, since these tables get built out during the conversion + int col = 0; + for (HashMapBlows hmb : columnCategories) { + if (hmb == null) { + output.writeInt(0); + } else { + hmb.write(output); + hmb.writeln(PApplet.createWriter(new File(columnTitles[col] + ".categories"))); +// output.writeInt(hmb.size()); +// for (Map.Entry e : hmb.entrySet()) { +// output.writeUTF(e.getKey()); +// output.writeInt(e.getValue()); +// } + } + col++; + } + + output.flush(); + output.close(); + + // come back and write the row count + RandomAccessFile raf = new RandomAccessFile(outputFile, "rw"); + raf.writeInt(rowCount); + raf.close(); + } + + + public void parseAwfulCSV(BufferedReader reader) throws IOException { + char[] c = new char[100]; + int count = 0; + boolean insideQuote = false; + int row = 0; + int col = 0; + int ch; + while ((ch = reader.read()) != -1) { + if (insideQuote) { + if (ch == '\"') { + // this is either the end of a quoted entry, or a quote character + reader.mark(1); + if (reader.read() == '\"') { + // it's "", which means a quote character + if (count == c.length) { + c = PApplet.expand(c); + } + c[count++] = '\"'; + } else { + // nope, just the end of a quoted csv entry + reader.reset(); + insideQuote = false; + // TODO nothing here that prevents bad csv data from showing up + // after the quote and before the comma... +// setString(row, col, new String(c, 0, count)); +// count = 0; +// col++; +// insideQuote = false; + } + } else { // inside a quote, but the character isn't a quote + if (count == c.length) { + c = PApplet.expand(c); + } + c[count++] = (char) ch; + } + } else { // not inside a quote + if (ch == '\"') { + insideQuote = true; + + } else if (ch == '\r') { + // check to see if next is a '\n' + reader.mark(1); + if (reader.read() != '\n') { + reader.reset(); + } + setString(row, col, new String(c, 0, count)); + count = 0; + row++; + col = 0; + + } else if (ch == '\n') { + setString(row, col, new String(c, 0, count)); + count = 0; + row++; + col = 0; + + } else if (ch == ',') { + setString(row, col, new String(c, 0, count)); + count = 0; + // starting a new column, make sure we have room + col++; + checkColumn(col); + + } else { // just a regular character, add it + if (count == c.length) { + c = PApplet.expand(c); + } + c[count++] = (char) ch; + } + } + } + // catch any leftovers + if (count > 0) { + setString(row, col, new String(c, 0, count)); + } + } + + + protected String[] splitLine(String line) { + return commaSeparatedValues ? splitLineCSV(line) : PApplet.split(line, '\t'); + } + + + /** + * Parse a line of text as comma-separated values, returning each value as + * one entry in an array of String objects. Remove quotes from entries that + * begin and end with them, and convert 'escaped' quotes to actual quotes. + * @param line line of text to be parsed + * @return an array of the individual values formerly separated by commas + */ + static public String[] splitLineCSV(String line) { + char[] c = line.toCharArray(); + int rough = 1; // at least one + boolean quote = false; + for (int i = 0; i < c.length; i++) { + if (!quote && (c[i] == ',')) { + rough++; + } else if (c[i] == '\"') { + quote = !quote; + } + } + String[] pieces = new String[rough]; + int pieceCount = 0; + int offset = 0; + while (offset < c.length) { + int start = offset; + int stop = nextComma(c, offset); + offset = stop + 1; // next time around, need to step over the comment + if (c[start] == '\"' && c[stop-1] == '\"') { + start++; + stop--; + } + int i = start; + int ii = start; + while (i < stop) { + if (c[i] == '\"') { + i++; // skip over pairs of double quotes become one + } + if (i != ii) { + c[ii] = c[i]; + } + i++; + ii++; + } + String s = new String(c, start, ii - start); + pieces[pieceCount++] = s; + } + // make any remaining entries blanks instead of nulls + for (int i = pieceCount; i < pieces.length; i++) { + pieces[i] = ""; + + } + return pieces; + } + + + static int nextComma(char[] c, int index) { + boolean quote = false; + for (int i = index; i < c.length; i++) { + if (!quote && (c[i] == ',')) { + return i; + } else if (c[i] == '\"') { + quote = !quote; + } + } + return c.length; + } + + + public void writeTSV(PrintWriter writer) { + if (columnTitles != null) { + for (int col = 0; col < columns.length; col++) { + if (col != 0) { + writer.print('\t'); + } + if (columnTitles[col] != null) { + writer.print(columnTitles[col]); + } + } + writer.println(); + } + for (int row = 0; row < rowCount; row++) { + for (int col = 0; col < getColumnCount(); col++) { + if (col != 0) { + writer.print('\t'); + } + String entry = getString(row, col); + // just write null entries as blanks, rather than spewing 'null' + // all over the spreadsheet file. + if (entry != null) { + writer.print(entry); + } + } + writer.println(); + } + writer.flush(); + } + + + public void writeCSV(PrintWriter writer) { + if (columnTitles != null) { + for (int col = 0; col < columns.length; col++) { + if (col != 0) { + writer.print(','); + } + if (columnTitles[col] != null) { + writeEntryCSV(writer, columnTitles[col]); + } + } + writer.println(); + } + for (int row = 0; row < rowCount; row++) { + for (int col = 0; col < getColumnCount(); col++) { + if (col != 0) { + writer.print(','); + } + String entry = getString(row, col); + // just write null entries as blanks, rather than spewing 'null' + // all over the spreadsheet file. + if (entry != null) { + writeEntryCSV(writer, entry); + } + } + // Prints the newline for the row, even if it's missing + writer.println(); + } + writer.flush(); + } + + + protected void writeEntryCSV(PrintWriter writer, String entry) { + if (entry != null) { + if (entry.indexOf('\"') != -1) { // convert quotes to double quotes + char[] c = entry.toCharArray(); + writer.print('\"'); + for (int i = 0; i < c.length; i++) { + if (c[i] == '\"') { + writer.print("\"\""); + } else { + writer.print(c[i]); + } + } + writer.print('\"'); + + // add quotes if commas or CR/LF are in the entry + } else if (entry.indexOf(',') != -1 || + entry.indexOf('\n') != -1 || + entry.indexOf('\r') != -1) { + writer.print('\"'); + writer.print(entry); + writer.print('\"'); + + + // add quotes if leading or trailing space + } else if ((entry.length() > 0) && + (entry.charAt(0) == ' ' || + entry.charAt(entry.length() - 1) == ' ')) { + writer.print('\"'); + writer.print(entry); + writer.print('\"'); + + } else { + writer.print(entry); + } + } + } + + + public void writeHTML(PrintWriter writer) { + writer.println(""); + for (int row = 0; row < getRowCount(); row++) { + writer.println(" "); + for (int col = 0; col < getColumnCount(); col++) { + String entry = getString(row, col); + writer.print(" "); + writer.println(""); + } + writer.println(" "); + } + writer.println("
"); + writeEntryHTML(writer, entry); +// String clean = (entry == null) ? "" : HTMLFairy.encodeEntities(entry); +// writer.println(" " + clean + "
"); + writer.flush(); + } + + + protected void writeEntryHTML(PrintWriter writer, String entry) { + //char[] chars = entry.toCharArray(); + for (char c : entry.toCharArray()) { //chars) { + if (c < 32 || c > 127) { + writer.print("&#"); + writer.print((int) c); + writer.print(';'); + } else { + writer.print(c); + } + } + } + + + /** + * Write this table as a TSV file. + * Exceptions will be printed, but not thrown. + * @param file the location to write to. + * @return true if written successfully + */ + public boolean writeCSV(File file) { + try { + writeCSV(new PrintWriter(new FileWriter(file))); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + return true; + } + + + public boolean writeTSV(File file) { + try { + writeTSV(new PrintWriter(new FileWriter(file))); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + return true; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public void addColumn() { + addColumn(null, STRING); + } + + + public void addColumn(String title) { + addColumn(title, STRING); + } + + + public void addColumn(String title, int type) { + insertColumn(columns.length, title, type); + } + + + public void insertColumn(int index) { + insertColumn(index, null, STRING); + } + + + public void insertColumn(int index, String title) { + insertColumn(index, title, STRING); + } + + + public void insertColumn(int index, String title, int type) { + if (title != null && columnTitles == null) { + columnTitles = new String[columns.length]; + } + if (columnTitles != null) { + columnTitles = PApplet.splice(columnTitles, title, index); + columnIndices = null; + } + columnTypes = PApplet.splice(columnTypes, type, index); + +// columnCategories = (HashMapBlows[]) +// PApplet.splice(columnCategories, new HashMapBlows(), index); + HashMapBlows[] catTemp = new HashMapBlows[columns.length + 1]; + // Faster than arrayCopy for a dozen or so entries + for (int i = 0; i < index; i++) { + catTemp[i] = columnCategories[i]; + } + catTemp[index] = new HashMapBlows(); + for (int i = index; i < columns.length; i++) { + catTemp[i+1] = columnCategories[i]; + } + columnCategories = catTemp; + + Object[] temp = new Object[columns.length + 1]; + System.arraycopy(columns, 0, temp, 0, index); + System.arraycopy(columns, index, temp, index+1, columns.length - index); + columns = temp; + + switch (type) { + case INT: columns[index] = new int[rowCount]; break; + case LONG: columns[index] = new long[rowCount]; break; + case FLOAT: columns[index] = new float[rowCount]; break; + case DOUBLE: columns[index] = new double[rowCount]; break; + case STRING: columns[index] = new String[rowCount]; break; + case CATEGORICAL: columns[index] = new int[rowCount]; break; + } + } + + + public void removeColumn(String dead) { + removeColumn(getColumnIndex(dead)); + } + + + public void removeColumn(int index) { + Object[] temp = new Object[columns.length + 1]; + System.arraycopy(columns, 0, temp, 0, index); + System.arraycopy(columns, index+1, temp, index, (columns.length - index) + 1); + columns = temp; + } + + + public int getColumnCount() { + return columns.length; + } + + + /** + * Change the number of columns in this table. Resizes all rows to ensure + * the same number of columns in each row. Entries in the additional (empty) + * columns will be set to null. + * @param newCount + */ + public void setColumnCount(int newCount) { + int oldCount = columns.length; + if (oldCount != newCount) { + columns = (Object[]) PApplet.expand(columns, newCount); + // create new columns, default to String as the data type + for (int c = oldCount; c < newCount; c++) { + columns[c] = new String[rowCount]; + } + + if (columnTitles != null) { + columnTitles = PApplet.expand(columnTitles, newCount); + } + columnTypes = PApplet.expand(columnTypes, newCount); + columnCategories = (HashMapBlows[]) + PApplet.expand(columnCategories, newCount); + } + } + + + public void setColumnType(String columnName, String columnType) { + setColumnType(getColumnIndex(columnName), columnType); + } + + + /** + * Set the data type for a column so that using it is more efficient. + * @param column the column to change + * @param columnType One of int, long, float, double, or String. + */ + public void setColumnType(int column, String columnType) { + int type = -1; + if (columnType.equals("String")) { + type = STRING; + } else if (columnType.equals("int")) { + type = INT; + } else if (columnType.equals("long")) { + type = LONG; + } else if (columnType.equals("float")) { + type = FLOAT; + } else if (columnType.equals("double")) { + type = DOUBLE; + } else if (columnType.equals("categorical")) { + type = CATEGORICAL; + } else { + throw new IllegalArgumentException("'" + columnType + "' is not a valid column type."); + } + setColumnType(column, type); + } + + + protected void setColumnType(String columnName, int newType) { + setColumnType(getColumnIndex(columnName), newType); + } + + + /** + * Sets the column type. If data already exists, then it'll be converted to + * the new type. + * @param column the column whose type should be changed + * @param newType something fresh, maybe try an int or a float for size? + */ + protected void setColumnType(int column, int newType) { + switch (newType) { + case INT: { + int[] intData = new int[rowCount]; + for (int row = 0; row < rowCount; row++) { + String s = getString(row, column); + intData[row] = PApplet.parseInt(s, missingInt); + } + columns[column] = intData; + break; + } + case LONG: { + long[] longData = new long[rowCount]; + for (int row = 0; row < rowCount; row++) { + String s = getString(row, column); + try { + longData[row] = Long.parseLong(s); + } catch (NumberFormatException nfe) { + longData[row] = missingLong; + } + } + columns[column] = longData; + break; + } + case FLOAT: { + float[] floatData = new float[rowCount]; + for (int row = 0; row < rowCount; row++) { + String s = getString(row, column); + floatData[row] = PApplet.parseFloat(s, missingFloat); + } + columns[column] = floatData; + break; + } + case DOUBLE: { + double[] doubleData = new double[rowCount]; + for (int row = 0; row < rowCount; row++) { + String s = getString(row, column); + try { + doubleData[row] = Double.parseDouble(s); + } catch (NumberFormatException nfe) { + doubleData[row] = missingDouble; + } + } + columns[column] = doubleData; + break; + } + case STRING: { + if (columnTypes[column] != STRING) { + String[] stringData = new String[rowCount]; + for (int row = 0; row < rowCount; row++) { + stringData[row] = getString(row, column); + } + columns[column] = stringData; + } + break; + } + case CATEGORICAL: { + int[] indexData = new int[rowCount]; + HashMapBlows categories = new HashMapBlows(); + for (int row = 0; row < rowCount; row++) { + String s = getString(row, column); + indexData[row] = categories.index(s); + } + columnCategories[column] = categories; + columns[column] = indexData; + break; + } + default: { + throw new IllegalArgumentException("That's not a valid column type."); + } + } +// System.out.println("new type is " + newType); + columnTypes[column] = newType; + } + + + /** + * Set the entire table to a specific data type. + */ + public void setTableType(String type) { + for (int col = 0; col < columns.length; col++) { + setColumnType(col, type); + } + } + + + /** + * Set the titles (and if a second column is present) the data types for + * this table based on a file loaded separately. + * @param dictionary + */ + public void setColumnTypes(Table dictionary) { + setColumnTitles(dictionary.getStringColumn(0)); + if (dictionary.getColumnCount() > 1) { + for (int i = 0; i < dictionary.getRowCount(); i++) { + setColumnType(i, dictionary.getString(i, 1)); + } + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Remove the first row from the data set, and use it as the column titles. + */ + public String[] removeTitleRow() { + String[] titles = getStringRow(0); + removeRow(0); + setColumnTitles(titles); + return titles; + } + + + public void setColumnTitles(String[] titles) { + if (titles != null) { + checkColumn(titles.length - 1); + } + columnTitles = titles; + columnIndices = null; // remove the cache + } + + + public void setColumnTitle(int column, String title) { + checkColumn(column); + if (columnTitles == null) { + columnTitles = new String[getColumnCount()]; + } + columnTitles[column] = title; + columnIndices = null; // reset these fellas + } + + + public String[] getColumnTitles() { + return columnTitles; + } + + + public String getColumnTitle(int col) { + return (columnTitles == null) ? null : columnTitles[col]; + } + + + public int getColumnIndex(String name) { + return getColumnIndex(name, true); + } + + + /** + * Get the index of a column. + * @param name Name of the column. + * @param report Whether to print to System.err if the column wasn't found. + * @return index of the found column, or -1 if not found. + */ + protected int getColumnIndex(String name, boolean report) { + if (columnTitles == null) { + System.err.println("Can't get column indices because no column titles are set."); + return -1; + } + // only create this on first get(). subsequent calls to set the title will + // also update this array, but only if it exists. + if (columnIndices == null) { + columnIndices = new HashMap(); + for (int col = 0; col < columns.length; col++) { + columnIndices.put(columnTitles[col], col); + } + } + Integer index = columnIndices.get(name); + if (index == null) { + if (report) { + System.err.println("No column named '" + name + "' was found."); + } + return -1; + } + return index.intValue(); + } + + + /** + * Same as getColumnIndex(), but creates the column if it doesn't exist. + * Named this way to not conflict with checkColumn(), an internal function + * used to ensure that a columns exists, and also to denote that it returns + * an int for the column index. + * @param title column title + * @return index of a new or previously existing column + */ + public int checkColumnIndex(String title) { + int index = getColumnIndex(title, false); + if (index != -1) { + return index; + } + addColumn(title); + return getColumnCount() - 1; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public int getRowCount() { + return rowCount; + } + + + public void setRowCount(int newCount) { + if (newCount != rowCount) { + if (newCount > 1000000) { + System.out.println("setting row count to " + PApplet.nfc(newCount)); + } + long t = System.currentTimeMillis(); + for (int col = 0; col < columns.length; col++) { + switch (columnTypes[col]) { + case INT: columns[col] = PApplet.expand((int[]) columns[col], newCount); break; + case LONG: columns[col] = PApplet.expand((long[]) columns[col], newCount); break; + case FLOAT: columns[col] = PApplet.expand((float[]) columns[col], newCount); break; + case DOUBLE: columns[col] = PApplet.expand((double[]) columns[col], newCount); break; + case STRING: columns[col] = PApplet.expand((String[]) columns[col], newCount); break; + case CATEGORICAL: columns[col] = PApplet.expand((int[]) columns[col], newCount); break; + } + if (newCount > 1000000) { + try { + Thread.sleep(10); // gc time! + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + if (newCount > 1000000) { + int ms = (int) (System.currentTimeMillis() - t); + System.out.println(" resize took " + PApplet.nfc(ms) + " ms"); + } + } + rowCount = newCount; + } + + + public void addRow() { + setRowCount(rowCount + 1); + } + + + public void addRow(String[] columns) { + setRow(getRowCount(), columns); + } + + + public void insertRow(int insert, String[] data) { + for (int col = 0; col < columns.length; col++) { + switch (columnTypes[col]) { + case CATEGORICAL: + case INT: { + int[] intTemp = new int[rowCount+1]; + System.arraycopy(columns[col], 0, intTemp, 0, insert); + System.arraycopy(columns[col], insert, intTemp, insert+1, (rowCount - insert) + 1); + columns[col] = intTemp; + break; + } + case LONG: { + long[] longTemp = new long[rowCount+1]; + System.arraycopy(columns[col], 0, longTemp, 0, insert); + System.arraycopy(columns[col], insert, longTemp, insert+1, (rowCount - insert) + 1); + columns[col] = longTemp; + break; + } + case FLOAT: { + float[] floatTemp = new float[rowCount+1]; + System.arraycopy(columns[col], 0, floatTemp, 0, insert); + System.arraycopy(columns[col], insert, floatTemp, insert+1, (rowCount - insert) + 1); + columns[col] = floatTemp; + break; + } + case DOUBLE: { + double[] doubleTemp = new double[rowCount+1]; + System.arraycopy(columns[col], 0, doubleTemp, 0, insert); + System.arraycopy(columns[col], insert, doubleTemp, insert+1, (rowCount - insert) + 1); + columns[col] = doubleTemp; + break; + } + case STRING: { + String[] stringTemp = new String[rowCount+1]; + System.arraycopy(columns[col], 0, stringTemp, 0, insert); + System.arraycopy(columns[col], insert, stringTemp, insert+1, (rowCount - insert) + 1); + columns[col] = stringTemp; + break; + } + } + } + setRow(insert, data); + rowCount++; + } + + + public void removeRow(int dead) { + for (int col = 0; col < columns.length; col++) { + switch (columnTypes[col]) { + case CATEGORICAL: + case INT: { + int[] intTemp = new int[rowCount-1]; +// int[] intData = (int[]) columns[col]; +// System.arraycopy(intData, 0, intTemp, 0, dead); +// System.arraycopy(intData, dead+1, intTemp, dead, (rowCount - dead) + 1); + System.arraycopy(columns[col], 0, intTemp, 0, dead); + System.arraycopy(columns[col], dead+1, intTemp, dead, (rowCount - dead) - 1); + columns[col] = intTemp; + break; + } + case LONG: { + long[] longTemp = new long[rowCount-1]; +// long[] longData = (long[]) columns[col]; +// System.arraycopy(longData, 0, longTemp, 0, dead); +// System.arraycopy(longData, dead+1, longTemp, dead, (rowCount - dead) + 1); + System.arraycopy(columns[col], 0, longTemp, 0, dead); + System.arraycopy(columns[col], dead+1, longTemp, dead, (rowCount - dead) - 1); + columns[col] = longTemp; + break; + } + case FLOAT: { + float[] floatTemp = new float[rowCount-1]; +// float[] floatData = (float[]) columns[col]; +// System.arraycopy(floatData, 0, floatTemp, 0, dead); +// System.arraycopy(floatData, dead+1, floatTemp, dead, (rowCount - dead) + 1); + System.arraycopy(columns[col], 0, floatTemp, 0, dead); + System.arraycopy(columns[col], dead+1, floatTemp, dead, (rowCount - dead) - 1); + columns[col] = floatTemp; + break; + } + case DOUBLE: { + double[] doubleTemp = new double[rowCount-1]; +// double[] doubleData = (double[]) columns[col]; +// System.arraycopy(doubleData, 0, doubleTemp, 0, dead); +// System.arraycopy(doubleData, dead+1, doubleTemp, dead, (rowCount - dead) + 1); + System.arraycopy(columns[col], 0, doubleTemp, 0, dead); + System.arraycopy(columns[col], dead+1, doubleTemp, dead, (rowCount - dead) - 1); + columns[col] = doubleTemp; + break; + } + case STRING: { + String[] stringTemp = new String[rowCount-1]; + System.arraycopy(columns[col], 0, stringTemp, 0, dead); + System.arraycopy(columns[col], dead+1, stringTemp, dead, (rowCount - dead) - 1); + columns[col] = stringTemp; + } + } + } + rowCount--; + } + + + public void setRow(int row, String[] pieces) { + checkSize(row, pieces.length - 1); + // pieces.length may be less than columns.length, so loop over pieces + for (int col = 0; col < pieces.length; col++) { + setRowCol(row, col, pieces[col]); + } + } + + + protected void setRowCol(int row, int col, String piece) { + switch (columnTypes[col]) { + case STRING: + String[] stringData = (String[]) columns[col]; + stringData[row] = piece; + break; + case INT: + int[] intData = (int[]) columns[col]; + intData[row] = PApplet.parseInt(piece, missingInt); + break; + case LONG: + long[] longData = (long[]) columns[col]; + try { + longData[row] = Long.parseLong(piece); + } catch (NumberFormatException nfe) { + longData[row] = missingLong; + } + break; + case FLOAT: + float[] floatData = (float[]) columns[col]; + floatData[row] = PApplet.parseFloat(piece, missingFloat); + break; + case DOUBLE: + double[] doubleData = (double[]) columns[col]; + try { + doubleData[row] = Double.parseDouble(piece); + } catch (NumberFormatException nfe) { + doubleData[row] = missingDouble; + } + break; + case CATEGORICAL: + int[] indexData = (int[]) columns[col]; + indexData[row] = columnCategories[col].index(piece); + break; + default: + throw new IllegalArgumentException("That's not a valid column type."); + } + } + + + public void convertRow(DataOutputStream output, String[] pieces) throws IOException { + if (pieces.length > getColumnCount()) { + throw new IllegalArgumentException("Row with too many columns: " + + PApplet.join(pieces, ",")); + } + // pieces.length may be less than columns.length, so loop over pieces + for (int col = 0; col < pieces.length; col++) { + switch (columnTypes[col]) { + case STRING: + output.writeUTF(pieces[col]); + break; + case INT: + output.writeInt(PApplet.parseInt(pieces[col], missingInt)); + break; + case LONG: + try { + output.writeLong(Long.parseLong(pieces[col])); + } catch (NumberFormatException nfe) { + output.writeLong(missingLong); + } + break; + case FLOAT: + output.writeFloat(PApplet.parseFloat(pieces[col], missingFloat)); + break; + case DOUBLE: + try { + output.writeDouble(Double.parseDouble(pieces[col])); + } catch (NumberFormatException nfe) { + output.writeDouble(missingDouble); + } + break; + case CATEGORICAL: + output.writeInt(columnCategories[col].index(pieces[col])); + break; + } + } + for (int col = pieces.length; col < getColumnCount(); col++) { + switch (columnTypes[col]) { + case STRING: + output.writeUTF(""); + break; + case INT: + output.writeInt(missingInt); + break; + case LONG: + output.writeLong(missingLong); + break; + case FLOAT: + output.writeFloat(missingFloat); + break; + case DOUBLE: + output.writeDouble(missingDouble); + break; + case CATEGORICAL: + output.writeInt(missingCategory); + break; + + } + } + } + + + protected void convertRowCol(DataOutputStream output, int row, int col, String piece) { + switch (columnTypes[col]) { + case STRING: + String[] stringData = (String[]) columns[col]; + stringData[row] = piece; + break; + case INT: + int[] intData = (int[]) columns[col]; + intData[row] = PApplet.parseInt(piece, missingInt); + break; + case LONG: + long[] longData = (long[]) columns[col]; + try { + longData[row] = Long.parseLong(piece); + } catch (NumberFormatException nfe) { + longData[row] = missingLong; + } + break; + case FLOAT: + float[] floatData = (float[]) columns[col]; + floatData[row] = PApplet.parseFloat(piece, missingFloat); + break; + case DOUBLE: + double[] doubleData = (double[]) columns[col]; + try { + doubleData[row] = Double.parseDouble(piece); + } catch (NumberFormatException nfe) { + doubleData[row] = missingDouble; + } + break; + default: + throw new IllegalArgumentException("That's not a valid column type."); + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public interface Row { + + public String getString(int column); + public String getString(String columnName); + public int getInt(int column); + public int getInt(String columnName); + public long getLong(int column); + public long getLong(String columnName); + public float getFloat(int column); + public float getFloat(String columnName); + public double getDouble(int column); + public double getDouble(String columnName); + } + + + protected RowIterator rowIterator; + + /** + * Note that this one iterator instance is shared by any calls to iterate the + * rows of this table. This is very efficient, but not very thread-safe. If + * you want to iterate in a multi-threaded manner, use createIterator(). + */ + public Iterator iterator() { + if (rowIterator == null) { + rowIterator = new RowIterator(); + } + rowIterator.reset(); + return rowIterator; + } + + + public Iterator createIterator() { + return new RowIterator(); + } + + + // temporary objects inside loop! garbage collection! argh! +// public Iterator iterator() { +// return new RowIterator(); +// } + + + class RowIterator implements Iterator { + int row; + Row tableRow = new Row() { + public String getString(int column) { + return Table.this.getString(row, column); + } + + public String getString(String columnName) { + return Table.this.getString(row, columnName); + } + + public int getInt(int column) { + return Table.this.getInt(row, column); + } + + public int getInt(String columnName) { + return Table.this.getInt(row, columnName); + } + + public long getLong(int column) { + return Table.this.getLong(row, column); + } + + public long getLong(String columnName) { + return Table.this.getLong(row, columnName); + } + + public float getFloat(int column) { + return Table.this.getFloat(row, column); + } + + public float getFloat(String columnName) { + return Table.this.getFloat(row, columnName); + } + + public double getDouble(int column) { + return Table.this.getDouble(row, column); + } + + public double getDouble(String columnName) { + return Table.this.getDouble(row, columnName); + } + }; + + public void remove() { + removeRow(row); + } + + public Row next() { + ++row; +// iteratorRow.setRow(row); +// return iteratorRow; + return tableRow; + } + + public boolean hasNext() { + return row+1 < getRowCount(); + } + + public void reset() { + row = -1; + } + } + + + static public Iterator createIterator(final ResultSet rs) { + return new Iterator() { + boolean already; + + public boolean hasNext() { + already = true; + try { + return rs.next(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + + public Row next() { + if (!already) { + try { + rs.next(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } else { + already = false; + } + + return new Row() { + public double getDouble(int column) { + try { + return rs.getDouble(column); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public double getDouble(String columnName) { + try { + return rs.getDouble(columnName); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public float getFloat(int column) { + try { + return rs.getFloat(column); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public float getFloat(String columnName) { + try { + return rs.getFloat(columnName); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public int getInt(int column) { + try { + return rs.getInt(column); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public int getInt(String columnName) { + try { + return rs.getInt(columnName); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public long getLong(int column) { + try { + return rs.getLong(column); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public long getLong(String columnName) { + try { + return rs.getLong(columnName); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public String getString(int column) { + try { + return rs.getString(column); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public String getString(String columnName) { + try { + return rs.getString(columnName); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + }; + } + + public void remove() { + throw new IllegalArgumentException("remove() not supported"); + } + }; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public int getInt(int row, int column) { + checkBounds(row, column); + if (columnTypes[column] == INT) { + int[] intData = (int[]) columns[column]; + return intData[row]; + } + String str = getString(row, column); + return (str == null || str.equals(missingString)) ? + missingInt : PApplet.parseInt(str, missingInt); + } + + + public int getInt(int row, String columnName) { + return getInt(row, getColumnIndex(columnName)); + } + + + public void setMissingInt(int value) { + missingInt = value; + } + + + public void setInt(int row, int column, int what) { + if (columnTypes[column] == STRING) { + setString(row, column, String.valueOf(what)); + + } else { + checkSize(row, column); + if (columnTypes[column] != INT) { + throw new IllegalArgumentException("Column " + column + " is not an int column."); + } + int[] intData = (int[]) columns[column]; + intData[row] = what; + } + } + + + public int[] getIntColumn(String name) { + int col = getColumnIndex(name); + return (col == -1) ? null : getIntColumn(col); + } + + + public int[] getIntColumn(int col) { + int[] outgoing = new int[rowCount]; + for (int row = 0; row < rowCount; row++) { + outgoing[row] = getInt(row, col); + } + return outgoing; + } + + + public int[] getIntRow(int row) { + int[] outgoing = new int[columns.length]; + for (int col = 0; col < columns.length; col++) { + outgoing[col] = getInt(row, col); + } + return outgoing; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public long getLong(int row, int column) { + checkBounds(row, column); + if (columnTypes[column] == LONG) { + long[] longData = (long[]) columns[column]; + return longData[row]; + } + String str = getString(row, column); + if (str == null || str.equals(missingString)) { + return missingLong; + } + try { + return Long.parseLong(str); + } catch (NumberFormatException nfe) { + return missingLong; + } + } + + + public long getLong(int row, String columnName) { + return getLong(row, getColumnIndex(columnName)); + } + + + public void setMissingLong(long value) { + missingLong = value; + } + + + public void setLong(int row, int column, long what) { + if (columnTypes[column] == STRING) { + setString(row, column, String.valueOf(what)); + + } else { + checkSize(row, column); + if (columnTypes[column] != LONG) { + throw new IllegalArgumentException("Column " + column + " is not a 'long' column."); + } + long[] longData = (long[]) columns[column]; + longData[row] = what; + } + } + + + public long[] getLongColumn(String name) { + int col = getColumnIndex(name); + return (col == -1) ? null : getLongColumn(col); + } + + + public long[] getLongColumn(int col) { + long[] outgoing = new long[rowCount]; + for (int row = 0; row < rowCount; row++) { + outgoing[row] = getLong(row, col); + } + return outgoing; + } + + + public long[] getLongRow(int row) { + long[] outgoing = new long[columns.length]; + for (int col = 0; col < columns.length; col++) { + outgoing[col] = getLong(row, col); + } + return outgoing; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Get a float value from the specified row and column. If the value is null + * or not parseable as a float, the "missing" value is returned. By default, + * this is Float.NaN, but can be controlled with setMissingFloat(). + */ + public float getFloat(int row, int column) { + checkBounds(row, column); + if (columnTypes[column] == FLOAT) { + float[] floatData = (float[]) columns[column]; + return floatData[row]; + } + String str = getString(row, column); + if (str == null || str.equals(missingString)) { + return missingFloat; + } + return PApplet.parseFloat(str, missingFloat); + } + + + public float getFloat(int row, String columnName) { + return getFloat(row, getColumnIndex(columnName)); + } + + + public void setMissingFloat(float value) { + missingFloat = value; + } + + + public void setFloat(int row, int column, float what) { + if (columnTypes[column] == STRING) { + setString(row, column, String.valueOf(what)); + + } else { + checkSize(row, column); + if (columnTypes[column] != FLOAT) { + throw new IllegalArgumentException("Column " + column + " is not a float column."); + } + float[] longData = (float[]) columns[column]; + longData[row] = what; + } + } + + + public float[] getFloatColumn(String name) { + int col = getColumnIndex(name); + return (col == -1) ? null : getFloatColumn(col); + } + + + public float[] getFloatColumn(int col) { + float[] outgoing = new float[rowCount]; + for (int row = 0; row < rowCount; row++) { + outgoing[row] = getFloat(row, col); + } + return outgoing; + } + + + public float[] getFloatRow(int row) { + float[] outgoing = new float[columns.length]; + for (int col = 0; col < columns.length; col++) { + outgoing[col] = getFloat(row, col); + } + return outgoing; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public double getDouble(int row, int column) { + checkBounds(row, column); + if (columnTypes[column] == DOUBLE) { + double[] doubleData = (double[]) columns[column]; + return doubleData[row]; + } + String str = getString(row, column); + if (str == null || str.equals(missingString)) { + return missingDouble; + } + try { + return Double.parseDouble(str); + } catch (NumberFormatException nfe) { + return missingDouble; + } + } + + + public double getDouble(int row, String columnName) { + return getDouble(row, getColumnIndex(columnName)); + } + + + public void setMissingDouble(double value) { + missingDouble = value; + } + + + public void setDouble(int row, int column, double what) { + if (columnTypes[column] == STRING) { + setString(row, column, String.valueOf(what)); + + } else { + checkSize(row, column); + if (columnTypes[column] != DOUBLE) { + throw new IllegalArgumentException("Column " + column + " is not a 'double' column."); + } + double[] doubleData = (double[]) columns[column]; + doubleData[row] = what; + } + } + + + public double[] getDoubleColumn(String name) { + int col = getColumnIndex(name); + return (col == -1) ? null : getDoubleColumn(col); + } + + + public double[] getDoubleColumn(int col) { + double[] outgoing = new double[rowCount]; + for (int row = 0; row < rowCount; row++) { + outgoing[row] = getDouble(row, col); + } + return outgoing; + } + + + public double[] getDoubleRow(int row) { + double[] outgoing = new double[columns.length]; + for (int col = 0; col < columns.length; col++) { + outgoing[col] = getDouble(row, col); + } + return outgoing; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + +//public long getTimestamp(String rowName, int column) { +//return getTimestamp(getRowIndex(rowName), column); +//} + + + /** + * Returns the time in milliseconds by parsing a SQL Timestamp at this cell. + */ +// public long getTimestamp(int row, int column) { +// String str = getString(row, column); +// java.sql.Timestamp timestamp = java.sql.Timestamp.valueOf(str); +// return timestamp.getTime(); +// } + + +// public long getExcelTimestamp(int row, int column) { +// return parseExcelTimestamp(getString(row, column)); +// } + + +// static protected DateFormat excelDateFormat; + +// static public long parseExcelTimestamp(String timestamp) { +// if (excelDateFormat == null) { +// excelDateFormat = new SimpleDateFormat("MM/dd/yy HH:mm"); +// } +// try { +// return excelDateFormat.parse(timestamp).getTime(); +// } catch (ParseException e) { +// e.printStackTrace(); +// return -1; +// } +// } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + +// public void setObject(int row, int column, Object value) { +// if (value == null) { +// data[row][column] = null; +// } else if (value instanceof String) { +// setString(row, column, (String) value); +// } else if (value instanceof Float) { +// setFloat(row, column, ((Float) value).floatValue()); +// } else if (value instanceof Integer) { +// setInt(row, column, ((Integer) value).intValue()); +// } else { +// setString(row, column, value.toString()); +// } +// } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Get a String value from the table. If the row is longer than the table + * @param row + * @param col + * @return + */ + public String getString(int row, int col) { + checkBounds(row, col); + if (columnTypes[col] == STRING) { + String[] stringData = (String[]) columns[col]; + return stringData[row]; + } else if (columnTypes[col] == CATEGORICAL) { + int index = getInt(row, col); + return columnCategories[col].key(index); + } else { + return String.valueOf(Array.get(columns[col], row)); + } + } + + + public String getString(int row, String columnName) { + return getString(row, getColumnIndex(columnName)); + } + + + public void setMissingString(String value) { + missingString = value; + } + + + public void setString(int row, int column, String what) { + checkSize(row, column); + if (columnTypes[column] != STRING) { + throw new IllegalArgumentException("Column " + column + " is not a String column."); + } + String[] stringData = (String[]) columns[column]; + stringData[row] = what; + } + + + public void setString(int row, String columnName, String what) { + int column = getColumnIndex(columnName); + setString(row, column, what); + } + + + public String[] getStringColumn(String name) { + int col = getColumnIndex(name); + return (col == -1) ? null : getStringColumn(col); + } + + + public String[] getStringColumn(int col) { + String[] outgoing = new String[rowCount]; + for (int i = 0; i < rowCount; i++) { + outgoing[i] = getString(i, col); + } + return outgoing; + } + + + public String[] getStringRow(int row) { + String[] outgoing = new String[columns.length]; + for (int col = 0; col < columns.length; col++) { + outgoing[col] = getString(row, col); + } + return outgoing; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Set all 'null' entries to "" (zero length String objects). + * If columns are typed, then this will only apply to String columns. + */ + public void makeNullEmpty() { + for (int col = 0; col < columns.length; col++) { + if (columnTypes[col] == STRING) { + String[] stringData = (String[]) columns[col]; + for (int row = 0; row < rowCount; row++) { + if (stringData[row] == null) { + stringData[row] = ""; + } + } + } + } + } + + + /** + * Set all "" entries (zero length String objects) to null values. + * If columns are typed, then this will only apply to String columns. + */ + public void makeEmptyNull() { + for (int col = 0; col < columns.length; col++) { + if (columnTypes[col] == STRING) { + String[] stringData = (String[]) columns[col]; + for (int row = 0; row < rowCount; row++) { + if (stringData[row] != null && stringData[row].length() == 0) { + stringData[row] = null; + } + } + } + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Searches the entire table for float values. + * Returns missing float (Float.NaN by default) if no valid numbers found. + */ + public float getMaxFloat() { + boolean found = false; + float max = PConstants.MIN_FLOAT; + for (int row = 0; row < getRowCount(); row++) { + for (int col = 0; col < getColumnCount(); col++) { + float value = getFloat(row, col); + if (!Float.isNaN(value)) { // TODO no, this should be comparing to the missing value + if (!found) { + max = value; + found = true; + } else if (value > max) { + max = value; + } + } + } + } + return found ? max : missingFloat; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Remove any of the specified characters from the entire table. + */ + public void removeTokens(String tokens) { + for (int col = 0; col < getColumnCount(); col++) { + removeTokens(tokens, col); + } + } + + + /** + * Removed any of the specified characters from a column. For instance, + * the following code removes dollar signs and commas from column 2: + *
+   * table.removeTokens(",$", 2);
+   * 
+ */ + public void removeTokens(String tokens, int column) { + for (int row = 0; row < rowCount; row++) { + String s = getString(row, column); + if (s != null) { + char[] c = s.toCharArray(); + int index = 0; + for (int j = 0; j < c.length; j++) { + if (tokens.indexOf(c[j]) == -1) { + if (index != j) { + c[index] = c[j]; + } + index++; + } + } + if (index != c.length) { + setString(row, column, new String(c, 0, index)); + } + } + } + } + + + public void removeTokens(String tokens, String column) { + removeTokens(tokens, getColumnIndex(column)); + } + + + // TODO this isn't i18n correct, and it's a dumb implementation +// public void removeLetters(int column) { +// String alphabet = "abcdefghijklmnopqrstuvwxyz"; +// removeTokens(alphabet + alphabet.toUpperCase(), column); +// } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Return the row that contains the first String that matches. + * @param what the String to match + * @param column the column to search + */ + public int findRow(String what, int column) { + checkBounds(-1, column); + if (columnTypes[column] == STRING) { + String[] stringData = (String[]) columns[column]; + if (what == null) { + for (int row = 0; row < rowCount; row++) { + if (stringData[row] == null) return row; + } + } else { + for (int row = 0; row < rowCount; row++) { + if (stringData[row] != null && stringData[row].equals(what)) { + return row; + } + } + } + } else { // less efficient, includes conversion as necessary + for (int row = 0; row < rowCount; row++) { + String str = getString(row, column); + if (str == null) { + if (what == null) { + return row; + } + } else if (str.equals(what)) { + return row; + } + } + } + return -1; + } + + + /** + * Return the row that contains the first String that matches. + * @param what the String to match + * @param columnName the column to search + */ + public int findRow(String what, String columnName) { + return findRow(what, getColumnIndex(columnName)); + } + + + /** + * Return a list of rows that contain the String passed in. If there are no + * matches, a zero length array will be returned (not a null array). + * @param what the String to match + * @param column the column to search + */ + public int[] findRows(String what, int column) { + int[] outgoing = new int[rowCount]; + int count = 0; + + checkBounds(-1, column); + if (columnTypes[column] == STRING) { + String[] stringData = (String[]) columns[column]; + if (what == null) { + for (int row = 0; row < rowCount; row++) { + if (stringData[row] == null) { + outgoing[count++] = row; + } + } + } else { + for (int row = 0; row < rowCount; row++) { + if (stringData[row] != null && stringData[row].equals(what)) { + outgoing[count++] = row; + } + } + } + } else { // less efficient, includes conversion as necessary + for (int row = 0; row < rowCount; row++) { + String str = getString(row, column); + if (str == null) { + if (what == null) { + outgoing[count++] = row; + } + } else if (str.equals(what)) { + outgoing[count++] = row; + } + } + } + return PApplet.subset(outgoing, 0, count); + } + + + /** + * Return a list of rows that contain the String passed in. If there are no + * matches, a zero length array will be returned (not a null array). + * @param what the String to match + * @param columnName the column to search + */ + public int[] findRows(String what, String columnName) { + return findRows(what, getColumnIndex(columnName)); + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Return the row that contains the first String that matches. + * @param what the String to match + * @param column the column to search + */ + public int matchRow(String regexp, int column) { + checkBounds(-1, column); + if (columnTypes[column] == STRING) { + String[] stringData = (String[]) columns[column]; + for (int row = 0; row < rowCount; row++) { + if (stringData[row] != null && + PApplet.match(stringData[row], regexp) != null) { + return row; + } + } + } else { // less efficient, includes conversion as necessary + for (int row = 0; row < rowCount; row++) { + String str = getString(row, column); + if (str != null && + PApplet.match(str, regexp) != null) { + return row; + } + } + } + return -1; + } + + + /** + * Return the row that contains the first String that matches. + * @param what the String to match + * @param columnName the column to search + */ + public int matchRow(String what, String columnName) { + return matchRow(what, getColumnIndex(columnName)); + } + + + /** + * Return a list of rows that contain the String passed in. If there are no + * matches, a zero length array will be returned (not a null array). + * @param what the String to match + * @param column the column to search + */ + public int[] matchRows(String regexp, int column) { + int[] outgoing = new int[rowCount]; + int count = 0; + + checkBounds(-1, column); + if (columnTypes[column] == STRING) { + String[] stringData = (String[]) columns[column]; + for (int row = 0; row < rowCount; row++) { + if (stringData[row] != null && + PApplet.match(stringData[row], regexp) != null) { + outgoing[count++] = row; + } + } + } else { // less efficient, includes conversion as necessary + for (int row = 0; row < rowCount; row++) { + String str = getString(row, column); + if (str != null && + PApplet.match(str, regexp) != null) { + outgoing[count++] = row; + } + } + } + return PApplet.subset(outgoing, 0, count); + } + + + /** + * Return a list of rows that match the regex passed in. If there are no + * matches, a zero length array will be returned (not a null array). + * @param what the String to match + * @param columnName the column to search + */ + public int[] matchRows(String what, String columnName) { + return matchRows(what, getColumnIndex(columnName)); + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Return a list of rows that contain the String passed in. If there are no + * matches, a zero length array will be returned (not a null array). + * @param what the String to match + * @param column the column to search + */ + public void replaceAll(String regex, String replacement, int column) { + checkBounds(-1, column); + if (columnTypes[column] == STRING) { + String[] stringData = (String[]) columns[column]; + for (int row = 0; row < rowCount; row++) { + if (stringData[row] != null) { + stringData[row] = stringData[row].replaceAll(regex, replacement); + } + } + } else { + throw new IllegalArgumentException("replaceAll() can only be used on String columns"); + } + } + + + /** + * Run String.replaceAll() on all entries in a column. + * Only works with columns that are already String values. + * @param what the String to match + * @param columnName the column to search + */ + public void replaceAll(String regex, String replacement, String columnName) { + replaceAll(regex, replacement, getColumnIndex(columnName)); + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + protected void checkColumn(int col) { + if (col >= columns.length) { + setColumnCount(col + 1); + } + } + + + protected void checkRow(int row) { + if (row >= rowCount) { + setRowCount(row + 1); + } + } + + + protected void checkSize(int row, int col) { + checkRow(row); + checkColumn(col); + } + + + protected void checkBounds(int row, int column) { + if (row < 0 || row >= rowCount) { + throw new ArrayIndexOutOfBoundsException("Row " + row + " does not exist."); + } + if (column < 0 || column >= columns.length) { + throw new ArrayIndexOutOfBoundsException("Column " + column + " does not exist."); + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public Table createSubset(int[] rowSubset) { + Table newbie = new Table(); + newbie.setColumnTitles(columnTitles); // also sets columns.length + newbie.columnTypes = columnTypes; + newbie.setRowCount(rowSubset.length); + + for (int i = 0; i < rowSubset.length; i++) { + int row = rowSubset[i]; + for (int col = 0; col < columns.length; col++) { + switch (columnTypes[col]) { + case STRING: newbie.setString(i, col, getString(row, col)); break; + case INT: newbie.setInt(i, col, getInt(row, col)); break; + case LONG: newbie.setLong(i, col, getLong(row, col)); break; + case FLOAT: newbie.setFloat(i, col, getFloat(row, col)); break; + case DOUBLE: newbie.setDouble(i, col, getDouble(row, col)); break; + } + } + } + return newbie; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public String[] getUniqueEntries(String column) { + return getUniqueEntries(getColumnIndex(column)); + } + + + class HashMapBlows { + HashMap dataToIndex = new HashMap(); + ArrayList indexToData = new ArrayList(); + + int index(String key) { + Integer value = dataToIndex.get(key); + if (value != null) { + return value; + } + + int v = dataToIndex.size(); + dataToIndex.put(key, v); + indexToData.add(key); + return v; + } + + String key(int index) { + return indexToData.get(index); + } + + int size() { + return dataToIndex.size(); + } + + void write(DataOutputStream output) throws IOException { + output.writeInt(size()); + for (String str : indexToData) { + output.writeUTF(str); + } + } + + void writeln(PrintWriter writer) throws IOException { + for (String str : indexToData) { + writer.println(str); + } + writer.flush(); + writer.close(); + } + + void read(DataInputStream input) throws IOException { + int count = input.readInt(); + dataToIndex = new HashMap(count); + for (int i = 0; i < count; i++) { + String str = input.readUTF(); + dataToIndex.put(str, i); + indexToData.add(str); + } + } + } + +// class HashMapBlows extends HashMap { +// +// int index(String what) { +// Integer value = get(what); +// if (value != null) { +// return value; +// } +// +// int v = size(); +// put(what, v); +// return v; +// } +// } + + + class HashMapSucks extends HashMap { + + void increment(String what) { + Integer value = get(what); + if (value == null) { + put(what, 1); + } else { + put(what, value + 1); + } + } + + void check(String what) { + if (get(what) == null) { + put(what, 0); + } + } + } + + + public String[] getUniqueEntries(int column) { + HashMapSucks found = new HashMapSucks(); + for (int row = 0; row < getRowCount(); row++) { + found.check(getString(row, column)); + } + String[] outgoing = new String[found.size()]; + found.keySet().toArray(outgoing); + return outgoing; + } + + + public HashMap getStringCount(String columnName) { + return getStringCount(getColumnIndex(columnName)); + } + + + public HashMap getStringCount(int column) { + HashMapSucks outgoing = new HashMapSucks(); + for (int row = 0; row < rowCount; row++) { + String entry = getString(row, column); + if (entry != null) { + outgoing.increment(entry); + } + } + return outgoing; + } + + + /** + * Return an object that maps the String values in one column back to the + * row from which they came. For instance, if the "name" of each row is + * found in the first column, getColumnRowLookup(0) would return an object + * that would map each name back to its row. + */ + public HashMap getRowLookup(int col) { + HashMap outgoing = new HashMap(); + for (int row = 0; row < getRowCount(); row++) { + outgoing.put(getString(row, col), row); + } + return outgoing; + } + + + // incomplete, basically this is silly to write all this repetitive code when + // it can be implemented in ~3 lines of code... +// /** +// * Return an object that maps the data from one column to the data of found +// * in another column. +// */ +// public HashMap getLookup(int col1, int col2) { +// HashMap outgoing = null; +// +// switch (columnTypes[col1]) { +// case INT: { +// if (columnTypes[col2] == INT) { +// outgoing = new HashMap(); +// for (int row = 0; row < getRowCount(); row++) { +// outgoing.put(getInt(row, col1), getInt(row, col2)); +// } +// } else if (columnTypes[col2] == LONG) { +// outgoing = new HashMap(); +// for (int row = 0; row < getRowCount(); row++) { +// outgoing.put(getInt(row, col1), getLong(row, col2)); +// } +// } else if (columnTypes[col2] == FLOAT) { +// outgoing = new HashMap(); +// for (int row = 0; row < getRowCount(); row++) { +// outgoing.put(getInt(row, col1), getFloat(row, col2)); +// } +// } else if (columnTypes[col2] == DOUBLE) { +// outgoing = new HashMap(); +// for (int row = 0; row < getRowCount(); row++) { +// outgoing.put(getInt(row, col1), getDouble(row, col2)); +// } +// } else if (columnTypes[col2] == STRING) { +// outgoing = new HashMap(); +// for (int row = 0; row < getRowCount(); row++) { +// outgoing.put(getInt(row, col1), getString(row, col2)); +// } +// } +// break; +// } +// } +// return outgoing; +// } + + + // public StringIntPairs getColumnRowLookup(int col) { +// StringIntPairs sc = new StringIntPairs(); +// String[] column = getStringColumn(col); +// for (int i = 0; i < column.length; i++) { +// sc.set(column[i], i); +// } +// return sc; +// } + + +// public String[] getUniqueEntries(int column) { +//// HashMap indices = new HashMap(); +//// for (int row = 0; row < rowCount; row++) { +//// indices.put(data[row][column], this); // 'this' is a dummy +//// } +// StringIntPairs sc = getStringCount(column); +// return sc.keys(); +// } +// +// +// public StringIntPairs getStringCount(String columnName) { +// return getStringCount(getColumnIndex(columnName)); +// } +// +// +// public StringIntPairs getStringCount(int column) { +// StringIntPairs outgoing = new StringIntPairs(); +// for (int row = 0; row < rowCount; row++) { +// String entry = data[row][column]; +// if (entry != null) { +// outgoing.increment(entry); +// } +// } +// return outgoing; +// } +// +// +// /** +// * Return an object that maps the String values in one column back to the +// * row from which they came. For instance, if the "name" of each row is +// * found in the first column, getColumnRowLookup(0) would return an object +// * that would map each name back to its row. +// */ +// public StringIntPairs getColumnRowLookup(int col) { +// StringIntPairs sc = new StringIntPairs(); +// String[] column = getStringColumn(col); +// for (int i = 0; i < column.length; i++) { +// sc.set(column[i], i); +// } +// return sc; +// } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public void trim() { + for (int col = 0; col < columns.length; col++) { + String[] stringData = (String[]) columns[col]; + for (int row = 0; row < rowCount; row++) { + if (stringData[row] != null) { + stringData[row] = PApplet.trim(stringData[row]); + } + } + } + } +} diff --git a/android/todo.txt b/android/todo.txt index 49234d2f8..7d4249db8 100644 --- a/android/todo.txt +++ b/android/todo.txt @@ -6,14 +6,16 @@ X consider switch to android 2.2 as the minimum andres A GL2 specific code in Processing 2.0a5 break P3D on GLES2 hardware A http://code.google.com/p/processing/issues/detail?id=1029 -_ Android OPENGL renderer + JAVA2D PGraphics results in PTexture exception -_ http://code.google.com/p/processing/issues/detail?id=1019 X OpenGL/ES requires precision specifier on float types X http://code.google.com/p/processing/issues/detail?id=1035 +_ Android OPENGL renderer + JAVA2D PGraphics results in PTexture exception +_ http://code.google.com/p/processing/issues/detail?id=1019 earlier X remove unnecessary processing.xml.* code from android-core X http://code.google.com/p/processing/issues/detail?id=214 +X remove unnecessary processing.xml.* code from android-core +X http://code.google.com/p/processing/issues/detail?id=214 _ Android mode is broken on Windows in Processing 2.0a5 @@ -374,8 +376,6 @@ DL _ http://code.google.com/p/processing/issues/detail?id=244 EH _ separate "PApplet" into separate View and Activity classes EH _ http://code.google.com/p/processing/issues/detail?id=212 -EM _ remove unnecessary processing.xml.* code from android-core -EM _ http://code.google.com/p/processing/issues/detail?id=214 EM _ implement size() and createGraphics() for arbitrary renderers EM _ http://code.google.com/p/processing/issues/detail?id=241 diff --git a/core/src/processing/core/PApplet.java b/core/src/processing/core/PApplet.java index 716042a67..c05aa0276 100644 --- a/core/src/processing/core/PApplet.java +++ b/core/src/processing/core/PApplet.java @@ -5352,21 +5352,20 @@ public class PApplet extends Applet */ + ////////////////////////////////////////////////////////////// - // NODE I/O (XML, JSON, etc.) + // DATA I/O + + public XML loadXML(String filename) { return new XML(this, filename); } -// public PData loadData(String filename) { -// if (filename.toLowerCase().endsWith(".json")) { -// return new PData(this, filename); -// } else { -// throw new RuntimeException("filename used for loadNode() must end with XML"); -// } -// } + public Table loadTable(String filename) { + return new Table(this, filename); + } diff --git a/core/todo.txt b/core/todo.txt index 2286079ec..020aeb614 100644 --- a/core/todo.txt +++ b/core/todo.txt @@ -4,6 +4,10 @@ X http://code.google.com/p/processing/issues/detail?id=969 X also add support for printing arrays of longs (doubles were already there) X createGraphics for JAVA2D generates the wrong error msg w/ w/h <= 0 X http://code.google.com/p/processing/issues/detail?id=983 +X move to processing.data.* package +X update the internal code for Android and desktop to add the import +X add loadTable().. +X loadXML and loadTable are now in desktop fixed in 2.0a5 A OpenGL noSmooth() does not work @@ -47,8 +51,6 @@ o vectors shouldn't be exposed, need to expose attr lists as arrays o or also add a method for getting the vectors? X no longer an issue post-NanoXML X several other items under the LIBRARIES / XML section below -X move to processing.data.* package -X update the internal code for Android and desktop to add the import _ change screenWidth/Height to displayWidth/Height _ also fix the Android etc docs @@ -156,8 +158,6 @@ _ http://code.google.com/p/processing/issues/detail?id=509 _ add reference/docs for urlEncode() and urlDecode() -_ add loadTable().. - _ add() to add things to lists, sum() for the math (sum is used less after all) _ add inputPath() and outputPath() -> sketch folder or sd card