From: Kevin Lamonte Date: Thu, 1 Aug 2019 21:11:57 +0000 (-0500) Subject: table CSV and text file X-Git-Tag: fanfix-swing-0.0.1~12^2~13^2~130 X-Git-Url: https://git.nikiroo.be/?a=commitdiff_plain;h=656c0dddc7c0faddd62d373f22916107d322429e;p=fanfix-swing.git table CSV and text file --- diff --git a/src/jexer/TTableWidget.java b/src/jexer/TTableWidget.java index 62679309..d8e01909 100644 --- a/src/jexer/TTableWidget.java +++ b/src/jexer/TTableWidget.java @@ -28,11 +28,17 @@ */ package jexer; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.List; import jexer.bits.CellAttributes; +import jexer.bits.StringUtils; import jexer.event.TKeypressEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; @@ -105,7 +111,7 @@ public class TTableWidget extends TWidget { /** * Extra columns to add. */ - private static final int EXTRA_COLUMNS = (DEBUG ? 10 * (8 + 1) : 0); + private static final int EXTRA_COLUMNS = (DEBUG ? 3 : 0); // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- @@ -608,25 +614,39 @@ public class TTableWidget extends TWidget { * @param y row relative to parent * @param width width of widget * @param height height of widget + * @param gridColumns number of columns in grid + * @param gridRows number of rows in grid */ public TTableWidget(final TWidget parent, final int x, final int y, - final int width, final int height) { + final int width, final int height, final int gridColumns, + final int gridRows) { super(parent, x, y, width, height); + /* + System.err.println("gridColumns " + gridColumns + + " gridRows " + gridRows); + */ + + if (gridColumns < 1) { + throw new IllegalArgumentException("Column count cannot be less " + + "than 1"); + } + if (gridRows < 1) { + throw new IllegalArgumentException("Row count cannot be less " + + "than 1"); + } + // Initialize the starting row and column. rows.add(new Row(0)); columns.add(new Column(0)); + assert (rows.get(0).height == 1); // Place a grid of cells that fit in this space. - int row = 0; - for (int i = 0; i < height + EXTRA_ROWS; i += rows.get(0).height) { - int column = 0; - for (int j = 0; j < width + EXTRA_COLUMNS; - j += columns.get(0).width) { - - Cell cell = new Cell(this, 0, 0, columns.get(0).width, - rows.get(0).height, column, row); + for (int row = 0; row < gridRows; row++) { + for (int column = 0; column < gridColumns; column++) { + Cell cell = new Cell(this, 0, 0, COLUMN_DEFAULT_WIDTH, 1, + column, row); if (DEBUG) { // For debugging: set a grid of cell index labels. @@ -634,23 +654,20 @@ public class TTableWidget extends TWidget { } rows.get(row).add(cell); columns.get(column).add(cell); - if ((i == 0) && - (j + columns.get(0).width < width + EXTRA_COLUMNS) - ) { + + if (columns.size() < gridColumns) { columns.add(new Column(column + 1)); } - column++; } - if (i + rows.get(0).height < height + EXTRA_ROWS) { + if (row < gridRows - 1) { rows.add(new Row(row + 1)); } - row++; } for (int i = 0; i < rows.size(); i++) { rows.get(i).setY(i + (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0)); } for (int j = 0; j < columns.size(); j++) { - columns.get(j).setX((j * COLUMN_DEFAULT_WIDTH) + + columns.get(j).setX((j * (COLUMN_DEFAULT_WIDTH + 1)) + (showRowLabels ? ROW_LABEL_WIDTH : 0)); } activate(columns.get(selectedColumn).get(selectedRow)); @@ -658,6 +675,23 @@ public class TTableWidget extends TWidget { alignGrid(); } + /** + * Public constructor. + * + * @param parent parent widget + * @param x column relative to parent + * @param y row relative to parent + * @param width width of widget + * @param height height of widget + */ + public TTableWidget(final TWidget parent, final int x, final int y, + final int width, final int height) { + + this(parent, x, y, width, height, + width / (COLUMN_DEFAULT_WIDTH + 1) + EXTRA_COLUMNS, + height + EXTRA_ROWS); + } + // ------------------------------------------------------------------------ // Event handlers --------------------------------------------------------- // ------------------------------------------------------------------------ @@ -1178,6 +1212,12 @@ public class TTableWidget extends TWidget { * Align the grid so that the selected cell is fully visible. */ private void alignGrid() { + + /* + System.err.println("alignGrid() # columns " + columns.size() + + " # rows " + rows.size()); + */ + int viewColumns = getWidth(); if (showRowLabels == true) { viewColumns -= ROW_LABEL_WIDTH; @@ -1342,6 +1382,67 @@ public class TTableWidget extends TWidget { } } + /** + * Save contents to file in CSV format. + * + * @param csvFile a File referencing the CSV data + * @throws IOException if a java.io operation throws + */ + public void loadCsvFile(final File csvFile) throws IOException { + BufferedReader reader = null; + + try { + reader = new BufferedReader(new FileReader(csvFile)); + + String line = null; + boolean first = true; + for (line = reader.readLine(); line != null; + line = reader.readLine()) { + + List list = StringUtils.fromCsv(line); + if (list.size() == 0) { + continue; + } + + if (list.size() > columns.size()) { + int n = list.size() - columns.size(); + for (int i = 0; i < n; i++) { + selectedColumn = columns.size() - 1; + insertColumnRight(selectedColumn); + } + } + assert (list.size() == columns.size()); + + if (first) { + // First row: just replace what is here. + selectedRow = 0; + first = false; + } else { + // All other rows: append to the end. + selectedRow = rows.size() - 1; + insertRowBelow(selectedRow); + selectedRow = rows.size() - 1; + } + for (int i = 0; i < list.size(); i++) { + rows.get(selectedRow).get(i).setText(list.get(i)); + } + + // TODO: detect header line + } + } finally { + if (reader != null) { + reader.close(); + } + } + + left = 0; + top = 0; + selectedRow = 0; + selectedColumn = 0; + alignGrid(); + activate(columns.get(selectedColumn).get(selectedRow)); + } + /** * Save contents to file in CSV format. * @@ -1349,7 +1450,23 @@ public class TTableWidget extends TWidget { * @throws IOException if a java.io operation throws */ public void saveToCsvFilename(final String filename) throws IOException { - // TODO + BufferedWriter writer = null; + + try { + writer = new BufferedWriter(new FileWriter(filename)); + for (Row row: rows) { + List list = new ArrayList(row.cells.size()); + for (Cell cell: row.cells) { + list.add(cell.getText()); + } + writer.write(StringUtils.toCsv(list)); + writer.write("\n"); + } + } finally { + if (writer != null) { + writer.close(); + } + } } /** @@ -1359,7 +1476,217 @@ public class TTableWidget extends TWidget { * @throws IOException if a java.io operation throws */ public void saveToTextFilename(final String filename) throws IOException { - // TODO + BufferedWriter writer = null; + + try { + writer = new BufferedWriter(new FileWriter(filename)); + + if ((topBorder == Border.SINGLE) && (leftBorder == Border.SINGLE)) { + // Emit top-left corner. + writer.write("\u250c"); + } + + if (topBorder == Border.SINGLE) { + int cellI = 0; + for (Cell cell: rows.get(0).cells) { + for (int i = 0; i < columns.get(cellI).width; i++) { + writer.write("\u2500"); + } + + if (columns.get(cellI).rightBorder == Border.SINGLE) { + if (cellI < columns.size() - 1) { + // Emit top tee. + writer.write("\u252c"); + } else { + // Emit top-right corner. + writer.write("\u2510"); + } + } + cellI++; + } + } + writer.write("\n"); + + int rowI = 0; + for (Row row: rows) { + + if (leftBorder == Border.SINGLE) { + // Emit left border. + writer.write("\u2502"); + } + + int cellI = 0; + for (Cell cell: row.cells) { + writer.write(String.format("%" + + columns.get(cellI).width + "s", cell.getText())); + + if (columns.get(cellI).rightBorder == Border.SINGLE) { + // Emit right border. + writer.write("\u2502"); + } + cellI++; + } + writer.write("\n"); + + if (row.bottomBorder == Border.NONE) { + // All done, move on to the next row. + continue; + } + + // Emit the bottom borders and intersections. + if ((leftBorder == Border.SINGLE) + && (row.bottomBorder != Border.NONE) + ) { + if (rowI < rows.size() - 1) { + if (row.bottomBorder == Border.SINGLE) { + // Emit left tee. + writer.write("\u251c"); + } else if (row.bottomBorder == Border.DOUBLE) { + // Emit left tee (double). + writer.write("\u255e"); + } else if (row.bottomBorder == Border.THICK) { + // Emit left tee (thick). + writer.write("\u251d"); + } + } + + if (rowI == rows.size() - 1) { + if (row.bottomBorder == Border.SINGLE) { + // Emit left bottom corner. + writer.write("\u2514"); + } else if (row.bottomBorder == Border.DOUBLE) { + // Emit left bottom corner (double). + writer.write("\u2558"); + } else if (row.bottomBorder == Border.THICK) { + // Emit left bottom corner (thick). + writer.write("\u2515"); + } + } + } + + cellI = 0; + for (Cell cell: row.cells) { + + for (int i = 0; i < columns.get(cellI).width; i++) { + if (row.bottomBorder == Border.SINGLE) { + writer.write("\u2500"); + } + if (row.bottomBorder == Border.DOUBLE) { + writer.write("\u2550"); + } + if (row.bottomBorder == Border.THICK) { + writer.write("\u2501"); + } + } + + if ((rowI < rows.size() - 1) + && (cellI == columns.size() - 1) + && (row.bottomBorder == Border.SINGLE) + && (columns.get(cellI).rightBorder == Border.SINGLE) + ) { + // Emit right tee. + writer.write("\u2524"); + } + if ((rowI < rows.size() - 1) + && (cellI == columns.size() - 1) + && (row.bottomBorder == Border.DOUBLE) + && (columns.get(cellI).rightBorder == Border.SINGLE) + ) { + // Emit right tee (double). + writer.write("\u2561"); + } + if ((rowI < rows.size() - 1) + && (cellI == columns.size() - 1) + && (row.bottomBorder == Border.THICK) + && (columns.get(cellI).rightBorder == Border.SINGLE) + ) { + // Emit right tee (thick). + writer.write("\u2525"); + } + if ((rowI == rows.size() - 1) + && (cellI == columns.size() - 1) + && (row.bottomBorder == Border.SINGLE) + && (columns.get(cellI).rightBorder == Border.SINGLE) + ) { + // Emit right bottom corner. + writer.write("\u2518"); + } + if ((rowI == rows.size() - 1) + && (cellI == columns.size() - 1) + && (row.bottomBorder == Border.DOUBLE) + && (columns.get(cellI).rightBorder == Border.SINGLE) + ) { + // Emit right bottom corner (double). + writer.write("\u255b"); + } + if ((rowI == rows.size() - 1) + && (cellI == columns.size() - 1) + && (row.bottomBorder == Border.THICK) + && (columns.get(cellI).rightBorder == Border.SINGLE) + ) { + // Emit right bottom corner (thick). + writer.write("\u2519"); + } + if ((rowI < rows.size() - 1) + && (cellI < columns.size() - 1) + && (row.bottomBorder == Border.SINGLE) + && (columns.get(cellI).rightBorder == Border.SINGLE) + ) { + // Emit intersection. + writer.write("\u253c"); + } + if ((rowI < rows.size() - 1) + && (cellI < columns.size() - 1) + && (row.bottomBorder == Border.DOUBLE) + && (columns.get(cellI).rightBorder == Border.SINGLE) + ) { + // Emit intersection (double). + writer.write("\u256a"); + } + if ((rowI < rows.size() - 1) + && (cellI < columns.size() - 1) + && (row.bottomBorder == Border.THICK) + && (columns.get(cellI).rightBorder == Border.SINGLE) + ) { + // Emit intersection (thick). + writer.write("\u253f"); + } + if ((rowI == rows.size() - 1) + && (cellI < columns.size() - 1) + && (row.bottomBorder == Border.SINGLE) + && (columns.get(cellI).rightBorder == Border.SINGLE) + ) { + // Emit bottom tee. + writer.write("\u2534"); + } + if ((rowI == rows.size() - 1) + && (cellI < columns.size() - 1) + && (row.bottomBorder == Border.DOUBLE) + && (columns.get(cellI).rightBorder == Border.SINGLE) + ) { + // Emit bottom tee (double). + writer.write("\u2567"); + } + if ((rowI == rows.size() - 1) + && (cellI < columns.size() - 1) + && (row.bottomBorder == Border.THICK) + && (columns.get(cellI).rightBorder == Border.SINGLE) + ) { + // Emit bottom tee (thick). + writer.write("\u2537"); + } + + cellI++; + } + + writer.write("\n"); + rowI++; + } + } finally { + if (writer != null) { + writer.close(); + } + } } /** @@ -1597,7 +1924,7 @@ public class TTableWidget extends TWidget { Row newRow = new Row(idx); for (int i = 0; i < columns.size(); i++) { Cell cell = new Cell(this, columns.get(i).getX(), - rows.get(selectedRow).getY(), COLUMN_DEFAULT_WIDTH, 1, i, idx); + rows.get(idx).getY(), COLUMN_DEFAULT_WIDTH, 1, i, idx); newRow.add(cell); columns.get(i).cells.add(idx, cell); } @@ -1628,7 +1955,7 @@ public class TTableWidget extends TWidget { throw new IndexOutOfBoundsException("Row count is " + rows.size() + ", requested index " + row); } - insertRowAt(selectedRow); + insertRowAt(row); selectedRow++; activate(columns.get(selectedColumn).get(selectedRow)); } @@ -1643,18 +1970,18 @@ public class TTableWidget extends TWidget { throw new IndexOutOfBoundsException("Row count is " + rows.size() + ", requested index " + row); } - int idx = selectedRow + 1; + int idx = row + 1; if (idx < rows.size()) { insertRowAt(idx); activate(columns.get(selectedColumn).get(selectedRow)); return; } - // selectedRow is the last row, we need to perform an append. + // row is the last row, we need to perform an append. Row newRow = new Row(idx); for (int i = 0; i < columns.size(); i++) { Cell cell = new Cell(this, columns.get(i).getX(), - rows.get(selectedRow).getY(), COLUMN_DEFAULT_WIDTH, 1, i, idx); + rows.get(row).getY(), COLUMN_DEFAULT_WIDTH, 1, i, idx); newRow.add(cell); columns.get(i).cells.add(cell); } @@ -1710,7 +2037,7 @@ public class TTableWidget extends TWidget { private void insertColumnAt(final int idx) { Column newColumn = new Column(idx); for (int i = 0; i < rows.size(); i++) { - Cell cell = new Cell(this, columns.get(selectedColumn).getX(), + Cell cell = new Cell(this, columns.get(idx).getX(), rows.get(i).getY(), COLUMN_DEFAULT_WIDTH, 1, idx, i); newColumn.add(cell); rows.get(i).cells.add(idx, cell); @@ -1742,7 +2069,7 @@ public class TTableWidget extends TWidget { throw new IndexOutOfBoundsException("Column count is " + columns.size() + ", requested index " + column); } - insertColumnAt(selectedColumn); + insertColumnAt(column); selectedColumn++; activate(columns.get(selectedColumn).get(selectedRow)); } @@ -1757,17 +2084,17 @@ public class TTableWidget extends TWidget { throw new IndexOutOfBoundsException("Column count is " + columns.size() + ", requested index " + column); } - int idx = selectedColumn + 1; + int idx = column + 1; if (idx < columns.size()) { insertColumnAt(idx); activate(columns.get(selectedColumn).get(selectedRow)); return; } - // selectedColumn is the last column, we need to perform an append. + // column is the last column, we need to perform an append. Column newColumn = new Column(idx); for (int i = 0; i < rows.size(); i++) { - Cell cell = new Cell(this, columns.get(selectedColumn).getX(), + Cell cell = new Cell(this, columns.get(column).getX(), rows.get(i).getY(), COLUMN_DEFAULT_WIDTH, 1, idx, i); newColumn.add(cell); rows.get(i).cells.add(cell); @@ -1933,7 +2260,14 @@ public class TTableWidget extends TWidget { if (selectedColumn == 0) { leftBorder = Border.NONE; } + if (selectedColumn > 0) { + columns.get(selectedColumn - 1).rightBorder = Border.NONE; + } columns.get(selectedColumn).rightBorder = Border.NONE; + if (selectedRow > 0) { + rows.get(selectedRow - 1).bottomBorder = Border.NONE; + rows.get(selectedRow - 1).height = 1; + } rows.get(selectedRow).bottomBorder = Border.NONE; rows.get(selectedRow).height = 1; bottomRightCorner(); @@ -1949,7 +2283,14 @@ public class TTableWidget extends TWidget { if (selectedColumn == 0) { leftBorder = Border.SINGLE; } + if (selectedColumn > 0) { + columns.get(selectedColumn - 1).rightBorder = Border.SINGLE; + } columns.get(selectedColumn).rightBorder = Border.SINGLE; + if (selectedRow > 0) { + rows.get(selectedRow - 1).bottomBorder = Border.SINGLE; + rows.get(selectedRow - 1).height = 2; + } rows.get(selectedRow).bottomBorder = Border.SINGLE; rows.get(selectedRow).height = 2; alignGrid(); diff --git a/src/jexer/TTableWindow.java b/src/jexer/TTableWindow.java index 88fca160..9365892f 100644 --- a/src/jexer/TTableWindow.java +++ b/src/jexer/TTableWindow.java @@ -28,6 +28,7 @@ */ package jexer; +import java.io.File; import java.io.IOException; import java.text.MessageFormat; import java.util.ResourceBundle; @@ -81,6 +82,26 @@ public class TTableWindow extends TScrollableWindow { setupAfterTable(); } + /** + * Public constructor loads a grid from a RFC4180 CSV file. + * + * @param parent the main application + * @param csvFile a File referencing the CSV data + * @throws IOException if a java.io operation throws + */ + public TTableWindow(final TApplication parent, + final File csvFile) throws IOException { + + super(parent, csvFile.getName(), 0, 0, + parent.getScreen().getWidth() / 2, + parent.getScreen().getHeight() / 2 - 2, + RESIZABLE | CENTERED); + + tableField = addTable(0, 0, getWidth() - 2, getHeight() - 2, 1, 1); + setupAfterTable(); + tableField.loadCsvFile(csvFile); + } + // ------------------------------------------------------------------------ // Event handlers --------------------------------------------------------- // ------------------------------------------------------------------------ @@ -307,10 +328,7 @@ public class TTableWindow extends TScrollableWindow { String filename = fileOpenBox("."); if (filename != null) { try { - // TODO - if (false) { - tableField.saveToCsvFilename(filename); - } + new TTableWindow(getApplication(), new File(filename)); } catch (IOException e) { messageBox(i18n.getString("errorDialogTitle"), MessageFormat.format(i18n. @@ -336,7 +354,8 @@ public class TTableWindow extends TScrollableWindow { */ @Override public void onMenu(final TMenuEvent menu) { - TInputBox inputBox; + TInputBox inputBox = null; + String filename = null; switch (menu.getId()) { case TMenu.MID_TABLE_RENAME_COLUMN: @@ -438,13 +457,46 @@ public class TTableWindow extends TScrollableWindow { tableField.getColumnWidth(tableField.getSelectedColumnNumber()) + 1); return; case TMenu.MID_TABLE_FILE_OPEN_CSV: - // TODO + try { + filename = fileOpenBox("."); + if (filename != null) { + try { + new TTableWindow(getApplication(), new File(filename)); + } catch (IOException e) { + messageBox(i18n.getString("errorDialogTitle"), + MessageFormat.format(i18n. + getString("errorReadingFile"), e.getMessage())); + } + } + } catch (IOException e) { + messageBox(i18n.getString("errorDialogTitle"), + MessageFormat.format(i18n. + getString("errorOpeningFileDialog"), e.getMessage())); + } return; case TMenu.MID_TABLE_FILE_SAVE_CSV: - // TODO + try { + filename = fileSaveBox("."); + if (filename != null) { + tableField.saveToCsvFilename(filename); + } + } catch (IOException e) { + messageBox(i18n.getString("errorDialogTitle"), + MessageFormat.format(i18n. + getString("errorWritingFile"), e.getMessage())); + } return; case TMenu.MID_TABLE_FILE_SAVE_TEXT: - // TODO + try { + filename = fileSaveBox("."); + if (filename != null) { + tableField.saveToTextFilename(filename); + } + } catch (IOException e) { + messageBox(i18n.getString("errorDialogTitle"), + MessageFormat.format(i18n. + getString("errorWritingFile"), e.getMessage())); + } return; default: break; diff --git a/src/jexer/TWidget.java b/src/jexer/TWidget.java index 72df0a2a..c9e0dedc 100644 --- a/src/jexer/TWidget.java +++ b/src/jexer/TWidget.java @@ -2272,4 +2272,22 @@ public abstract class TWidget implements Comparable { return new TTableWidget(this, x, y, width, height); } + /** + * Convenience function to add an editable 2D data table to this + * container/window. + * + * @param x column relative to parent + * @param y row relative to parent + * @param width width of widget + * @param height height of widget + * @param gridColumns number of columns in grid + * @param gridRows number of rows in grid + */ + public TTableWidget addTable(final int x, final int y, final int width, + final int height, final int gridColumns, final int gridRows) { + + return new TTableWidget(this, x, y, width, height, gridColumns, + gridRows); + } + } diff --git a/src/jexer/bits/StringUtils.java b/src/jexer/bits/StringUtils.java index 1a2079d3..a98756ed 100644 --- a/src/jexer/bits/StringUtils.java +++ b/src/jexer/bits/StringUtils.java @@ -29,7 +29,7 @@ package jexer.bits; import java.util.List; -import java.util.LinkedList; +import java.util.ArrayList; /** * StringUtils contains methods to: @@ -39,6 +39,8 @@ import java.util.LinkedList; * * - Unescape C0 control codes. * + * - Read/write a line of RFC4180 comma-separated values strings to/from a + * list of strings. */ public class StringUtils { @@ -50,7 +52,7 @@ public class StringUtils { * @return the list of lines */ public static List left(final String str, final int n) { - List result = new LinkedList(); + List result = new ArrayList(); /* * General procedure: @@ -137,7 +139,7 @@ public class StringUtils { * @return the list of lines */ public static List right(final String str, final int n) { - List result = new LinkedList(); + List result = new ArrayList(); /* * Same as left(), but preceed each line with spaces to make it n @@ -164,7 +166,7 @@ public class StringUtils { * @return the list of lines */ public static List center(final String str, final int n) { - List result = new LinkedList(); + List result = new ArrayList(); /* * Same as left(), but preceed/succeed each line with spaces to make @@ -196,7 +198,7 @@ public class StringUtils { * @return the list of lines */ public static List full(final String str, final int n) { - List result = new LinkedList(); + List result = new ArrayList(); /* * Same as left(), but insert spaces between words to make each line @@ -283,4 +285,123 @@ public class StringUtils { return sb.toString(); } + /** + * Read a line of RFC4180 comma-separated values (CSV) into a list of + * strings. + * + * @param line the CSV line, with or without without line terminators + * @return the list of strings + */ + public static List fromCsv(final String line) { + List result = new ArrayList(); + + StringBuilder str = new StringBuilder(); + boolean quoted = false; + boolean fieldQuoted = false; + + for (int i = 0; i < line.length(); i++) { + char ch = line.charAt(i); + + /* + System.err.println("ch '" + ch + "' str '" + str + "' " + + " fieldQuoted " + fieldQuoted + " quoted " + quoted); + */ + + if (ch == ',') { + if (fieldQuoted && quoted) { + // Terminating a quoted field. + result.add(str.toString()); + str = new StringBuilder(); + quoted = false; + fieldQuoted = false; + } else if (fieldQuoted) { + // Still waiting to see the terminating quote for this + // field. + str.append(ch); + } else if (quoted) { + // An unmatched double-quote and comma. This should be + // an invalid sequence. We will treat it as a quote + // terminating the field. + str.append('\"'); + result.add(str.toString()); + str = new StringBuilder(); + quoted = false; + fieldQuoted = false; + } else { + // A field separator. + result.add(str.toString()); + str = new StringBuilder(); + quoted = false; + fieldQuoted = false; + } + continue; + } + + if (ch == '\"') { + if ((str.length() == 0) && (!fieldQuoted)) { + // The opening quote to a quoted field. + fieldQuoted = true; + } else if (quoted) { + // This is a double-quote. + str.append('\"'); + quoted = false; + } else { + // This is the beginning of a quote. + quoted = true; + } + continue; + } + + // Normal character, pass it on. + str.append(ch); + } + + // Include the final field. + result.add(str.toString()); + + return result; + } + + /** + * Write a list of strings to on line of RFC4180 comma-separated values + * (CSV). + * + * @param list the list of strings + * @return the CSV line, without any line terminators + */ + public static String toCsv(final List list) { + StringBuilder result = new StringBuilder(); + int i = 0; + for (String str: list) { + + if (!str.contains("\"") && !str.contains(",")) { + // Just append the string with a comma. + result.append(str); + } else if (!str.contains("\"") && str.contains(",")) { + // Contains commas, but no quotes. Just double-quote it. + result.append("\""); + result.append(str); + result.append("\""); + } else if (str.contains("\"")) { + // Contains quotes and maybe commas. Double-quote it and + // replace quotes inside. + result.append("\""); + for (int j = 0; j < str.length(); j++) { + char ch = str.charAt(j); + result.append(ch); + if (ch == '\"') { + result.append("\""); + } + } + result.append("\""); + } + + if (i < list.size() - 1) { + result.append(","); + } + i++; + } + return result.toString(); + } + }