Skip to content

Commit

Permalink
Merge pull request #304 from C2FO/issue300
Browse files Browse the repository at this point in the history
Added alwaysWriteHeaders option for #300
  • Loading branch information
doug-martin authored Dec 20, 2019
2 parents e8d1ebb + 7f22c2a commit 77a4fee
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 40 deletions.
1 change: 1 addition & 0 deletions History.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

* [ADDED] Ability to Transform Header [#287](https://github.com/C2FO/fast-csv/issues/287)
* [ADDED] Example require and import to README [#301](https://github.com/C2FO/fast-csv/issues/301)
* [ADDED] Added new formatting option `alwaysWriteHeaders` to always write headers even if no rows are provided [#300](https://github.com/C2FO/fast-csv/issues/300)

# v3.6.0

Expand Down
2 changes: 2 additions & 0 deletions docs/formatting.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
* If there is not a headers row and you want to provide one then set to a `string[]`
* **NOTE** If the row is an object the headers must match fields in the object, otherwise you will end up with empty fields
* **NOTE** If there are more headers than columns then additional empty columns will be added
* `alwaysWriteHeaders: {boolean} = false`: Set to true if you always want headers written, even if no rows are written.
* **NOTE** This will throw an error if headers are not specified as an array.
* `quoteColumns: {boolean|boolean[]|{[string]: boolean} = false`
* If `true` then columns and headers will be quoted (unless `quoteHeaders` is specified).
* If it is an object then each key that has a true value will be quoted ((unless `quoteHeaders` is specified)
Expand Down
15 changes: 11 additions & 4 deletions src/formatter/CsvFormatterStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,16 @@ export default class CsvFormatterStream extends Transform {
}

public _flush(cb: TransformCallback): void {
if (this.formatterOptions.includeEndRowDelimiter) {
this.push(this.formatterOptions.rowDelimiter);
}
cb();
this.rowFormatter.finish((err, rows): void => {
if (err) {
return cb(err);
}
if (rows) {
rows.forEach((r): void => {
this.push(Buffer.from(r, 'utf8'));
});
}
return cb();
});
}
}
3 changes: 3 additions & 0 deletions src/formatter/FormatterOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface FormatterOptionsArgs {
includeEndRowDelimiter?: boolean;
writeBOM?: boolean;
transform?: RowTransformFunction;
alwaysWriteHeaders?: boolean;
}

export class FormatterOptions {
Expand Down Expand Up @@ -49,6 +50,8 @@ export class FormatterOptions {

public readonly BOM: string = '\ufeff';

public readonly alwaysWriteHeaders: boolean = false;

public constructor(opts: FormatterOptionsArgs = {}) {
Object.assign(this, opts || {});

Expand Down
15 changes: 15 additions & 0 deletions src/formatter/formatter/RowFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@ export default class RowFormatter {
});
}

public finish(cb: RowFormatterCallback): void {
const rows = [];
// check if we should write headers and we didnt get any rows
if (this.formatterOptions.alwaysWriteHeaders && this.rowCount === 0) {
if (!this.headers) {
return cb(new Error('`alwaysWriteHeaders` option is set to true but `headers` option not provided.'));
}
rows.push(this.formatColumns(this.headers, true));
}
if (this.formatterOptions.includeEndRowDelimiter) {
rows.push(this.formatterOptions.rowDelimiter);
}
return cb(null, rows);
}

// check if we need to write header return true if we should also write a row
// could be false if headers is true and the header row(first item) is passed in
private checkHeaders(row: Row): { headers?: string[] | null; shouldFormatColumns: boolean } {
Expand Down
74 changes: 74 additions & 0 deletions test/formatter/CsvFormatterStream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,80 @@ describe('CsvFormatterStream', () => {
});
});

describe('header option', () => {
it('should write an array of objects without headers', () =>
formatRows(objectRows, { headers: false }).then(formatted =>
assert.deepStrictEqual(formatted, ['a1,b1', '\na2,b2']),
));

it('should write an array of objects with headers', () =>
formatRows(objectRows, { headers: true }).then(formatted =>
assert.deepStrictEqual(formatted, ['a,b', '\na1,b1', '\na2,b2']),
));

it('should write an array of arrays without headers', () => {
const rows = [
['a1', 'b1'],
['a2', 'b2'],
];
return formatRows(rows, { headers: false }).then(formatted =>
assert.deepStrictEqual(formatted, ['a1,b1', '\na2,b2']),
);
});

it('should write an array of arrays with headers', () =>
formatRows(arrayRows, { headers: true }).then(parsedCsv =>
assert.deepStrictEqual(parsedCsv, ['a,b', '\na1,b1', '\na2,b2']),
));

it('should write an array of multi-dimensional arrays without headers', () =>
formatRows(multiDimensionalRows, { headers: false }).then(parsedCsv =>
assert.deepStrictEqual(parsedCsv, ['a1,b1', '\na2,b2']),
));

it('should write an array of multi-dimensional arrays with headers', () =>
formatRows(multiDimensionalRows, { headers: true }).then(parsedCsv =>
assert.deepStrictEqual(parsedCsv, ['a,b', '\na1,b1', '\na2,b2']),
));

it('should not write anything if headers are provided but no rows are provided', () =>
formatRows([], { headers: true }).then(parsedCsv => assert.deepStrictEqual(parsedCsv, [])));

describe('alwaysWriteHeaders option', () => {
it('should write the headers if rows are not provided', () => {
const headers = ['h1', 'h2'];
return formatRows([], { headers, alwaysWriteHeaders: true }).then(parsedCsv =>
assert.deepStrictEqual(parsedCsv, [headers.join(',')]),
);
});

it('should write the headers ones if rows are provided', () => {
const headers = ['h1', 'h2'];
return formatRows(arrayRows, { headers, alwaysWriteHeaders: true }).then(parsedCsv =>
assert.deepStrictEqual(parsedCsv, [headers.join(','), '\na,b', '\na1,b1', '\na2,b2']),
);
});

it('should fail if no headers are provided', () => {
return formatRows(arrayRows, { alwaysWriteHeaders: true }).catch(e =>
assert.strictEqual(
e.message,
'`alwaysWriteHeaders` option is set to true but `headers` option not provided.',
),
);
});

it('should write the headers and an endRowDelimiter if includeEndRowDelimiter is true', () => {
const headers = ['h1', 'h2'];
return formatRows([], {
headers,
includeEndRowDelimiter: true,
alwaysWriteHeaders: true,
}).then(parsedCsv => assert.deepStrictEqual(parsedCsv, [headers.join(','), '\n']));
});
});
});

it('should add a final rowDelimiter if includeEndRowDelimiter is true', () =>
formatRows(objectRows, { headers: true, includeEndRowDelimiter: true }).then(written =>
assert.deepStrictEqual(written, ['a,b', '\na1,b1', '\na2,b2', '\n']),
Expand Down
12 changes: 11 additions & 1 deletion test/formatter/FormatterOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,22 @@ describe('FormatterOptions', () => {
});

describe('#writeBOM', () => {
it('should set includeEndRowDelimiter to false by default', () => {
it('should set writeBOM to false by default', () => {
assert.strictEqual(createOptions().writeBOM, false);
});

it('should set to true if the writeBOM is specified', () => {
assert.strictEqual(createOptions({ writeBOM: true }).writeBOM, true);
});
});

describe('#alwaysWriteHeaders', () => {
it('should set alwaysWriteHeaders to false by default', () => {
assert.strictEqual(createOptions().alwaysWriteHeaders, false);
});

it('should set to provided value if the alwaysWriteHeaders is specified', () => {
assert.strictEqual(createOptions({ alwaysWriteHeaders: true }).alwaysWriteHeaders, true);
});
});
});
63 changes: 28 additions & 35 deletions test/formatter/formatter/FieldFormatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,109 +3,102 @@ import { FormatterOptionsArgs } from '../../../src';
import { FormatterOptions, FieldFormatter } from '../../../src/formatter';

describe('FieldFormatter', () => {
describe('#format', () => {
const createFormatter = (formatterOptions: FormatterOptionsArgs = {}, headers?: string[]) => {
const formatter = new FieldFormatter(new FormatterOptions(formatterOptions));
if (headers) {
formatter.headers = headers;
}
return formatter;
};

const formatField = (field: string, fieldIndex: number, isHeader: boolean, fieldFormatter: FieldFormatter) =>
fieldFormatter.format(field, fieldIndex, isHeader);
const createFormatter = (formatterOptions: FormatterOptionsArgs = {}) => {
return new FieldFormatter(new FormatterOptions(formatterOptions));
};

describe('#format', () => {
describe('header columns', () => {
it('should return the field not quoted if it contains no quotes', () => {
const formatter = createFormatter();
assert.strictEqual(formatField('header', 0, true, formatter), 'header');
assert.strictEqual(formatter.format('header', 0, true), 'header');
});

it('should quote the field and escape quotes if it contains a quote character', () => {
const formatter = createFormatter();
assert.strictEqual(formatField('hea"d"er', 0, true, formatter), '"hea""d""er"');
assert.strictEqual(formatter.format('hea"d"er', 0, true), '"hea""d""er"');
});

it('should quote the field and if it contains a rowDelimiter', () => {
const formatter = createFormatter({ rowDelimiter: '\r\n' });
assert.strictEqual(formatField('hea\r\nder', 0, true, formatter), '"hea\r\nder"');
assert.strictEqual(formatter.format('hea\r\nder', 0, true), '"hea\r\nder"');
});

it('should quote the field if quoteHeaders is true', () => {
const formatter = createFormatter({ quoteHeaders: true });
assert.strictEqual(formatField('header', 0, true, formatter), '"header"');
assert.strictEqual(formatter.format('header', 0, true), '"header"');
});

it('should quote the header if quote headers is an array and the index of the header is true in the quoteHeaders array', () => {
const formatter = createFormatter({ quoteHeaders: [true] });
assert.strictEqual(formatField('header', 0, true, formatter), '"header"');
assert.strictEqual(formatter.format('header', 0, true), '"header"');
});

it('should not quote the header if quote headers is an array and the index of the header is false in the quoteHeaders array', () => {
const formatter = createFormatter({ quoteHeaders: [false] });
assert.strictEqual(formatField('header', 0, true, formatter), 'header');
assert.strictEqual(formatter.format('header', 0, true), 'header');
});

it('should quote the header if quoteHeaders is an object and quoteHeaders object has true for the column name', () => {
const formatter = createFormatter({ quoteHeaders: { header: true } }, ['header']);
assert.strictEqual(formatField('header', 0, true, formatter), '"header"');
const formatter = createFormatter({ quoteHeaders: { header: true }, headers: ['header'] });
assert.strictEqual(formatter.format('header', 0, true), '"header"');
});

it('should not quote the header if quoteHeaders is an object and quoteHeaders object has false for the column nam', () => {
const formatter = createFormatter({ quoteHeaders: { header: false } }, ['header']);
assert.strictEqual(formatField('header', 0, true, formatter), 'header');
const formatter = createFormatter({ quoteHeaders: { header: false }, headers: ['header'] });
assert.strictEqual(formatter.format('header', 0, true), 'header');
});

it('should not quote the header if quoteHeaders is an object and quoteHeaders object does not contain the header', () => {
const formatter = createFormatter({ quoteHeaders: { header2: true } }, ['header']);
assert.strictEqual(formatField('header', 0, true, formatter), 'header');
const formatter = createFormatter({ quoteHeaders: { header2: true }, headers: ['header'] });
assert.strictEqual(formatter.format('header', 0, true), 'header');
});
});

describe('non-header columns', () => {
it('should return the field not quoted if it contains no quotes', () => {
const formatter = createFormatter();
assert.strictEqual(formatField('col', 0, false, formatter), 'col');
assert.strictEqual(formatter.format('col', 0, false), 'col');
});

it('should quote the field and escape quotes if it contains a quote character', () => {
const formatter = createFormatter();
assert.strictEqual(formatField('c"o"l', 0, false, formatter), '"c""o""l"');
assert.strictEqual(formatter.format('c"o"l', 0, false), '"c""o""l"');
});

it('should quote the field if it contains a rowDelimiter', () => {
const formatter = createFormatter({ rowDelimiter: '\r\n' });
assert.strictEqual(formatField('col\r\n', 0, false, formatter), '"col\r\n"');
assert.strictEqual(formatter.format('col\r\n', 0, false), '"col\r\n"');
});

it('should quote the field if quoteColumns is true', () => {
const formatter = createFormatter({ quoteColumns: true });
assert.strictEqual(formatField('col', 0, false, formatter), '"col"');
assert.strictEqual(formatter.format('col', 0, false), '"col"');
});

it('should quote the header if quote headers is an array and the index of the header is true in the quoteColumns array', () => {
const formatter = createFormatter({ quoteColumns: [true] });
assert.strictEqual(formatField('col', 0, false, formatter), '"col"');
assert.strictEqual(formatter.format('col', 0, false), '"col"');
});

it('should not quote the header if quote headers is an array and the index of the header is false in the quoteColumns array', () => {
const formatter = createFormatter({ quoteColumns: [false] });
assert.strictEqual(formatField('col', 0, false, formatter), 'col');
assert.strictEqual(formatter.format('col', 0, false), 'col');
});

it('should quote the header if quoteColumns is an object and quoteColumns object has true for the column name', () => {
const formatter = createFormatter({ quoteColumns: { header: true } }, ['header']);
assert.strictEqual(formatField('col', 0, false, formatter), '"col"');
const formatter = createFormatter({ quoteColumns: { header: true }, headers: ['header'] });
assert.strictEqual(formatter.format('col', 0, false), '"col"');
});

it('should not quote the header if quoteColumns is an object and quoteColumns object has false for the column nam', () => {
const formatter = createFormatter({ quoteColumns: { header: false } }, ['header']);
assert.strictEqual(formatField('col', 0, false, formatter), 'col');
const formatter = createFormatter({ quoteColumns: { header: false }, headers: ['header'] });
assert.strictEqual(formatter.format('col', 0, false), 'col');
});

it('should not quote the header if quoteColumns is an object and quoteColumns object does not contain the header', () => {
const formatter = createFormatter({ quoteColumns: { header2: true } }, ['header']);
assert.strictEqual(formatField('col', 0, false, formatter), 'col');
const formatter = createFormatter({ quoteColumns: { header2: true }, headers: ['header'] });
assert.strictEqual(formatter.format('col', 0, false), 'col');
});
});
});
Expand Down
48 changes: 48 additions & 0 deletions test/formatter/formatter/RowFormatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ describe('RowFormatter', () => {
});
});

const finish = (formatter: RowFormatter): Promise<Row> =>
new Promise((res, rej): void => {
formatter.finish((err, formatted): void => {
if (err) {
return rej(err);
}
return res(formatted);
});
});

describe('#format', () => {
describe('with array', () => {
const headerRow = ['a', 'b'];
Expand Down Expand Up @@ -306,6 +316,44 @@ describe('RowFormatter', () => {
});
});

describe('#finish', () => {
describe('alwaysWriteHeaders option', () => {
it('should return a headers row if no rows have been written', () => {
const headers = ['h1', 'h2'];
const formatter = createFormatter({ headers, alwaysWriteHeaders: true });
return finish(formatter).then(rows => assert.deepStrictEqual(rows, [headers.join(',')]));
});

it('should not return a headers row if rows have been written', () => {
const headers = ['h1', 'h2'];
const formatter = createFormatter({ headers, alwaysWriteHeaders: true });
return formatRow(['c1', 'c2'], formatter)
.then(rows => {
assert.deepStrictEqual(rows, ['h1,h2', '\nc1,c2']);
return finish(formatter);
})
.then(rows => assert.deepStrictEqual(rows, []));
});

it('should reject if headers are not specified', () => {
const formatter = createFormatter({ alwaysWriteHeaders: true });
return finish(formatter).catch(e =>
assert.strictEqual(
e.message,
'`alwaysWriteHeaders` option is set to true but `headers` option not provided.',
),
);
});
});

describe('includeEndRowDelimiter option', () => {
it('should write the endRowDelimiter if ', () => {
const formatter = createFormatter({ includeEndRowDelimiter: true });
return finish(formatter).then(rows => assert.deepStrictEqual(rows, ['\n']));
});
});
});

describe('#rowTransform', () => {
it('should throw an error if the transform is set and is not a function', () => {
const formatter = createFormatter();
Expand Down

0 comments on commit 77a4fee

Please sign in to comment.