UNPKG

json-2-csv

Version:

A JSON to CSV and CSV to JSON converter that natively supports sub-documents and auto-generates the CSV heading.

213 lines (187 loc) 8.47 kB
'use strict'; var _ = require('underscore'), path = require('doc-path'), constants = require('./constants'); var options = {}; // Initialize the options - this will be populated when the csv2json function is called. /** * Generate the JSON heading from the CSV * @param lines * @param callback * @returns {*} */ var retrieveHeading = function (lines, callback) { // If there are no lines passed in, return an error if (!lines.length) { return callback(new Error(constants.Errors.csv2json.noDataRetrieveHeading)); // Pass an error back to the user } // Generate and return the heading keys return _.map(splitLine(lines[0]), function (headerKey, index) { return { value: headerKey, index: index }; }); }; /** * Does the given value represent an array? * @param value * @returns {boolean} */ var isArrayRepresentation = function (value) { // Verify that there is a value and it starts with '[' and ends with ']' return (value && /^\[.*\]$/.test(value)); }; /** * Converts the value from a CSV 'array' * @param val * @returns {Array} */ var convertArrayRepresentation = function (arrayRepresentation) { // Remove the '[' and ']' characters arrayRepresentation = arrayRepresentation.replace(/(\[|\])/g, ''); // Split the arrayRepresentation into an array by the array delimiter arrayRepresentation = arrayRepresentation.split(options.DELIMITER.ARRAY); // Filter out non-empty strings return _.filter(arrayRepresentation, function (value) { return value; }); }; /** * Create a JSON document with the given keys (designated by the CSV header) * and the values (from the given line) * @param keys String[] * @param line String * @returns {Object} created json document */ var createDocument = function (keys, line) { var line = splitLine(line), // Split the line using the given field delimiter after trimming whitespace val; // Temporary variable to set the current key's value to // Reduce the keys into a JSON document representing the given line return _.reduce(keys, function (document, key) { // If there is a value at the key's index in the line, set the value; otherwise null val = line[key.index] ? line[key.index] : null; // If the value is an array representation, convert it if (isArrayRepresentation(val)) { val = convertArrayRepresentation(val); } // Otherwise add the key and value to the document return path.setPath(document, key.value, val); }, {}); }; /** * Main helper function to convert the CSV to the JSON document array * @param lines String[] * @param callback Function callback function * @returns {Array} */ var convertCSV = function (lines, callback) { var generatedHeaders = retrieveHeading(lines, callback), // Retrieve the headings from the CSV, unless the user specified the keys nonHeaderLines = lines.splice(1), // All lines except for the header line // If the user provided keys, filter the generated keys to just the user provided keys so we also have the key index headers = options.KEYS ? _.filter(generatedHeaders, function (headerKey) { return _.contains(options.KEYS, headerKey.value); }) : generatedHeaders; return _.reduce(nonHeaderLines, function (documentArray, line) { // For each line, create the document and add it to the array of documents if (!line) { return documentArray; } // skip over empty lines var generatedDocument = createDocument(headers, line.trim()); return documentArray.concat(generatedDocument); }, []); }; /** * Helper function that splits a line so that we can handle wrapped fields * @param line */ var splitLine = function (line) { // If the fields are not wrapped, return the line split by the field delimiter if (!options.DELIMITER.WRAP) { return line.split(options.DELIMITER.FIELD); } // Parse out the line... var splitLine = [], character, charBefore, charAfter, lastCharacterIndex = line.length - 1, stateVariables = { insideWrapDelimiter: false, parsingValue: true, startIndex: 0 }, index = 0; // Loop through each character in the line to identify where to split the values while(index < line.length) { // Current character character = line[index]; // Previous character charBefore = index ? line[index - 1] : ''; // Next character charAfter = index < lastCharacterIndex ? line[index + 1] : ''; // If we reached the end of the line, add the remaining value if (index === lastCharacterIndex) { splitLine.push(line.substring(stateVariables.startIndex, stateVariables.insideWrapDelimiter ? index : undefined)); } // If the line starts with a wrap delimiter else if (character === options.DELIMITER.WRAP && index === 0) { stateVariables.insideWrapDelimiter = true; stateVariables.parsingValue = true; stateVariables.startIndex = index + 1; } // If we reached a wrap delimiter with a field delimiter after it (ie. *",) else if (character === options.DELIMITER.WRAP && charAfter === options.DELIMITER.FIELD) { splitLine.push(line.substring(stateVariables.startIndex, index)); stateVariables.startIndex = index + 2; // next value starts after the field delimiter stateVariables.insideWrapDelimiter = false; stateVariables.parsingValue = false; } // If we reached a wrap delimiter with a field delimiter after it (ie. ,"*) else if (character === options.DELIMITER.WRAP && charBefore === options.DELIMITER.FIELD) { if (stateVariables.parsingValue) { splitLine.push(line.substring(stateVariables.startIndex, index-1)); } stateVariables.insideWrapDelimiter = true; stateVariables.parsingValue = true; stateVariables.startIndex = index + 1; } // If we reached a field delimiter and are not inside the wrap delimiters (ie. *,*) else if (character === options.DELIMITER.FIELD && charBefore !== options.DELIMITER.WRAP && charAfter !== options.DELIMITER.WRAP && !stateVariables.insideWrapDelimiter && stateVariables.parsingValue) { splitLine.push(line.substring(stateVariables.startIndex, index)); stateVariables.startIndex = index + 1; } else if (character === options.DELIMITER.FIELD && charBefore === options.DELIMITER.WRAP && charAfter !== options.DELIMITER.WRAP) { stateVariables.insideWrapDelimiter = false; stateVariables.parsingValue = true; stateVariables.startIndex = index + 1; } // Otherwise increment to the next character index++; } return splitLine; }; module.exports = { /** * Internally exported csv2json function * Takes options as a document, data as a CSV string, and a callback that will be used to report the results * @param opts Object options object * @param data String csv string * @param callback Function callback function */ csv2json: function (opts, data, callback) { // If a callback wasn't provided, throw an error if (!callback) { throw new Error(constants.Errors.callbackRequired); } // Shouldn't happen, but just in case if (!opts) { return callback(new Error(constants.Errors.optionsRequired)); } options = opts; // Options were passed, set the global options value // If we don't receive data, report an error if (!data) { return callback(new Error(constants.Errors.csv2json.cannotCallCsv2JsonOn + data + '.')); } // The data provided is not a string if (!_.isString(data)) { return callback(new Error(constants.Errors.csv2json.csvNotString)); // Report an error back to the caller } // Split the CSV into lines using the specified EOL option var lines = data.split(options.EOL), json = convertCSV(lines, callback); // Retrieve the JSON document array return callback(null, json); // Send the data back to the caller } };