convert-csv-to-json 4.45.0 → 4.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/csvToJson.js CHANGED
@@ -8,7 +8,9 @@ const {
8
8
  ConfigurationError,
9
9
  CsvFormatError,
10
10
  JsonValidationError
11
- } = require('./util/errors');
11
+ } = require('./core/errors');
12
+ const Configurable = require('./core/configurable');
13
+ const ParserConfig = require('./core/parserConfig');
12
14
 
13
15
  const DEFAULT_FIELD_DELIMITER = ",";
14
16
  const QUOTE_CHAR = '"';
@@ -21,114 +23,52 @@ const CR = '\r';
21
23
  * Provides chainable API for configuring and converting CSV data
22
24
  * @category 2-Sync
23
25
  */
24
- class CsvToJson {
26
+ class CsvToJson extends Configurable {
25
27
 
26
28
  /**
27
- * Enable or disable automatic type formatting for values
28
- * When enabled, numeric strings are converted to numbers, 'true'/'false' to booleans
29
- * @param {boolean} active - Whether to format values by type
30
- * @returns {this} For method chaining
31
- */
32
- formatValueByType(active) {
33
- this.printValueFormatByType = active;
34
- return this;
35
- }
36
-
37
- /**
38
- * Enable or disable support for RFC 4180 quoted fields
39
- * When enabled, fields wrapped in double quotes can contain delimiters and newlines
40
- * @param {boolean} active - Whether to support quoted fields
41
- * @returns {this} For method chaining
42
- */
43
- supportQuotedField(active) {
44
- this.isSupportQuotedField = active;
45
- return this;
46
- }
47
-
48
- /**
49
- * Set the field delimiter character
50
- * @param {string} delimiter - Character(s) to use as field separator (default: ',')
51
- * @returns {this} For method chaining
29
+ * Parse CSV content using a frozen configuration snapshot.
30
+ * @param {string} parsedCsv - Raw CSV content as string
31
+ * @param {ParserConfig} config - Frozen parser configuration
32
+ * @returns {Array<object>} Parsed JSON array
52
33
  */
53
- fieldDelimiter(delimiter) {
54
- this.delimiter = delimiter;
55
- return this;
56
- }
34
+ csvToJsonWithConfig(parsedCsv, config) {
35
+ this.validateInputConfig(config);
57
36
 
58
- /**
59
- * Configure whitespace handling in header field names
60
- * @param {boolean} active - If true, removes all whitespace from header names; if false, only trims edges
61
- * @returns {this} For method chaining
62
- */
63
- trimHeaderFieldWhiteSpace(active) {
64
- this.isTrimHeaderFieldWhiteSpace = active;
65
- return this;
66
- }
37
+ const records = this.parseRecords(parsedCsv);
38
+ const fieldDelimiter = this.getFieldDelimiter(config);
39
+ let index = this.getIndexHeader(config);
40
+ let headers;
67
41
 
68
- /**
69
- * Set the row index where CSV headers are located
70
- * @param {number} indexHeaderValue - Zero-based row index containing headers
71
- * @returns {this} For method chaining
72
- * @throws {ConfigurationError} If not a valid number
73
- */
74
- indexHeader(indexHeaderValue) {
75
- if (isNaN(indexHeaderValue)) {
76
- throw ConfigurationError.invalidHeaderIndex(indexHeaderValue);
42
+ while (index < records.length) {
43
+ headers = this.getFields(records[index], config, fieldDelimiter);
44
+ if (stringUtils.hasContent(headers)) {
45
+ break;
46
+ }
47
+ index++;
77
48
  }
78
- this.indexHeaderValue = indexHeaderValue;
79
- return this;
80
- }
81
-
82
-
83
- /**
84
- * Configure sub-array parsing for special field values
85
- * Fields bracketed by delimiter and containing separator are parsed into arrays
86
- * @param {string} delimiter - Bracket character (default: '*')
87
- * @param {string} separator - Item separator within brackets (default: ',')
88
- * @returns {this} For method chaining
89
- * @example
90
- * // Input: "*val1,val2,val3*"
91
- * // Output: ["val1", "val2", "val3"]
92
- * .parseSubArray('*', ',')
93
- */
94
- parseSubArray(delimiter = '*',separator = ',') {
95
- this.parseSubArrayDelimiter = delimiter;
96
- this.parseSubArraySeparator = separator;
97
- return this;
98
- }
99
49
 
100
- /**
101
- * Set file encoding for reading CSV files
102
- * @param {string} encoding - Node.js supported encoding (e.g., 'utf8', 'latin1', 'ascii')
103
- * @returns {this} For method chaining
104
- */
105
- encoding(encoding){
106
- this.encoding = encoding;
107
- return this;
108
- }
109
-
110
- /**
111
- * Configure columns to exclude from output
112
- * @param {Array<number>} indexes - Column indexes to ignore
113
- * @returns {this} For method chaining
114
- * @private Used internally after validation in index.js
115
- */
116
- ignoreColumnIndexes(indexes) {
117
- this.indexesToIgnore = new Set(indexes);
118
- return this;
119
- }
50
+ if (!headers) {
51
+ throw CsvFormatError.missingHeader();
52
+ }
120
53
 
121
- /**
122
- * Sets a mapper function to transform each row after conversion
123
- * @param {function(object, number): (object|null)} mapperFn - Function that receives (row, index) and returns transformed row or null to filter out
124
- * @returns {this} For method chaining
125
- */
126
- mapRows(mapperFn) {
127
- if (typeof mapperFn !== 'function') {
128
- throw new TypeError('mapperFn must be a function');
54
+ const jsonResult = [];
55
+ for (let i = index + 1; i < records.length; i++) {
56
+ const currentLine = this.getFields(records[i], config, fieldDelimiter);
57
+
58
+ if (stringUtils.hasContent(currentLine)) {
59
+ let row = this.buildJsonResult(headers, currentLine, config);
60
+ if (config.rowMapper) {
61
+ row = config.rowMapper(row, i - (index + 1));
62
+ if (row != null) {
63
+ jsonResult.push(row);
64
+ }
65
+ } else {
66
+ jsonResult.push(row);
67
+ }
68
+ }
129
69
  }
130
- this.rowMapper = mapperFn;
131
- return this;
70
+
71
+ return jsonResult;
132
72
  }
133
73
 
134
74
  /**
@@ -174,7 +114,8 @@ class CsvToJson {
174
114
  * console.log(rows);
175
115
  */
176
116
  getJsonFromCsv(fileInputName) {
177
- let parsedCsv = fileUtils.readFile(fileInputName, this.encoding);
117
+ const config = this.getParserConfig();
118
+ const parsedCsv = fileUtils.readFile(fileInputName, config.encoding || 'utf8');
178
119
  return this.csvToJson(parsedCsv);
179
120
  }
180
121
 
@@ -219,58 +160,7 @@ class CsvToJson {
219
160
  * @private
220
161
  */
221
162
  csvToJson(parsedCsv) {
222
- this.validateInputConfig();
223
-
224
- // Parse CSV into individual records, respecting quoted fields that may contain newlines
225
- let records = this.parseRecords(parsedCsv);
226
-
227
- let fieldDelimiter = this.getFieldDelimiter();
228
- let index = this.getIndexHeader();
229
- let headers;
230
-
231
- // Find the header row
232
- while (index < records.length) {
233
- if (this.isSupportQuotedField) {
234
- headers = this.split(records[index]);
235
- } else {
236
- headers = records[index].split(fieldDelimiter);
237
- }
238
-
239
- if (stringUtils.hasContent(headers)) {
240
- break;
241
- }
242
- index++;
243
- }
244
-
245
- if (!headers) {
246
- throw CsvFormatError.missingHeader();
247
- }
248
-
249
- let jsonResult = [];
250
- for (let i = (index + 1); i < records.length; i++) {
251
- let currentLine;
252
- if (this.isSupportQuotedField) {
253
- currentLine = this.split(records[i]);
254
- } else {
255
- currentLine = records[i].split(fieldDelimiter);
256
- }
257
-
258
- if (stringUtils.hasContent(currentLine)) {
259
- let row = this.buildJsonResult(headers, currentLine);
260
-
261
- // Apply row mapper if defined
262
- if (this.rowMapper) {
263
- row = this.rowMapper(row, i - (index + 1)); // Pass row and 0-based row index
264
- // If mapper returns null/undefined, skip this row (allows filtering)
265
- if (row != null) {
266
- jsonResult.push(row);
267
- }
268
- } else {
269
- jsonResult.push(row);
270
- }
271
- }
272
- }
273
- return jsonResult;
163
+ return this.csvToJsonWithConfig(parsedCsv, this.getParserConfig());
274
164
  }
275
165
 
276
166
  /**
@@ -353,51 +243,71 @@ class CsvToJson {
353
243
 
354
244
  /**
355
245
  * Get the configured field delimiter, or default if not set
246
+ * @param {ParserConfig} [config] - Parser configuration
356
247
  * @returns {string} Field delimiter character
357
248
  * @private
358
249
  */
359
- getFieldDelimiter() {
360
- if (this.delimiter) {
361
- return this.delimiter;
250
+ getFieldDelimiter(config = this.config) {
251
+ if (config.delimiter) {
252
+ return config.delimiter;
362
253
  }
363
254
  return DEFAULT_FIELD_DELIMITER;
364
255
  }
365
256
 
366
257
  /**
367
258
  * Get the configured header row index, or default (0) if not set
259
+ * @param {ParserConfig} [config] - Parser configuration
368
260
  * @returns {number} Header row index
369
261
  * @private
370
262
  */
371
- getIndexHeader(){
372
- if(this.indexHeaderValue !== null && !isNaN(this.indexHeaderValue)){
373
- return this.indexHeaderValue;
263
+ getIndexHeader(config = this.config) {
264
+ if (config.indexHeaderValue !== null && !isNaN(config.indexHeaderValue)) {
265
+ return config.indexHeaderValue;
374
266
  }
375
267
  return 0;
376
268
  }
377
269
 
270
+ /**
271
+ * Get fields for a CSV record line according to configured parser rules.
272
+ * Uses quoted-field splitting when RFC 4180 support is enabled.
273
+ * @param {string} line - CSV record line
274
+ * @param {ParserConfig} [config] - Parser configuration
275
+ * @param {string} [fieldDelimiter] - Field delimiter
276
+ * @returns {string[]} Parsed fields
277
+ * @private
278
+ */
279
+ getFields(line, config = this.config, fieldDelimiter = this.getFieldDelimiter(config)) {
280
+ if (config.isSupportQuotedField) {
281
+ return this.split(line, config);
282
+ }
283
+ return line.split(fieldDelimiter);
284
+ }
285
+
378
286
  /**
379
287
  * Build a JSON object from headers and field values
380
288
  * Applies type formatting and sub-array parsing as configured
381
289
  * @param {string[]} headers - Array of header field names
382
290
  * @param {string[]} currentLine - Array of field values
291
+ * @param {ParserConfig} [config] - Frozen parser configuration
383
292
  * @returns {object} JSON object with header names as keys
384
293
  * @private
385
294
  */
386
- buildJsonResult(headers, currentLine) {
295
+ buildJsonResult(headers, currentLine, config = this.config) {
387
296
  let jsonObject = {};
297
+ const ignoredIndexes = config.indexesToIgnore ? new Set(config.indexesToIgnore) : new Set();
388
298
  for (let j = 0; j < headers.length; j++) {
389
- if (this.indexesToIgnore?.has(j)) {
299
+ if (ignoredIndexes.has(j)) {
390
300
  continue;
391
301
  }
392
302
 
393
- let propertyName = stringUtils.trimPropertyName(this.isTrimHeaderFieldWhiteSpace, headers[j]);
303
+ let propertyName = stringUtils.trimPropertyName(config.isTrimHeaderFieldWhiteSpace, headers[j]);
394
304
  let value = currentLine[j];
395
305
 
396
- if(this.isParseSubArray(value)){
397
- value = this.buildJsonSubArray(value);
306
+ if (this.isParseSubArray(value, config)) {
307
+ value = this.buildJsonSubArray(value, config);
398
308
  }
399
309
 
400
- if (this.printValueFormatByType && !Array.isArray(value)) {
310
+ if (config.printValueFormatByType && !Array.isArray(value)) {
401
311
  value = stringUtils.getValueFormatByType(currentLine[j]);
402
312
  }
403
313
 
@@ -409,18 +319,19 @@ class CsvToJson {
409
319
  /**
410
320
  * Parse a field value into a sub-array using configured delimiter and separator
411
321
  * @param {string} value - Field value to parse
322
+ * @param {ParserConfig} [config] - Frozen parser configuration
412
323
  * @returns {Array<string|number|boolean>} Array of parsed values
413
324
  * @private
414
325
  */
415
- buildJsonSubArray(value) {
326
+ buildJsonSubArray(value, config = this.config) {
416
327
  let extractedValues = value.substring(
417
- value.indexOf(this.parseSubArrayDelimiter) + 1,
418
- value.lastIndexOf(this.parseSubArrayDelimiter)
328
+ value.indexOf(config.parseSubArrayDelimiter) + 1,
329
+ value.lastIndexOf(config.parseSubArrayDelimiter)
419
330
  );
420
331
  extractedValues.trim();
421
- value = extractedValues.split(this.parseSubArraySeparator);
422
- if(this.printValueFormatByType){
423
- for(let i=0; i < value.length; i++){
332
+ value = extractedValues.split(config.parseSubArraySeparator);
333
+ if (config.printValueFormatByType) {
334
+ for (let i = 0; i < value.length; i++) {
424
335
  value[i] = stringUtils.getValueFormatByType(value[i]);
425
336
  }
426
337
  }
@@ -430,12 +341,13 @@ class CsvToJson {
430
341
  /**
431
342
  * Check if a field value should be parsed as a sub-array
432
343
  * @param {string} value - Field value to check
344
+ * @param {ParserConfig} [config] - Frozen parser configuration
433
345
  * @returns {boolean} True if value is bracketed with sub-array delimiter
434
346
  * @private
435
347
  */
436
- isParseSubArray(value){
437
- if(this.parseSubArrayDelimiter){
438
- if (value && (value.indexOf(this.parseSubArrayDelimiter) === 0 && value.lastIndexOf(this.parseSubArrayDelimiter) === (value.length - 1))) {
348
+ isParseSubArray(value, config = this.config) {
349
+ if (config.parseSubArrayDelimiter) {
350
+ if (value && (value.indexOf(config.parseSubArrayDelimiter) === 0 && value.lastIndexOf(config.parseSubArrayDelimiter) === (value.length - 1))) {
439
351
  return true;
440
352
  }
441
353
  }
@@ -444,18 +356,19 @@ class CsvToJson {
444
356
 
445
357
  /**
446
358
  * Validate configuration for conflicts and incompatibilities
359
+ * @param {ParserConfig} [config] - Parser configuration
447
360
  * @throws {ConfigurationError} If incompatible options are set
448
361
  * @private
449
362
  */
450
- validateInputConfig(){
451
- if(this.isSupportQuotedField) {
452
- if(this.getFieldDelimiter() === '"'){
363
+ validateInputConfig(config = this.config) {
364
+ if (config.isSupportQuotedField) {
365
+ if (this.getFieldDelimiter(config) === '"') {
453
366
  throw ConfigurationError.quotedFieldConflict('fieldDelimiter', '"');
454
367
  }
455
- if(this.parseSubArraySeparator === '"'){
368
+ if (config.parseSubArraySeparator === '"') {
456
369
  throw ConfigurationError.quotedFieldConflict('parseSubArraySeparator', '"');
457
370
  }
458
- if(this.parseSubArrayDelimiter === '"'){
371
+ if (config.parseSubArrayDelimiter === '"') {
459
372
  throw ConfigurationError.quotedFieldConflict('parseSubArrayDelimiter', '"');
460
373
  }
461
374
  }
@@ -478,9 +391,10 @@ class CsvToJson {
478
391
  * - Escaped quotes (double quotes within quoted fields)
479
392
  * - Empty quoted fields
480
393
  * @param {string} line - A single CSV record line
394
+ * @param {ParserConfig} [config] - Parser configuration
481
395
  * @returns {string[]} Array of field values
482
396
  */
483
- split(line) {
397
+ split(line, config = this.config) {
484
398
  if (line.length === 0) {
485
399
  return [];
486
400
  }
@@ -488,7 +402,7 @@ class CsvToJson {
488
402
  let fields = [];
489
403
  let currentField = '';
490
404
  let insideQuotes = false;
491
- let delimiter = this.getFieldDelimiter();
405
+ let delimiter = this.getFieldDelimiter(config);
492
406
 
493
407
  for (let i = 0; i < line.length; i++) {
494
408
  let char = line[i];
@@ -568,3 +482,4 @@ class CsvToJson {
568
482
  }
569
483
 
570
484
  module.exports = new CsvToJson();
485
+ module.exports.CsvToJson = CsvToJson;
@@ -3,115 +3,24 @@
3
3
 
4
4
  const fileUtils = require('./util/fileUtils');
5
5
  const csvToJson = require('./csvToJson');
6
- const { InputValidationError } = require('./util/errors');
7
- const StreamProcessor = require('./streamProcessor');
6
+ const Configurable = require('./core/configurable');
7
+ const { InputValidationError } = require('./core/errors');
8
+ const StreamProcessor = require('./core/streamProcessor');
8
9
 
9
10
  /**
10
11
  * Asynchronous CSV to JSON converter
11
- * Proxies configuration to sync instance but provides async file I/O methods
12
+ * Provides async file I/O methods and isolated parser configuration
12
13
  * @category 3-Async
13
14
  */
14
- class CsvToJsonAsync {
15
+ class CsvToJsonAsync extends Configurable {
15
16
  /**
16
17
  * Constructor initializes proxy to sync csvToJson instance
17
18
  */
18
19
  constructor() {
19
- // Proxy the configuration methods to the sync instance
20
+ super();
20
21
  this.csvToJson = csvToJson;
21
22
  }
22
23
 
23
- /**
24
- * Enable or disable automatic type formatting for values
25
- * @param {boolean} active - Whether to format values by type
26
- * @returns {this} For method chaining
27
- */
28
- formatValueByType(active) {
29
- this.csvToJson.formatValueByType(active);
30
- return this;
31
- }
32
-
33
- /**
34
- * Enable or disable support for RFC 4180 quoted fields
35
- * @param {boolean} active - Whether to support quoted fields
36
- * @returns {this} For method chaining
37
- */
38
- supportQuotedField(active) {
39
- this.csvToJson.supportQuotedField(active);
40
- return this;
41
- }
42
-
43
- /**
44
- * Set the field delimiter character
45
- * @param {string} delimiter - Character(s) to use as field separator
46
- * @returns {this} For method chaining
47
- */
48
- fieldDelimiter(delimiter) {
49
- this.csvToJson.fieldDelimiter(delimiter);
50
- return this;
51
- }
52
-
53
- /**
54
- * Configure whitespace handling in header field names
55
- * @param {boolean} active - If true, removes all whitespace; if false, only trims edges
56
- * @returns {this} For method chaining
57
- */
58
- trimHeaderFieldWhiteSpace(active) {
59
- this.csvToJson.trimHeaderFieldWhiteSpace(active);
60
- return this;
61
- }
62
-
63
- /**
64
- * Set the row index where CSV headers are located
65
- * @param {number} indexHeader - Zero-based row index containing headers
66
- * @returns {this} For method chaining
67
- */
68
- indexHeader(indexHeader) {
69
- this.csvToJson.indexHeader(indexHeader);
70
- return this;
71
- }
72
-
73
- /**
74
- * Configure sub-array parsing for special field values
75
- * @param {string} delimiter - Bracket character (default: '*')
76
- * @param {string} separator - Item separator within brackets (default: ',')
77
- * @returns {this} For method chaining
78
- */
79
- parseSubArray(delimiter = '*', separator = ',') {
80
- this.csvToJson.parseSubArray(delimiter, separator);
81
- return this;
82
- }
83
-
84
- /**
85
- * Set a mapper function to transform each row after conversion
86
- * @param {function(object, number): (object|null)} mapperFn - Function receiving (row, index) that returns transformed row or null to filter
87
- * @returns {this} For method chaining
88
- */
89
- mapRows(mapperFn) {
90
- this.csvToJson.mapRows(mapperFn);
91
- return this;
92
- }
93
-
94
- /**
95
- * Set file encoding for reading CSV files
96
- * @param {string} encoding - Node.js supported encoding (e.g., 'utf8', 'latin1')
97
- * @returns {this} For method chaining
98
- */
99
- encoding(encoding) {
100
- this.csvToJson.encoding = encoding;
101
- return this;
102
- }
103
-
104
- /**
105
- * Configure columns to exclude from output
106
- * @param {Array<number>} indexes - Column indexes to ignore
107
- * @returns {this} For method chaining
108
- * @private Used internally after validation in index.js
109
- */
110
- ignoreColumnIndexes(indexes) {
111
- this.csvToJson.ignoreColumnIndexes(indexes);
112
- return this;
113
- }
114
-
115
24
  /**
116
25
  * Read a CSV file and write parsed JSON to an output file (async)
117
26
  * @param {string} fileInputName - Path to input CSV file
@@ -162,15 +71,17 @@ class CsvToJsonAsync {
162
71
  );
163
72
  }
164
73
 
74
+ const config = this.getParserConfig();
75
+
165
76
  if (options.raw) {
166
77
  if (inputFileNameOrCsv === '') {
167
78
  return [];
168
79
  }
169
- return this.csvToJson.csvToJson(inputFileNameOrCsv);
80
+ return this.csvToJson.csvToJsonWithConfig(inputFileNameOrCsv, config);
170
81
  }
171
82
 
172
- const parsedCsv = await fileUtils.readFileAsync(inputFileNameOrCsv, this.csvToJson.encoding || 'utf8');
173
- return this.csvToJson.csvToJson(parsedCsv);
83
+ const parsedCsv = await fileUtils.readFileAsync(inputFileNameOrCsv, config.encoding || 'utf8');
84
+ return this.csvToJson.csvToJsonWithConfig(parsedCsv, config);
174
85
  }
175
86
 
176
87
  /**
@@ -205,7 +116,8 @@ class CsvToJsonAsync {
205
116
  async getJsonFromStreamAsync(stream) {
206
117
  this._validateStream(stream);
207
118
 
208
- const streamProcessor = new StreamProcessor(this.csvToJson, { isBrowser: false });
119
+ const config = this.getParserConfig();
120
+ const streamProcessor = new StreamProcessor(config, { isBrowser: false });
209
121
  return streamProcessor.processStream(stream);
210
122
  }
211
123
 
@@ -249,7 +161,8 @@ class CsvToJsonAsync {
249
161
  }
250
162
 
251
163
  const fs = require('fs');
252
- const encoding = typeof this.csvToJson.encoding === 'string' ? this.csvToJson.encoding : 'utf8';
164
+ const config = this.getParserConfig();
165
+ const encoding = typeof config.encoding === 'string' ? config.encoding : 'utf8';
253
166
  const stream = fs.createReadStream(filePath, { encoding });
254
167
  return this.getJsonFromStreamAsync(stream);
255
168
  }