From 1560564819c8b1254ca4ad43487830a4296570f6 Mon Sep 17 00:00:00 2001 From: doug-martin Date: Mon, 2 Nov 2020 21:31:55 -0600 Subject: [PATCH] fix(formatter,#503): Do not ignore rows when headers is false * Fixes issue where row contents were ignored if the first row is empty and headers is false --- documentation/docs/formatting/options.md | 6 ++-- .../__tests__/formatter/RowFormatter.spec.ts | 6 ++++ .../format/__tests__/issues/issue503.spec.ts | 34 +++++++++++++++++++ packages/format/src/formatter/RowFormatter.ts | 16 ++++++--- 4 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 packages/format/__tests__/issues/issue503.spec.ts diff --git a/documentation/docs/formatting/options.md b/documentation/docs/formatting/options.md index 86a9b749..46bee03d 100644 --- a/documentation/docs/formatting/options.md +++ b/documentation/docs/formatting/options.md @@ -58,9 +58,9 @@ Set to `true` if you want the first character written to the stream to be a utf- **Type**: `null|boolean|string[]` **Default**: `null` If `true` then the headers will be auto detected from the first row. [Example](./examples.mdx#auto-discovery) - * If the row is a one-dimensional array then headers is a no-op - * If the row is an object then the keys will be used. - * If the row is an array of two element arrays (`[ ['header', 'column'], ['header2', 'column2'] ]`) then the first element in each array will be used. +* If the row is a one-dimensional array then the `headers` will be the first row processed +* If the row is an object then the keys will be used. +* If the row is an array of two element arrays (`[ ['header', 'column'], ['header2', 'column2'] ]`) then the first element in each array will be used. If there is not a headers row and you want to provide one then set to a `string[]`. [Example](./examples.mdx#provided-headers) diff --git a/packages/format/__tests__/formatter/RowFormatter.spec.ts b/packages/format/__tests__/formatter/RowFormatter.spec.ts index bcb97693..55e5756c 100644 --- a/packages/format/__tests__/formatter/RowFormatter.spec.ts +++ b/packages/format/__tests__/formatter/RowFormatter.spec.ts @@ -93,6 +93,12 @@ describe('RowFormatter', () => { const formatter = createFormatter({ headers: false }); await expect(formatRow(headerRow, formatter)).resolves.toEqual([headerRow.join(',')]); }); + + it('should still format all rows without headers', async () => { + const formatter = createFormatter({ headers: false }); + await expect(formatRow([], formatter)).resolves.toEqual(['']); + await expect(formatRow(headerRow, formatter)).resolves.toEqual([`\n${headerRow.join(',')}`]); + }); }); describe('with headers=true', () => { diff --git a/packages/format/__tests__/issues/issue503.spec.ts b/packages/format/__tests__/issues/issue503.spec.ts new file mode 100644 index 00000000..038160f4 --- /dev/null +++ b/packages/format/__tests__/issues/issue503.spec.ts @@ -0,0 +1,34 @@ +import { RecordingStream } from '../__fixtures__'; +import { RowArray, write } from '../../src'; + +describe('Issue #503 - https://github.com/C2FO/fast-csv/issues/503', () => { + it('should emit all columns after an empty row', () => { + return new Promise((res, rej) => { + const rs = new RecordingStream(); + const data: RowArray[] = [[], ['something']]; + + write(data, { quote: false, headers: false, writeHeaders: false }) + .pipe(rs) + .on('error', rej) + .on('finish', () => { + expect(rs.data).toEqual(['\nsomething']); + res(); + }); + }); + }); + + it('should not assume first row is a header if header = false', () => { + return new Promise((res, rej) => { + const rs = new RecordingStream(); + const data: RowArray[] = [['1'], [], ['1', '2', '3']]; + + write(data, { quote: false, headers: false, writeHeaders: false }) + .pipe(rs) + .on('error', rej) + .on('finish', () => { + expect(rs.data).toEqual(['1', '\n', '\n1,2,3']); + res(); + }); + }); + }); +}); diff --git a/packages/format/src/formatter/RowFormatter.ts b/packages/format/src/formatter/RowFormatter.ts index bb3a1f86..bb5066ab 100644 --- a/packages/format/src/formatter/RowFormatter.ts +++ b/packages/format/src/formatter/RowFormatter.ts @@ -9,16 +9,20 @@ type RowFormatterTransform = (row: I, cb: RowTrans type RowFormatterCallback = (error: Error | null, data?: RowArray) => void; export class RowFormatter { - private static isHashArray(row: Row): row is RowHashArray { + private static isRowHashArray(row: Row): row is RowHashArray { if (Array.isArray(row)) { return Array.isArray(row[0]) && row[0].length === 2; } return false; } + private static isRowArray(row: Row): row is RowArray { + return Array.isArray(row) && !this.isRowHashArray(row); + } + // get headers from a row item private static gatherHeaders(row: Row): string[] { - if (RowFormatter.isHashArray(row)) { + if (RowFormatter.isRowHashArray(row)) { // lets assume a multi-dimesional array with item 0 being the header return row.map((it): string => it[0]); } @@ -130,7 +134,6 @@ export class RowFormatter { // either the headers were provided by the user or we have already gathered them. return { shouldFormatColumns: true, headers: this.headers }; } - const headers = RowFormatter.gatherHeaders(row); this.headers = headers; this.fieldFormatter.headers = headers; @@ -151,7 +154,7 @@ export class RowFormatter { if (!Array.isArray(row)) { return this.headers.map((header): string => row[header] as string); } - if (RowFormatter.isHashArray(row)) { + if (RowFormatter.isRowHashArray(row)) { return this.headers.map((header, i): string => { const col = (row[i] as unknown) as string; if (col) { @@ -160,6 +163,11 @@ export class RowFormatter { return ''; }); } + // if its a one dimensional array and headers were not provided + // then just return the row + if (RowFormatter.isRowArray(row) && !this.shouldWriteHeaders) { + return row; + } return this.headers.map((header, i): string => row[i]); }