diff --git a/src/io/csv.js b/src/io/csv.js index 9dda273e8b..e573dd8fee 100644 --- a/src/io/csv.js +++ b/src/io/csv.js @@ -133,8 +133,9 @@ export function parse(csv, options, reviver = v => v) { } } - // flush the last value - if (ctx.entry.length !== 0) { + // flush the last value. In a one-column row, the pending value is still in + // ctx.value with ctx.entry empty, so checking the entry alone drops it. + if (state !== 0 || ctx.entry.length !== 0) { valueEnd(ctx); entryEnd(ctx); } diff --git a/src/io/files.js b/src/io/files.js index 612f44b905..3c4490384b 100644 --- a/src/io/files.js +++ b/src/io/files.js @@ -511,9 +511,10 @@ function files(p5, fn) { }); if (header) { - ret.columns = data.shift(); + ret.columns = data.shift() || []; } else { - ret.columns = Array(data[0].length).fill(null); + // An empty file parses to no rows at all. + ret.columns = Array(data.length ? data[0].length : 0).fill(null); } data.forEach(line => { diff --git a/test/unit/assets/csv_empty.csv b/test/unit/assets/csv_empty.csv new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/unit/assets/csv_one_column.csv b/test/unit/assets/csv_one_column.csv new file mode 100644 index 0000000000..afd0bc93b1 --- /dev/null +++ b/test/unit/assets/csv_one_column.csv @@ -0,0 +1,3 @@ +alice +bob +carol \ No newline at end of file diff --git a/test/unit/assets/csv_one_value.csv b/test/unit/assets/csv_one_value.csv new file mode 100644 index 0000000000..f70d7bba4a --- /dev/null +++ b/test/unit/assets/csv_one_value.csv @@ -0,0 +1 @@ +42 \ No newline at end of file diff --git a/test/unit/io/csv.js b/test/unit/io/csv.js new file mode 100644 index 0000000000..a708066933 --- /dev/null +++ b/test/unit/io/csv.js @@ -0,0 +1,34 @@ +import { parse } from '../../../src/io/csv'; + +suite('csv parse', function () { + test('keeps the last row of a one-column file without a trailing newline', function () { + expect(parse('a\nb')).toEqual([['a'], ['b']]); + }); + + test('parses a single value as one row', function () { + expect(parse('only')).toEqual([['only']]); + expect(parse('42')).toEqual([['42']]); + }); + + test('parses a single quoted value as one row', function () { + expect(parse('"only"')).toEqual([['only']]); + }); + + test('keeps a row after an empty line', function () { + expect(parse('a\n\nb')).toEqual([['a'], [''], ['b']]); + }); + + test('does not add a row for a trailing newline', function () { + expect(parse('a\n')).toEqual([['a']]); + expect(parse('x,y\n1,2\n')).toEqual([['x', 'y'], ['1', '2']]); + }); + + test('keeps multi-column rows and trailing empty fields', function () { + expect(parse('x,y\n1,2')).toEqual([['x', 'y'], ['1', '2']]); + expect(parse('x,')).toEqual([['x', '']]); + }); + + test('returns no rows for an empty string', function () { + expect(parse('')).toEqual([]); + }); +}); diff --git a/test/unit/io/loadTable.js b/test/unit/io/loadTable.js index 819e4d4f16..0cddb58877 100644 --- a/test/unit/io/loadTable.js +++ b/test/unit/io/loadTable.js @@ -72,6 +72,23 @@ suite('loadTable', function () { assert.strictEqual(table.getRow(1).getNum(1), 31); }); + test('keeps the last row of a one-column file without a trailing newline', async () => { + const table = await mockP5Prototype.loadTable('/test/unit/assets/csv_one_column.csv'); + assert.equal(table.getRowCount(), 3); + assert.strictEqual(table.getRow(2).getString(0), 'carol'); + }); + + test('loads a file holding a single value', async () => { + const table = await mockP5Prototype.loadTable('/test/unit/assets/csv_one_value.csv'); + assert.equal(table.getRowCount(), 1); + assert.strictEqual(table.getRow(0).getNum(0), 42); + }); + + test('loads an empty file as an empty table', async () => { + const table = await mockP5Prototype.loadTable('/test/unit/assets/csv_empty.csv'); + assert.equal(table.getRowCount(), 0); + }); + test('using the header option works', async () => { const table = await mockP5Prototype.loadTable(validFile, ',', true); assert.equal(table.getRowCount(), 3);