Skip to content

Commit eba1491

Browse files
committed
Adding support for array values in docs. Updated README
1 parent a1619cd commit eba1491

File tree

8 files changed

+228
-79
lines changed

8 files changed

+228
-79
lines changed

README.md

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ var converter = require('json-2-csv');
2727

2828
#### json2csv(array, callback, options)
2929

30-
* `array` - An array of JSON documents
31-
* `callback` - A function of the form `function (err, csv)`; This function will receive any errors and/or the CSV generated.
30+
* `csv` - A string of CSV
31+
* `callback` - A function of the form `function (err, array)`; This function will receive any errors and/or the array of JSON documents generated.
3232
* `options` - (Optional) A JSON document specifying any of {`DELIMITER`, `EOL`, `PARSE_CSV_NUMBERS`}
33-
* `DELIMITER` - String - Field Delimiter. Default: `','`
33+
* `DELIMITER` - Document - Specifies the different types of delimiters
34+
* `FIELD` - String - Field Delimiter. Default: `','`
35+
* `ARRAY` - String - Array Value Delimiter. Default: `';'`
3436
* `EOL` - String - End of Line Delimiter. Default: `'\n'`
3537
* `PARSE_CSV_NUMBERS` - Boolean - Should numbers that are found in the CSV be converted to numbers? Default: `false`
3638

@@ -42,21 +44,21 @@ var converter = require('json-2-csv');
4244

