Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance improvements. #378

Merged
merged 1 commit into from
Apr 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions lib/JSON2CSVAsyncParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const { Transform } = require('stream');
const JSON2CSVTransform = require('./JSON2CSVTransform');
const { fastJoin } = require('./utils');

class JSON2CSVAsyncParser {
constructor(opts, transformOpts) {
Expand Down Expand Up @@ -40,10 +41,10 @@ class JSON2CSVAsyncParser {

promise() {
return new Promise((resolve, reject) => {
let csv = '';
let csvBuffer = [];
this.processor
.on('data', chunk => (csv += chunk.toString()))
.on('finish', () => resolve(csv))
.on('data', chunk => csvBuffer.push(chunk.toString()))
.on('finish', () => resolve(fastJoin(csvBuffer, '')))
.on('error', err => reject(err));
});
}
Expand Down
108 changes: 48 additions & 60 deletions lib/JSON2CSVBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const os = require('os');
const lodashGet = require('lodash.get');
const { setProp, flattenReducer } = require('./utils');
const { getProp, setProp, fastJoin, flattenReducer } = require('./utils');

class JSON2CSVBase {
constructor(opts) {
Expand All @@ -29,7 +29,7 @@ class JSON2CSVBase {
: '"';
processedOpts.doubleQuote = typeof processedOpts.doubleQuote === 'string'
? processedOpts.doubleQuote
: Array(3).join(processedOpts.quote);
: processedOpts.quote + processedOpts.quote;
processedOpts.header = processedOpts.header !== false;
processedOpts.includeEmptyRows = processedOpts.includeEmptyRows || false;
processedOpts.withBOM = processedOpts.withBOM || false;
Expand All @@ -49,7 +49,9 @@ class JSON2CSVBase {
if (typeof fieldInfo === 'string') {
return {
label: fieldInfo,
value: row => lodashGet(row, fieldInfo, this.opts.defaultValue),
value: (fieldInfo.includes('.') || fieldInfo.includes('['))
? row => lodashGet(row, fieldInfo, this.opts.defaultValue)
: row => getProp(row, fieldInfo, this.opts.defaultValue),
stringify: true,
};
}
Expand All @@ -62,7 +64,9 @@ class JSON2CSVBase {
if (typeof fieldInfo.value === 'string') {
return {
label: fieldInfo.label || fieldInfo.value,
value: row => lodashGet(row, fieldInfo.value, defaultValue),
value: (fieldInfo.value.includes('.') || fieldInfo.value.includes('['))
? row => lodashGet(row, fieldInfo.value, defaultValue)
: row => getProp(row, fieldInfo.value, defaultValue),
stringify: fieldInfo.stringify !== undefined ? fieldInfo.stringify : true,
};
}
Expand Down Expand Up @@ -93,9 +97,10 @@ class JSON2CSVBase {
* @returns {String} titles as a string
*/
getHeader() {
return this.opts.fields
.map(fieldInfo => this.processValue(fieldInfo.label, true))
.join(this.opts.delimiter);
return fastJoin(
this.opts.fields.map(fieldInfo => this.processValue(fieldInfo.label, true)),
this.opts.delimiter
);
}

memoizePreprocessRow() {
Expand Down Expand Up @@ -139,15 +144,20 @@ class JSON2CSVBase {
* @returns {String} CSV string (row)
*/
processRow(row) {
if (!row
|| (Object.getOwnPropertyNames(row).length === 0
&& !this.opts.includeEmptyRows)) {
if (!row) {
return undefined;
}

return this.opts.fields
.map(fieldInfo => this.processCell(row, fieldInfo))
.join(this.opts.delimiter);
const processedRow = this.opts.fields.map(fieldInfo => this.processCell(row, fieldInfo));

if (!this.opts.includeEmptyRows && processedRow.every(field => field === undefined)) {
return undefined;
}

return fastJoin(
processedRow,
this.opts.delimiter
);
}

/**
Expand All @@ -173,61 +183,39 @@ class JSON2CSVBase {
return undefined;
}

const isValueString = typeof value === 'string';
if (isValueString) {
value = value
.replace(/\n/g, '\u2028')
.replace(/\r/g, '\u2029')
.replace(/\t/g, '\u21E5');
}

//JSON.stringify('\\') results in a string with two backslash
//characters in it. I.e. '\\\\'.
let stringifiedValue = (stringify
? JSON.stringify(value)
: value);

if (typeof value === 'object' && !/^".*"$/.test(stringifiedValue)) {
// Stringify object that are not stringified to a
// JSON string (like Date) to escape commas, quotes, etc.
stringifiedValue = JSON.stringify(stringifiedValue);
}
const valueType = typeof value;
if (valueType !== 'boolean' && valueType !== 'number' && valueType !== 'string') {
value = JSON.stringify(value);

if (stringifiedValue === undefined) {
return undefined;
}
if (value === undefined) {
return undefined;
}

if (isValueString) {
stringifiedValue = stringifiedValue
.replace(/\u2028/g, '\n')
.replace(/\u2029/g, '\r')
.replace(/\u21E5/g, '\t');
if (value[0] === '"') {
value = value.replace(/^"(.+)"$/,'$1');
}
}

if (this.opts.quote === '"') {
// Replace automatically escaped single quotes by doubleQuotes
stringifiedValue = stringifiedValue
.replace(/\\"(?!$)/g, this.opts.doubleQuote);
} else {
// Unescape double quotes ('"')
// Replace single quote with double quote
// Replace wrapping quotes
stringifiedValue = stringifiedValue
.replace(/\\"(?!$)/g, '"')
.replace(new RegExp(this.opts.quote, 'g'), this.opts.doubleQuote)
.replace(/^"/, this.opts.quote)
.replace(/"$/, this.opts.quote);
}
if (typeof value === 'string') {
if(value.includes(this.opts.quote)) {
value = value.replace(new RegExp(this.opts.quote, 'g'), this.opts.doubleQuote);
}

// Remove double backslashes
stringifiedValue = stringifiedValue
.replace(/\\\\/g, '\\');
// This should probably be remove together with the whole strignify option
if (stringify) {
value = `${this.opts.quote}${value}${this.opts.quote}`;
} else {
value = value
.replace(new RegExp(`^${this.opts.doubleQuote}`), this.opts.quote)
.replace(new RegExp(`${this.opts.doubleQuote}$`), this.opts.quote);
}

if (this.opts.excelStrings && typeof value === 'string') {
stringifiedValue = '"="' + stringifiedValue + '""';
if (this.opts.excelStrings) {
value = `"="${value}""`;
}
}

return stringifiedValue;
return value;
}

/**
Expand Down
10 changes: 5 additions & 5 deletions lib/JSON2CSVParser.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const JSON2CSVBase = require('./JSON2CSVBase');
const { flattenReducer } = require('./utils');
const { fastJoin, flattenReducer } = require('./utils');

class JSON2CSVParser extends JSON2CSVBase {
constructor(opts) {
Expand Down Expand Up @@ -73,10 +73,10 @@ class JSON2CSVParser extends JSON2CSVBase {
* @returns {String} CSV string (body)
*/
processData(data) {
return data
.map(row => this.processRow(row))
.filter(row => row) // Filter empty rows
.join(this.opts.eol);
return fastJoin(
data.map(row => this.processRow(row)).filter(row => row), // Filter empty rows
this.opts.eol
);
}
}

Expand Down
22 changes: 22 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
'use strict';

function getProp(obj, path, defaultValue) {
return obj[path] === undefined ? defaultValue : obj[path];
}

function setProp(obj, path, value) {
const pathArray = Array.isArray(path) ? path : path.split('.');
const key = pathArray[0];
Expand All @@ -18,7 +22,25 @@ function flattenReducer(acc, arr) {
}
}

function fastJoin(arr, separator) {
let isFirst = true;
return arr.reduce((acc, elem) => {
if (elem === null || elem === undefined) {
elem = '';
}

if (isFirst) {
isFirst = false;
return `${elem}`;
}

return `${acc}${separator}${elem}`;
}, '');
}

module.exports = {
getProp,
setProp,
fastJoin,
flattenReducer
};
4 changes: 2 additions & 2 deletions test/CLI.js
Original file line number Diff line number Diff line change
Expand Up @@ -560,8 +560,8 @@ module.exports = (testRunner, jsonFixtures, csvFixtures) => {
const csv = stdout;
t.equal(csv, [
'"a string"',
'"with a \ndescription\\n and\na new line"',
'"with a \r\ndescription and\r\nanother new line"'
'"with a \u2028description\\n and\na new line"',
'"with a \u2029\u2028description and\r\nanother new line"'
].join('\r\n'));
t.end();
});
Expand Down
4 changes: 2 additions & 2 deletions test/JSON2CSVAsyncParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -716,8 +716,8 @@ module.exports = (testRunner, jsonFixtures, csvFixtures, inMemoryJsonFixtures) =
parser.fromInput(jsonFixtures.escapeEOL()).promise()
.then(csv => t.equal(csv, [
'"a string"',
'"with a \ndescription\\n and\na new line"',
'"with a \r\ndescription and\r\nanother new line"'
'"with a \u2028description\\n and\na new line"',
'"with a \u2029\u2028description and\r\nanother new line"'
].join('\r\n')))
.catch(err => t.notOk(true, err.message))
.then(() => t.end());
Expand Down
4 changes: 2 additions & 2 deletions test/JSON2CSVParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -657,8 +657,8 @@ module.exports = (testRunner, jsonFixtures, csvFixtures) => {

t.equal(csv, [
'"a string"',
'"with a \ndescription\\n and\na new line"',
'"with a \r\ndescription and\r\nanother new line"'
'"with a \u2028description\\n and\na new line"',
'"with a \u2029\u2028description and\r\nanother new line"'
].join('\r\n'));
t.end();
});
Expand Down
4 changes: 2 additions & 2 deletions test/JSON2CSVTransform.js
Original file line number Diff line number Diff line change
Expand Up @@ -1081,8 +1081,8 @@ module.exports = (testRunner, jsonFixtures, csvFixtures, inMemoryJsonFixtures) =
.on('end', () => {
t.equal(csv, [
'"a string"',
'"with a \ndescription\\n and\na new line"',
'"with a \r\ndescription and\r\nanother new line"'
'"with a \u2028description\\n and\na new line"',
'"with a \u2029\u2028description and\r\nanother new line"'
].join('\r\n'));
t.end();
})
Expand Down