Skip to content

Commit 697a68d

Browse files
committed
Merge pull request #23 from mrodrig/csv2json-wrap-fix
csv2json Wrapped Field Support
2 parents 08c6003 + a8998de commit 697a68d

12 files changed

+503
-132
lines changed

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,12 @@ BMW,X5,2014,3287,M
9090
* `DELIMITER` - Document - Specifies the different types of delimiters
9191
* `FIELD` - String - Field Delimiter. Default: `','`
9292
* `ARRAY` - String - Array Value Delimiter. Default: `';'`
93+
* `WRAP` - String - The character that field values are wrapped in. Default: `''`
9394
* `EOL` - String - End of Line Delimiter. Default: `'\n'`
9495
* `PARSE_CSV_NUMBERS` - Boolean - (TODO) Should numbers that are found in the CSV be converted to numbers? Default: `false`
9596
* `KEYS` - Array - Specify the keys (as strings) that should be converted. Default: `null`
9697
* If you have a nested object (ie. {info : {name: 'Mike'}}), then set options.KEYS to ['info.name']
97-
* If you want all keys to be converted, then specify ```null``` or don't specify the option to utilize the default.
98+
* If you want all keys to be converted, then specify `null` or don't specify the option to utilize the default.
9899

99100
##### csv2json Example:
100101

@@ -145,10 +146,10 @@ $ npm run coverage
145146

146147
Current Coverage is:
147148
```
148-
Statements : 94.49% ( 120/127 )
149-
Branches : 90.24% ( 74/82 )
150-
Functions : 100% ( 30/30 )
151-
Lines : 97.39% ( 112/115 )
149+
Statements : 96.88% ( 155/160 )
150+
Branches : 93.55% ( 116/124 )
151+
Functions : 100% ( 31/31 )
152+
Lines : 97.99% ( 146/149 )
152153
```
153154

154155
## Features
@@ -162,6 +163,7 @@ Lines : 97.39% ( 112/115 )
162163
- Ability to re-generate the JSON documents that were used to generate the CSV (including nested documents)
163164
- Allows for custom field delimiters, end of line delimiters, etc.
164165
- Promisifiable via bluebird's .promisify(<function>) and .promisifyAll(<object>) (as of 1.1.1)
166+
- Wrapped value support for json2csv and csv2json (as of 1.3.0)
165167

166168
## F.A.Q.
167169