4345
var documents = [
4446
{
45-
'Make': 'Nissan',
46-
'Model': 'Murano',
47-
'Year': '2013'
48-
'Specifications': {
49-
'Mileage': '7106',
50-
'Trim': 'S AWD'
47+
Make: 'Nissan',
48+
Model: 'Murano',
49+
Year: '2013'
50+
Specifications: {
51+
Mileage: '7106',
52+
Trim: 'S AWD'
5153
}
5254
},
5355
{
54-
'Make': 'BMW',
55-
'Model' 'X5',
56-
'Year': '2014',
57-
'Specifications': {
58-
'Mileage': '3287',
59-
'Trim': 'M'
56+
Make: 'BMW',
57+
Model' 'X5',
58+
Year: '2014',
59+
Specifications: {
60+
Mileage: '3287',
61+
Trim: 'M'
6062
}
6163
}
6264
];
@@ -83,7 +85,9 @@ BMW,X5,2014,3287,M
8385
* `csv` - A string of CSV
8486
* `callback` - A function of the form `function (err, array)`; This function will receive any errors and/or the array of JSON documents generated.
8587
* `options` - (Optional) A JSON document specifying any of {`DELIMITER`, `EOL`, `PARSE_CSV_NUMBERS`}
86-
* `DELIMITER` - String - Field Delimiter. Default: `','`
88+
* `DELIMITER` - Document - Specifies the different types of delimiters
89+
* `FIELD` - String - Field Delimiter. Default: `','`
90+
* `ARRAY` - String - Array Value Delimiter. Default: `';'`
8791
* `EOL` - String - End of Line Delimiter. Default: `'\n'`
8892
* `PARSE_CSV_NUMBERS` - Boolean - Should numbers that are found in the CSV be converted to numbers? Default: `false`
8993
@@ -134,15 +138,17 @@ _Note_: This requires `mocha`, `should`, `async`, and `underscore`.
134138
- Header Generation (per document keys)
135139
- Verifies all documents have same schema
136140
- Supports sub-documents natively
141+
- Supports arrays as document values for both json2csv and csv2json
137142
- Custom ordering of columns (see F.A.Q. for more information)
138143
- Ability to re-generate the JSON documents that were used to generate the CSV (including nested documents)
139144
- Allows for custom field delimiters, end of line delimiters, etc.
140145
141146
## F.A.Q.
142147
143148
- Can the order of the keys be changed in the output?
144-
__Yes.__ Currently, changing the order of the keys in the JSON document will also change the order of the columns. (Node 10.26)
149+
__Yes.__ Currently, changing the order of the keys in the JSON document will also change the order of the columns. (Tested on Node 10.xx)
145150
146151
## TODO
147152
- Use PARSE_CSV_NUMBERS option to actually convert numbers. Not currently implemented.
148-
- Add test cases (& fix potential issues) where data is an array of values
153+
- Respect nested arrays when in json2csv - Currently flattens them
154+
- If quotes in CSV header, strip them? Add as an option?

lib/converter.js

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,25 @@ var json2Csv = require('./json-2-csv'), // Require our json-2-csv code
55
_ = require('underscore'); // Require underscore
66

77
// Default options; By using a function this is essentially a 'static' variable
8-
var OPTIONS = function () {
9-
return {
10-
DELIMITER : ',',
11-
EOL : '\n',
12-
PARSE_CSV_NUMBERS : false
13-
};
14-
}
8+
var defaultOptions = {
9+
DELIMITER : {
10+
FIELD : ',',
11+
ARRAY : ';'
12+
},
13+
EOL : '\n',
14+
PARSE_CSV_NUMBERS : false
15+
};
1516

1617
// Build the options to be passed to the appropriate function
1718
// If a user does not provide custom options, then we use our default
1819
// If options are provided, then we set each valid key that was passed
19-
var buildOptions = function (opts) {
20-
var out = _.extend(OPTIONS(), {});
21-
if (!opts) { return out; } // If undefined or null, return defaults
22-
_.each(_.keys(opts), function (key) {
23-
if (out[key]) { // If key is valid, set it
24-
out[key] = opts[key];
25-
} // Else ignore its value
26-
});
27-
return out; // Return customized version
20+
var buildOptions = function (opts, cb) {
21+
opts = opts ? opts : {}; // If undefined, set to an empty doc
22+
var out = _.defaults(opts, defaultOptions);
23+
// If the delimiter fields are the same, report an error to the caller
24+
if (out.DELIMITER.FIELD === out.DELIMITER.ARRAY) { return cb(new Error('The field and array delimiters must differ.')); }
25+
// Otherwise, send the options back
26+
else { return cb(null, out); }
2827
};
2928

3029
// Export the following functions that will be client accessible
@@ -35,8 +34,13 @@ module.exports = {
3534
// a callback that will be called with (err, csv) after
3635
// processing is completed, and optional options
3736
json2csv: function (array, callback, opts) {
38-
opts = buildOptions(opts); // Build the options
39-
json2Csv.json2csv(opts, array, callback); // Call our internal json2csv function
37+
buildOptions(opts, function (err, options) { // Build the options
38+
if (err) {
39+
return callback(err);
40+
} else {
41+
json2Csv.json2csv(options, array, callback); // Call our internal json2csv function
42+
}
43+
});
4044
},
4145

4246

@@ -45,8 +49,12 @@ module.exports = {
4549
// a callback that will be called with (err, csv) after
4650
// processing is completed, and optional options
4751
csv2json: function (csv, callback, opts) {
48-
opts = buildOptions(opts);
49-
csv2Json.csv2json(opts, csv, callback);
52+
buildOptions(opts, function (err, options) { // Build the options
53+
if (err) {
54+
return callback(err);
55+
} else {
56+
csv2Json.csv2json(options, csv, callback); // Call our internal csv2json function
57+
}
58+
});
5059
}
51-
5260
};

lib/csv-2-json.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ var retrieveHeading = function (lines, callback) {
1111
return callback(new Error("No data provided to retrieve heading.")); // Pass an error back to the user
1212
}
1313
var heading = lines[0]; // Grab the top line (header line)
14-
return heading.split(options.DELIMITER); // Return the heading split by the field options.DELIMITER
14+
return heading.split(options.DELIMITER.FIELD); // Return the heading split by the field delimiter
1515
};
1616

1717
// Add a nested key and its value in the given document
@@ -35,19 +35,38 @@ var addNestedKey = function (key, value, doc) {
3535
// Helper function to check if the given key already exists in the given document
3636
var keyExists = function (key, doc) {
3737
return (typeof doc[key] !== 'undefined'); // If the key doesn't exist, then the type is 'undefined'
38-
}
38+
};
39+
40+
var isArrayRepresentation = function (value) {
41+
return (value && value.indexOf('[') === 0 && value.lastIndexOf(']') === value.length-1);
42+
};
43+
44+
var convertArrayRepresentation = function (val) {
45+
val = _.filter(val.substring(1, val.length-1).split(options.DELIMITER.ARRAY), function (value) {
46+
return value;
47+
});
48+
_.each(val, function (value, indx) {
49+
if (isArrayRepresentation(value)) {
50+
val[indx] = convertArrayRepresentation(value);
51+
}
52+
});
53+
return val;
54+
};
3955

4056
// Create a JSON document with the given keys (designated by the CSV header) and the values (from the given line)
4157
var createDoc = function (keys, line, callback) {
4258
var doc = {}, // JSON document to start with and manipulate
4359
val, // Temporary variable to set the current key's value to
44-
line = line.trim().split(options.DELIMITER); // Split the line using the given DELIMITER after trimming whitespace
60+
line = line.trim().split(options.DELIMITER.FIELD); // Split the line using the given field delimiter after trimming whitespace
4561
if (line == '') { return false; } // If we have an empty line, then return false so we can remove all blank lines (falsy values)
4662
if (keys.length !== line.length) { // If the number of keys is different than the number of values in the current line
4763
return callback(new Error("Not every line has a correct number of values.")); // Pass the error back to the client
4864
}
4965
_.each(keys, function (key, indx) {
5066
val = line[indx] === '' ? null : line[indx];
67+
if (isArrayRepresentation(val)) {
68+
val = convertArrayRepresentation(val);
69+
}
5170
if (key.indexOf('.')) { // If key has '.' representing nested document
5271
doc = addNestedKey(key, val, doc); // Update the document to add the nested key structure
5372
} else { // Else we just have a straight key:value mapping

lib/json-2-csv.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,22 @@ var retrieveSubHeading = function (heading, data) {
1111
newKey; // temporary variable to aid in determining the heading - used to generate the 'nested' headings
1212
_.each(subKeys, function (subKey, indx) {
1313
// If the given heading is empty, then we set the heading to be the subKey, otherwise set it as a nested heading w/ a dot
14-
newKey = heading === '' ? subKey : heading + '.' + subKey;
15-
if (typeof data[subKey] === 'object' && data[subKey] !== null) { // If we have another nested document
14+
newKey = heading === '' ? subKey : heading + '.' + subKey;
15+
if (typeof data[subKey] === 'object' && data[subKey] !== null && typeof data[subKey].length === 'undefined') { // If we have another nested document
1616
subKeys[indx] = retrieveSubHeading(newKey, data[subKey]); // Recur on the subdocument to retrieve the full key name
1717
} else {
1818
subKeys[indx] = newKey; // Set the key name since we don't have a sub document
1919
}
2020
});
21-
return subKeys.join(options.DELIMITER); // Return the headings joined by our delimiter
21+
return subKeys.join(options.DELIMITER.FIELD); // Return the headings joined by our field delimiter
2222
};
2323

2424
// Retrieve the headings for all documents and return it. This checks that all documents have the same schema.
2525
var retrieveHeading = function (data) {
2626
return function (cb) { // Returns a function that takes a callback - the function is passed to async.parallel
2727
var keys = _.keys(data); // Retrieve the current data keys
2828
_.each(keys, function (key, indx) { // for each key
29-
if (typeof data[key] === 'object') {
29+
if (typeof data[key] === 'object') {
3030
// if the data at the key is a document, then we retrieve the subHeading starting with an empty string heading and the doc
3131
keys[indx] = retrieveSubHeading('', data[key]);
3232
}
@@ -35,7 +35,7 @@ var retrieveHeading = function (data) {
3535
keys = _.uniq(keys);
3636
// If we have more than 1 unique list, then not all docs have the same schema - report an error
3737
if (keys.length > 1) { throw new Error('Not all documents have the same schema.', keys); }
38-
cb(null, _.flatten(keys).join(options.DELIMITER)); // Return headings back
38+
return cb(null, _.flatten(keys).join(options.DELIMITER.FIELD)); // Return headings back
3939
};
4040
};
4141

@@ -46,14 +46,16 @@ var convertData = function (data, keys) {
4646
_.each(keys, function (key, indx) { // For each key
4747
value = data[key]; // Set the current data that we are looking at
4848
if (keys.indexOf(key) > -1) { // If the keys contain the current key, then process the data
49-
if (typeof value === 'object') { // If we have an object
49+
if (typeof value === 'object' && value !== null && typeof value.length === 'undefined') { // If we have an object
5050
output.push(convertData(value, _.keys(value))); // Push the recursively generated CSV
51+
} else if (typeof value === 'object' && value !== null && typeof value.length === 'number') { // We have an array of values
52+
output.push('[' + value.join(options.DELIMITER.ARRAY) + ']');
5153
} else {
5254
output.push(value); // Otherwise push the current value
5355
}
5456
}
5557
});
56-
return output.join(options.DELIMITER); // Return the data joined by our field delimiter
58+
return output.join(options.DELIMITER.FIELD); // Return the data joined by our field delimiter
5759
};
5860

5961
// Generate the CSV representing the given data.
@@ -74,17 +76,17 @@ module.exports = {
7476
else { options = opts; } // Options were passed, set the global options value
7577
if (!data) { callback(new Error('Cannot call json2csv on ' + data + '.')); return null; } // If we don't receive data, report an error
7678
if (typeof data !== 'object') { // If the data was not a single document or an array of documents
77-
cb(new Error('Data provided was not an array of documents.')); // Report the error back to the caller
79+
return cb(new Error('Data provided was not an array of documents.')); // Report the error back to the caller
7880
} else if (typeof data === 'object' && !data.length) { // Single document, not an array
7981
data = [data]; // Convert to an array of the given document
8082
}
8183
// Retrieve the heading and the CSV asynchronously in parallel
8284
async.parallel([retrieveHeading(data), generateCsv(data)], function (err, res) {
8385
if (!err) {
8486
// Data received with no errors, join the two responses with an end of line delimiter to setup heading and CSV body
85-
callback(null, res.join(options.EOL));
87+
return callback(null, res.join(options.EOL));
8688
} else {
87-
callback(err, null); // Report received error back to caller
89+
return callback(err, null); // Report received error back to caller
8890
}
8991
});
9092
}

test/CSV/arrayValueDocs.csv

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
info.name,coursesTaken,year
2+
Mike,[CS2500/CS2510],Sophomore
3+
John,[ANTH1101/POL2312/MATH2142/POL3305/LAW2100],Senior
4+
Joe,[],Freshman

test/JSON/arrayValueDocs.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[
2+
{
3+
"info": {
4+
"name": "Mike"
5+
},
6+
"coursesTaken": ["CS2500", "CS2510"],
7+
"year": "Sophomore"
8+
},
9+
{
10+
"info": {
11+
"name": "John"
12+
},
13+
"coursesTaken": ["ANTH1101", "POL2312", "MATH2142", "POL3305", "LAW2100"],
14+
"year": "Senior"
15+
},
16+
{
17+
"info": {
18+
"name": "Joe"
19+
},
20+
"coursesTaken": [],
21+
"year": "Freshman"
22+
}
23+
]

0 commit comments

Comments
 (0)