diff --git a/js/util/csv.js b/js/util/csv.js new file mode 100644 index 000000000..79b36bc1e --- /dev/null +++ b/js/util/csv.js @@ -0,0 +1,95 @@ +/** + * Small module for exporting data to CSV. + */ +var _ = require('lodash'); +var preconditions = require('preconditions').singleton(); +var moment = require('../../lib/moment/moment'); + +var logger = require('../util/log.js'); +var config = require('../../config'); + + +var COL_DELIMITER = ','; +var ROW_DELIMITER = '\r\n'; + +function getValue(obj, property) { + if (_.isFunction(property)) { + try { + return property(obj); + } catch (err) { + if (_.isString(err)) return err; + return undefined; + } + } + if (!_.isObject(obj)) return undefined; + return obj.hasOwnProperty(property) ? obj[property] : undefined; +}; + +function formatValue(value, type, format) { + if (_.isUndefined(value) || _.isNull(value)) return ''; + + var r; + switch (type) { + default: + case 'string': + r = value.toString(); + r.replace('"', '\\"'); + break; + case 'date': + r = moment(value).format(format); + break; + case 'number': + r = value.toString(); + break; + } + + // escape when commas in values + if (r.indexOf(',') !== -1) { + r = '"' + r + '"'; + } + return r; +}; + +function getHeader(descriptor) { + return _.map(descriptor.columns, function (col) { + return col.label || (_.isString(col.property) ? col.property : '') || ''; + }); +}; + +function processDataRow(data, descriptor) { + return _.map(descriptor.columns, function (col) { + var value = getValue(data, col.property); + var formatted = formatValue(value, col.type, col.format); + return formatted; + }); +}; + +/** + * @desc Convert json object to csv based on a descriptor + * + * @param {array} data - the array of json objects to convert to csv + * @param {object} descriptor - an object that parameterizes the conversion + * @param {function} cb - called with the resulting csv + */ +module.exports.toCsv = function(data, descriptor, cb) { + preconditions.shouldBeArray(data); + preconditions.shouldBeObject(descriptor); + preconditions.shouldBeArray(descriptor.columns); + preconditions.shouldBeFunction(cb); + + var colDelimiter = descriptor.colDelimiter || COL_DELIMITER; + var rowDelimiter = descriptor.rowDelimiter || ROW_DELIMITER; + + var rows = _.map(data, function (dataRow) { + return processDataRow(dataRow, descriptor); + }); + + var header = getHeader(descriptor); + rows.unshift(header); + + var csv = _.reduce(rows, function (memo, row) { + return memo + row.join(colDelimiter) + rowDelimiter; + }, ''); + + return cb(null, csv); +}; diff --git a/test/util.csv.js b/test/util.csv.js new file mode 100644 index 000000000..114e9c2b4 --- /dev/null +++ b/test/util.csv.js @@ -0,0 +1,166 @@ +'use strict'; + +var _ = require('lodash'); +var chai = chai || require('chai'); +var sinon = sinon || require('sinon'); +var should = chai.should(); + +var csv = require('../js/util/csv') +var moment = require('../lib/moment/moment'); + +describe('csv utils', function() { + it('should convert simple json', function(done) { + var data = [ + { name: 'Lennon John', age: 40, lastLogin: moment(1417608870000), }, + { name: 'Cobain, Kurt', age: 27, lastLogin: moment('2014-11-01'), }, + ]; + + var descriptor = { + columns: [ + { label: 'Name', property: 'name', type: 'string' }, + { label: 'Age', property: 'age', type: 'number' }, + { property: 'lastLogin', type: 'date' }, + ], + }; + + csv.toCsv(data, descriptor, function (err, res) { + res.should.equal('Name,Age,lastLogin\r\nLennon John,40,2014-12-03T09:14:30-03:00\r\n"Cobain, Kurt",27,2014-11-01T00:00:00-03:00\r\n'); + done(); + }); + }); + it('should handle empty data', function(done) { + var data = []; + + var descriptor = { + columns: [ + { label: 'Name', property: 'name', type: 'string' }, + { label: 'Age', property: 'age', type: 'number' }, + { property: 'lastLogin', type: 'date' }, + ], + }; + + csv.toCsv(data, descriptor, function (err, res) { + res.should.equal('Name,Age,lastLogin\r\n'); + done(); + }); + }); + it('should handle null row in data', function(done) { + var data = [ + { name: 'John', age: 40 }, + null, + { name: 'Kurt', age: 27 }, + ]; + + var descriptor = { + columns: [ + { label: 'Name', property: 'name', type: 'string' }, + { label: 'Age', property: 'age', type: 'number' }, + ], + }; + + csv.toCsv(data, descriptor, function (err, res) { + res.should.equal('Name,Age\r\nJohn,40\r\n,\r\nKurt,27\r\n'); + done(); + }); + }); + it('should format dates', function(done) { + var data = [ + { name: 'John', age: 40, lastLogin: moment(1417608870000), }, + { name: 'Kurt', age: 27, lastLogin: moment('2014-11-01'), }, + ]; + + var descriptor = { + columns: [ + { property: 'name', type: 'string' }, + { property: 'age', type: 'number' }, + { property: 'lastLogin', type: 'date', format: 'YYYY MM DD' }, + ], + }; + + csv.toCsv(data, descriptor, function (err, res) { + res.should.equal('name,age,lastLogin\r\nJohn,40,2014 12 03\r\nKurt,27,2014 11 01\r\n'); + done(); + }); + }); + it('should compute values from function properties', function(done) { + var data = [ + { name: 'John', payments: 400, withdrawals: 300, }, + { name: 'Kurt', payments: 270.5, withdrawals: 200, }, + ]; + + var descriptor = { + columns: [ + { property: 'name', type: 'string' }, + { label: 'Balance', property: function (obj) { return obj.payments - obj.withdrawals; }, type: 'number' }, + ], + }; + + csv.toCsv(data, descriptor, function (err, res) { + res.should.equal('name,Balance\r\nJohn,100\r\nKurt,70.5\r\n'); + done(); + }); + }); + it('should not fail on error from calculated values', function(done) { + var data = [ + { name: 'John', error: 0 }, + { name: 'Kurt', error: 1 }, + ]; + + var descriptor = { + columns: [{ + property: 'name', + type: 'string' + }, { + label: 'Error', + property: function(obj) { + if (obj.error) { + throw 'dummy error'; + } else { + return 'ok'; + } + } + }, ], + }; + csv.toCsv(data, descriptor, function (err, res) { + res.should.equal('name,Error\r\nJohn,ok\r\nKurt,dummy error\r\n'); + done(); + }); + }); + it('should use blank label if label not specified for computed property', function(done) { + var data = [ + { name: 'John', payments: 400, withdrawals: 300, }, + { name: 'Kurt', payments: 270.5, withdrawals: 200, }, + ]; + + var descriptor = { + columns: [ + { property: 'name', type: 'string' }, + { property: function (obj) { return obj.payments - obj.withdrawals; }, type: 'number' }, + ], + }; + + csv.toCsv(data, descriptor, function (err, res) { + res.should.equal('name,\r\nJohn,100\r\nKurt,70.5\r\n'); + done(); + }); + }); + it('should handle non existent properties', function(done) { + var data = [ + { name: 'John', age: 40 }, + { name: 'Kurt', age: 27 }, + ]; + + var descriptor = { + columns: [ + { property: 'name', type: 'string' }, + { property: 'age', type: 'number' }, + { property: 'lastLogin', type: 'date', format: 'YYYY MM DD' }, + ], + }; + + csv.toCsv(data, descriptor, function (err, res) { + res.should.equal('name,age,lastLogin\r\nJohn,40,\r\nKurt,27,\r\n'); + done(); + }); + }); +});