bower.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-2-csv",
3-
"version": "1.2.1",
3+
"version": "1.3.0",
44
"homepage": "https://github.com/mrodrig/json-2-csv",
55
"moduleType": [
66
"node"

lib/csv-2-json.js

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ var retrieveHeading = function (lines, callback) {
1919
}
2020

2121
// Generate and return the heading keys
22-
return _.map(lines[0].split(options.DELIMITER.FIELD),
22+
return _.map(splitLine(lines[0]),
2323
function (headerKey, index) {
2424
return {
2525
value: headerKey,
@@ -64,7 +64,7 @@ var convertArrayRepresentation = function (arrayRepresentation) {
6464
* @returns {Object} created json document
6565
*/
6666
var createDocument = function (keys, line) {
67-
var line = line.split(options.DELIMITER.FIELD), // Split the line using the given field delimiter after trimming whitespace
67+
var line = splitLine(line), // Split the line using the given field delimiter after trimming whitespace
6868
val; // Temporary variable to set the current key's value to
6969

7070
// Reduce the keys into a JSON document representing the given line
@@ -102,6 +102,83 @@ var convertCSV = function (lines, callback) {
102102
}, []);
103103
};
104104

105+
/**
106+
* Helper function that splits a line so that we can handle wrapped fields
107+
* @param line
108+
*/
109+
var splitLine = function (line) {
110+
// If the fields are not wrapped, return the line split by the field delimiter
111+
if (!options.DELIMITER.WRAP) { return line.split(options.DELIMITER.FIELD); }
112+
113+
// Parse out the line...
114+
var splitLine = [],
115+
character,
116+
charBefore,
117+
charAfter,
118+
lastCharacterIndex = line.length - 1,
119+
stateVariables = {
120+
insideWrapDelimiter: false,
121+
parsingValue: true,
122+
startIndex: 0
123+
},
124+
index = 0;
125+
126+
// Loop through each character in the line to identify where to split the values
127+
while(index < line.length) {
128+
// Current character
129+
character = line[index];
130+
// Previous character
131+
charBefore = index ? line[index - 1] : '';
132+
// Next character
133+
charAfter = index < lastCharacterIndex ? line[index + 1] : '';
134+
135+
// If we reached the end of the line, add the remaining value
136+
if (index === lastCharacterIndex) {
137+
splitLine.push(line.substring(stateVariables.startIndex, stateVariables.insideWrapDelimiter ? index : undefined));
138+
}
139+
// If the line starts with a wrap delimiter
140+
else if (character === options.DELIMITER.WRAP && index === 0) {
141+
stateVariables.insideWrapDelimiter = true;
142+
stateVariables.parsingValue = true;
143+
stateVariables.startIndex = index + 1;
144+
}
145+
146+
// If we reached a wrap delimiter with a field delimiter after it (ie. *",)
147+
else if (character === options.DELIMITER.WRAP && charAfter === options.DELIMITER.FIELD) {
148+
splitLine.push(line.substring(stateVariables.startIndex, index));
149+
stateVariables.startIndex = index + 2; // next value starts after the field delimiter
150+
stateVariables.insideWrapDelimiter = false;
151+
stateVariables.parsingValue = false;
152+
}
153+
// If we reached a wrap delimiter with a field delimiter after it (ie. ,"*)
154+
else if (character === options.DELIMITER.WRAP && charBefore === options.DELIMITER.FIELD) {
155+
if (stateVariables.parsingValue) {
156+
splitLine.push(line.substring(stateVariables.startIndex, index-1));
157+
}
158+
stateVariables.insideWrapDelimiter = true;
159+
stateVariables.parsingValue = true;
160+
stateVariables.startIndex = index + 1;
161+
}
162+
// If we reached a field delimiter and are not inside the wrap delimiters (ie. *,*)
163+
else if (character === options.DELIMITER.FIELD && charBefore !== options.DELIMITER.WRAP
164+
&& charAfter !== options.DELIMITER.WRAP && !stateVariables.insideWrapDelimiter
165+
&& stateVariables.parsingValue) {
166+
splitLine.push(line.substring(stateVariables.startIndex, index));
167+
stateVariables.startIndex = index + 1;
168+
}
169+
else if (character === options.DELIMITER.FIELD && charBefore === options.DELIMITER.WRAP
170+
&& charAfter !== options.DELIMITER.WRAP) {
171+
stateVariables.insideWrapDelimiter = false;
172+
stateVariables.parsingValue = true;
173+
stateVariables.startIndex = index + 1;
174+
}
175+
// Otherwise increment to the next character
176+
index++;
177+
}
178+
179+
return splitLine;
180+
};
181+
105182
module.exports = {
106183

107184
/**
@@ -116,11 +193,11 @@ module.exports = {
116193
if (!callback) { throw new Error(constants.Errors.callbackRequired); }
117194

118195
// Shouldn't happen, but just in case
119-
if (!opts) { return callback(new Error(constants.Errors.optionsRequired)); return null; }
196+
if (!opts) { return callback(new Error(constants.Errors.optionsRequired)); }
120197
options = opts; // Options were passed, set the global options value
121198

122199
// If we don't receive data, report an error
123-
if (!data) { return callback(new Error(constants.Errors.csv2json.cannotCallCsv2JsonOn + data + '.')); return null; }
200+
if (!data) { return callback(new Error(constants.Errors.csv2json.cannotCallCsv2JsonOn + data + '.')); }
124201

125202
// The data provided is not a string
126203
if (!_.isString(data)) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"author": "mrodrig",
33
"name": "json-2-csv",
44
"description": "A JSON to CSV and CSV to JSON converter that natively supports sub-documents and auto-generates the CSV heading.",
5-
"version": "1.2.1",
5+
"version": "1.3.0",
66
"repository": {
77
"type": "git",
88
"url": "http://github.com/mrodrig/json-2-csv.git"

test.js

Lines changed: 0 additions & 68 deletions
This file was deleted.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"carModel","priceRange.min","priceRange.max"
2+
Audi,"9000",11000
3+
"BMW",14000,16000
4+
"Mercedes",19000,"21000"
5+
"Porsche","29000",31000
6+
,200,300
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[
2+
{
3+
"carModel": "Audi",
4+
"priceRange": {
5+
"min": "9000",
6+
"max": "11000"
7+
}
8+
},
9+
{
10+
"carModel": "BMW",
11+
"priceRange": {
12+
"min": "14000",
13+
"max": "16000"
14+
}
15+
},
16+
{
17+
"carModel": "Mercedes",
18+
"priceRange": {
19+
"min": "19000",
20+
"max": "21000"
21+
}
22+
},
23+
{
24+
"carModel": "Porsche",
25+
"priceRange": {
26+
"min": "29000",
27+
"max": "31000"
28+
}
29+
},
30+
{
31+
"carModel": null,
32+
"priceRange": {
33+
"min": "200",
34+
"max": "300"
35+
}
36+
}
37+
]

0 commit comments

Comments
 (0)