diff --git a/.gitignore b/.gitignore index b8b19f1..bfe5283 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ node_modules build lib test -coverage \ No newline at end of file +coverage +/.nyc_output/ +*.tgz diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..e610bed --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +registry=http://registry.npmjs.com diff --git a/package.json b/package.json index a3af98f..1195342 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "excel-as-json", - "version": "2.0.2", + "name": "excel-as-json2", + "version": "0.1.0-dev.0", "description": "Convert Excel data to JSON", "author": "Steve Tarver ", "license": "MIT", @@ -28,15 +28,11 @@ }, "homepage": "https://github.com/stevetarver/excel-as-json", "dependencies": { - "excel": "0.1.7" + "exceljs": "^3.9.0" }, "devDependencies": { "chai": "4.1.2", - "codecov.io": "0.1.6", - "coffee-coverage": "3.0.0", - "coffeescript": "2.2.4", - "coveralls": "3.0.0", - "istanbul": "0.4.5", - "mocha": "5.1.0" + "mocha": "5.1.0", + "nyc": "^15.0.1" } } diff --git a/spec/all-specs.coffee b/spec/all-specs.coffee deleted file mode 100644 index 848cda2..0000000 --- a/spec/all-specs.coffee +++ /dev/null @@ -1,8 +0,0 @@ -require './assignSpec' -require './convertSpec' -require './convertValueSpec' -require './parseKeyNameSpec' -require './transposeSpec' -require './validateOptionsSpec' -require './processFileSpec' -require './regressionSpec' diff --git a/spec/all-specs.js b/spec/all-specs.js new file mode 100644 index 0000000..da9bd35 --- /dev/null +++ b/spec/all-specs.js @@ -0,0 +1,8 @@ +require('./assignSpec'); +require('./convertSpec'); +require('./convertValueSpec'); +require('./parseKeyNameSpec'); +require('./transposeSpec'); +require('./validateOptionsSpec'); +require('./processFileSpec'); +require('./regressionSpec'); diff --git a/spec/assignSpec.coffee b/spec/assignSpec.coffee deleted file mode 100644 index 93f1f63..0000000 --- a/spec/assignSpec.coffee +++ /dev/null @@ -1,154 +0,0 @@ -assign = require('../src/excel-as-json').assign - -# TODO: How to get chai defined in a more global way -chai = require 'chai' -chai.should() -expect = chai.expect; - -# NOTE: the excel package uses '' for all empty cells -EMPTY_CELL = '' -DEFAULT_OPTIONS = - omitEmptyFields: false - convertTextToNumber: true - - -describe 'assign', -> - - it 'should assign first level properties', -> - subject = {} - assign subject, 'foo', 'clyde', DEFAULT_OPTIONS - subject.foo.should.equal 'clyde' - - - it 'should assign second level properties', -> - subject = {} - assign subject, 'foo.bar', 'wombat', DEFAULT_OPTIONS - subject.foo.bar.should.equal 'wombat' - - - it 'should assign third level properties', -> - subject = {} - assign subject, 'foo.bar.bazz', 'honey badger', DEFAULT_OPTIONS - subject.foo.bar.bazz.should.equal 'honey badger' - - - it 'should convert text to numbers', -> - subject = {} - assign subject, 'foo.bar.bazz', '42', DEFAULT_OPTIONS - subject.foo.bar.bazz.should.equal 42 - - - it 'should convert text to booleans', -> - subject = {} - assign subject, 'foo.bar.bazz', 'true', DEFAULT_OPTIONS - subject.foo.bar.bazz.should.equal true - assign subject, 'foo.bar.bazz', 'false', DEFAULT_OPTIONS - subject.foo.bar.bazz.should.equal false - - - it 'should overwrite existing values', -> - subject = {} - assign subject, 'foo.bar.bazz', 'honey badger', DEFAULT_OPTIONS - subject.foo.bar.bazz.should.equal 'honey badger' - assign subject, 'foo.bar.bazz', "don't care", DEFAULT_OPTIONS - subject.foo.bar.bazz.should.equal "don't care" - - - it 'should assign properties to objects in a list', -> - subject = {} - assign subject, 'foo.bar[0].what', 'that', DEFAULT_OPTIONS - subject.foo.bar[0].what.should.equal 'that' - - - it 'should assign properties to objects in a list with first entry out of order', -> - subject = {} - assign subject, 'foo.bar[1].what', 'that', DEFAULT_OPTIONS - assign subject, 'foo.bar[0].what', 'this', DEFAULT_OPTIONS - subject.foo.bar[0].what.should.equal 'this' - subject.foo.bar[1].what.should.equal 'that' - - - it 'should assign properties to objects in a list with second entry out of order', -> - subject = {} - assign subject, 'foo.bar[0].what', 'this', DEFAULT_OPTIONS - assign subject, 'foo.bar[2].what', 'that', DEFAULT_OPTIONS - assign subject, 'foo.bar[1].what', 'other', DEFAULT_OPTIONS - subject.foo.bar[0].what.should.equal 'this' - subject.foo.bar[2].what.should.equal 'that' - subject.foo.bar[1].what.should.equal 'other' - - - it 'should split a semicolon delimited list for flat arrays', -> - subject = {} - assign subject, 'foo.bar[]', 'peter;paul;mary', DEFAULT_OPTIONS - subject.foo.bar.toString().should.equal ['peter','paul','mary'].toString() - - - it 'should convert text in a semicolon delimited list to numbers', -> - subject = {} - assign subject, 'foo.bar[]', 'peter;-43;mary', DEFAULT_OPTIONS - subject.foo.bar.toString().should.equal ['peter',-43,'mary'].toString() - - - it 'should convert text in a semicolon delimited list to booleans', -> - subject = {} - assign subject, 'foo.bar[]', 'peter;false;true', DEFAULT_OPTIONS - subject.foo.bar.toString().should.equal ['peter',false,true].toString() - - - it 'should not split a semicolon list with a terminal indexed array', -> - subject = {} - console.log('Note: warnings on this test expected') - assign subject, 'foo.bar[0]', 'peter;paul;mary', DEFAULT_OPTIONS - subject.foo.bar.should.equal 'peter;paul;mary' - - - it 'should omit empty scalar fields when directed', -> - o = - omitEmptyFields: true - convertTextToNumber: true - subject = {} - assign subject, 'foo', EMPTY_CELL, o - subject.should.not.have.property 'foo' - - - it 'should omit empty nested scalar fields when directed', -> - o = - omitEmptyFields: true - convertTextToNumber: true - subject = {} - assign subject, 'foo.bar', EMPTY_CELL, o - subject.should.have.property 'foo' - subject.foo.should.not.have.property 'bar' - - - it 'should omit nested array fields when directed', -> - o = - omitEmptyFields: true - convertTextToNumber: true - - # specified as an entire list - subject = {} - console.log('Note: warnings on this test expected') - assign subject, 'foo[]', EMPTY_CELL, o - subject.should.not.have.property 'foo' - - # specified as a list - subject = {} - assign subject, 'foo[0]', EMPTY_CELL, o - subject.should.not.have.property 'foo' - - # specified as a list of objects - subject = {} - assign subject, 'foo[0].bar', 'bazz', o - assign subject, 'foo[1].bar', EMPTY_CELL, o - subject.foo[1].should.not.have.property 'bar' - - - it 'should treat text that looks like numbers as text when directed', -> - o = - convertTextToNumber: false - - subject = {} - assign subject, 'part', '00938', o - subject.part.should.be.a('string').and.equal('00938') diff --git a/spec/assignSpec.js b/spec/assignSpec.js new file mode 100644 index 0000000..ecaa4a5 --- /dev/null +++ b/spec/assignSpec.js @@ -0,0 +1,185 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { + assign +} = require('../src/excel-as-json'); + +// TODO: How to get chai defined in a more global way +const chai = require('chai'); +chai.should(); +const { + expect +} = chai; + +// NOTE: the excel package uses '' for all empty cells +const EMPTY_CELL = ''; +const DEFAULT_OPTIONS = { + omitEmptyFields: false, + convertTextToNumber: true +}; + + +describe('assign', function() { + + it('should assign first level properties', function() { + const subject = {}; + assign(subject, 'foo', 'clyde', DEFAULT_OPTIONS); + return subject.foo.should.equal('clyde'); + }); + + + it('should assign second level properties', function() { + const subject = {}; + assign(subject, 'foo.bar', 'wombat', DEFAULT_OPTIONS); + return subject.foo.bar.should.equal('wombat'); + }); + + + it('should assign third level properties', function() { + const subject = {}; + assign(subject, 'foo.bar.bazz', 'honey badger', DEFAULT_OPTIONS); + return subject.foo.bar.bazz.should.equal('honey badger'); + }); + + + it('should convert text to numbers', function() { + const subject = {}; + assign(subject, 'foo.bar.bazz', '42', DEFAULT_OPTIONS); + return subject.foo.bar.bazz.should.equal(42); + }); + + + it('should convert text to booleans', function() { + const subject = {}; + assign(subject, 'foo.bar.bazz', 'true', DEFAULT_OPTIONS); + subject.foo.bar.bazz.should.equal(true); + assign(subject, 'foo.bar.bazz', 'false', DEFAULT_OPTIONS); + return subject.foo.bar.bazz.should.equal(false); + }); + + + it('should overwrite existing values', function() { + const subject = {}; + assign(subject, 'foo.bar.bazz', 'honey badger', DEFAULT_OPTIONS); + subject.foo.bar.bazz.should.equal('honey badger'); + assign(subject, 'foo.bar.bazz', "don't care", DEFAULT_OPTIONS); + return subject.foo.bar.bazz.should.equal("don't care"); + }); + + + it('should assign properties to objects in a list', function() { + const subject = {}; + assign(subject, 'foo.bar[0].what', 'that', DEFAULT_OPTIONS); + return subject.foo.bar[0].what.should.equal('that'); + }); + + + it('should assign properties to objects in a list with first entry out of order', function() { + const subject = {}; + assign(subject, 'foo.bar[1].what', 'that', DEFAULT_OPTIONS); + assign(subject, 'foo.bar[0].what', 'this', DEFAULT_OPTIONS); + subject.foo.bar[0].what.should.equal('this'); + return subject.foo.bar[1].what.should.equal('that'); + }); + + + it('should assign properties to objects in a list with second entry out of order', function() { + const subject = {}; + assign(subject, 'foo.bar[0].what', 'this', DEFAULT_OPTIONS); + assign(subject, 'foo.bar[2].what', 'that', DEFAULT_OPTIONS); + assign(subject, 'foo.bar[1].what', 'other', DEFAULT_OPTIONS); + subject.foo.bar[0].what.should.equal('this'); + subject.foo.bar[2].what.should.equal('that'); + return subject.foo.bar[1].what.should.equal('other'); + }); + + + it('should split a semicolon delimited list for flat arrays', function() { + const subject = {}; + assign(subject, 'foo.bar[]', 'peter;paul;mary', DEFAULT_OPTIONS); + return subject.foo.bar.toString().should.equal(['peter','paul','mary'].toString()); + }); + + + it('should convert text in a semicolon delimited list to numbers', function() { + const subject = {}; + assign(subject, 'foo.bar[]', 'peter;-43;mary', DEFAULT_OPTIONS); + return subject.foo.bar.toString().should.equal(['peter',-43,'mary'].toString()); + }); + + + it('should convert text in a semicolon delimited list to booleans', function() { + const subject = {}; + assign(subject, 'foo.bar[]', 'peter;false;true', DEFAULT_OPTIONS); + return subject.foo.bar.toString().should.equal(['peter',false,true].toString()); + }); + + + it('should not split a semicolon list with a terminal indexed array', function() { + const subject = {}; + console.log('Note: warnings on this test expected'); + assign(subject, 'foo.bar[0]', 'peter;paul;mary', DEFAULT_OPTIONS); + return subject.foo.bar.should.equal('peter;paul;mary'); + }); + + + it('should omit empty scalar fields when directed', function() { + const o = { + omitEmptyFields: true, + convertTextToNumber: true + }; + const subject = {}; + assign(subject, 'foo', EMPTY_CELL, o); + return subject.should.not.have.property('foo'); + }); + + + it('should omit empty nested scalar fields when directed', function() { + const o = { + omitEmptyFields: true, + convertTextToNumber: true + }; + const subject = {}; + assign(subject, 'foo.bar', EMPTY_CELL, o); + subject.should.have.property('foo'); + return subject.foo.should.not.have.property('bar'); + }); + + + it('should omit nested array fields when directed', function() { + const o = { + omitEmptyFields: true, + convertTextToNumber: true + }; + + // specified as an entire list + let subject = {}; + console.log('Note: warnings on this test expected'); + assign(subject, 'foo[]', EMPTY_CELL, o); + subject.should.not.have.property('foo'); + + // specified as a list + subject = {}; + assign(subject, 'foo[0]', EMPTY_CELL, o); + subject.should.not.have.property('foo'); + + // specified as a list of objects + subject = {}; + assign(subject, 'foo[0].bar', 'bazz', o); + assign(subject, 'foo[1].bar', EMPTY_CELL, o); + return subject.foo[1].should.not.have.property('bar'); + }); + + + return it('should treat text that looks like numbers as text when directed', function() { + const o = + {convertTextToNumber: false}; + + const subject = {}; + assign(subject, 'part', '00938', o); + return subject.part.should.be.a('string').and.equal('00938'); + }); +}); diff --git a/spec/convertSpec.coffee b/spec/convertSpec.coffee deleted file mode 100644 index 237bf21..0000000 --- a/spec/convertSpec.coffee +++ /dev/null @@ -1,166 +0,0 @@ -convert = require('../src/excel-as-json').convert - -# TODO: How to get chai defined in a more global way -chai = require 'chai' -chai.should() -expect = chai.expect; - -DEFAULT_OPTIONS = - isColOriented: false - omitEmptyFields: false - convertTextToNumber: true - -describe 'convert', -> - - it 'should convert a row to a list of object', -> - data = [ - ['a', 'b', 'c' ], - [ 1, 2, 'true' ]] - result = convert data, DEFAULT_OPTIONS - JSON.stringify(result).should.equal '[{"a":1,"b":2,"c":true}]' - - - it 'should convert rows to a list of objects', -> - data = [ - ['a', 'b', 'c'], - [ 1, 2, 3 ], - [ 4, 5, 6 ]] - result = convert data, DEFAULT_OPTIONS - JSON.stringify(result).should.equal '[{"a":1,"b":2,"c":3},{"a":4,"b":5,"c":6}]' - - - it 'should convert rows to a list of objects, omitting empty values', -> - o = - isColOriented: false - omitEmptyFields: true - data = [ - ['a', 'b', 'c'], - [ 1, '', 3 ], - [ '', 5, 6 ], - [ '', 5, '' ]] - result = convert data, o - JSON.stringify(result).should.equal '[{"a":1,"c":3},{"b":5,"c":6},{"b":5}]' - - - it 'should convert a column to list of object', -> - o = - isColOriented: true - omitEmptyFields: false - data = [['a', 1], - ['b', 2], - ['c', 3]] - result = convert data, o - JSON.stringify(result).should.equal '[{"a":1,"b":2,"c":3}]' - - - it 'should convert columns to list of objects', -> - o = - isColOriented: true - omitEmptyFields: false - data = [['a', 1, 4 ], - ['b', 2, 5 ], - ['c', 3, 6 ]] - result = convert data, o - JSON.stringify(result).should.equal '[{"a":1,"b":2,"c":3},{"a":4,"b":5,"c":6}]' - - - it 'should understand dotted key paths with 2 elements', -> - data = [ - ['a', 'b.a', 'b.b'], - [ 1, 2, 3 ], - [ 4, 5, 6 ]] - result = convert data, DEFAULT_OPTIONS - JSON.stringify(result).should.equal '[{"a":1,"b":{"a":2,"b":3}},{"a":4,"b":{"a":5,"b":6}}]' - - - it 'should understand dotted key paths with 2 elements and omit elements appropriately', -> - o = - isColOriented: false - omitEmptyFields: true - data = [ - ['a', 'b.a', 'b.b'], - [ 1, 2, 3 ], - [ '', 5, '' ]] - result = convert data, o - JSON.stringify(result).should.equal '[{"a":1,"b":{"a":2,"b":3}},{"b":{"a":5}}]' - - - it 'should understand dotted key paths with 3 elements', -> - data = [['a', 'b.a.b', 'c'], - [ 1, 2, 3 ], - [ 4, 5, 6 ]] - result = convert data, DEFAULT_OPTIONS - JSON.stringify(result).should.equal '[{"a":1,"b":{"a":{"b":2}},"c":3},{"a":4,"b":{"a":{"b":5}},"c":6}]' - - - it 'should understand indexed arrays in dotted paths', -> - data = [['a[0].a', 'b.a.b', 'c'], - [ 1, 2, 3 ], - [ 4, 5, 6 ]] - result = convert data, DEFAULT_OPTIONS - JSON.stringify(result).should.equal '[{"a":[{"a":1}],"b":{"a":{"b":2}},"c":3},{"a":[{"a":4}],"b":{"a":{"b":5}},"c":6}]' - - - it 'should understand indexed arrays in dotted paths', -> - data = [['a[0].a', 'a[0].b', 'c'], - [ 1, 2, 3 ], - [ 4, 5, 6 ]] - result = convert data, DEFAULT_OPTIONS - JSON.stringify(result).should.equal '[{"a":[{"a":1,"b":2}],"c":3},{"a":[{"a":4,"b":5}],"c":6}]' - - - it 'should understand indexed arrays when out of order', -> - data = [['a[1].a', 'a[0].a', 'c'], - [ 1, 2, 3 ], - [ 4, 5, 6 ]] - result = convert data, DEFAULT_OPTIONS - JSON.stringify(result).should.equal '[{"a":[{"a":2},{"a":1}],"c":3},{"a":[{"a":5},{"a":4}],"c":6}]' - - - it 'should understand indexed arrays in deep dotted paths', -> - data = [['a[0].a', 'b.a[0].b', 'c.a.b[0].d'], - [ 1, 2, 3 ], - [ 4, 5, 6 ]] - result = convert data, DEFAULT_OPTIONS - JSON.stringify(result).should.equal '[{"a":[{"a":1}],"b":{"a":[{"b":2}]},"c":{"a":{"b":[{"d":3}]}}},{"a":[{"a":4}],"b":{"a":[{"b":5}]},"c":{"a":{"b":[{"d":6}]}}}]' - - - it 'should understand flat arrays as terminal key names', -> - data = [['a[]', 'b.a[]', 'c.a.b[]'], - ['a;b', 'c;d', 'e;f' ], - ['g;h', 'i;j', 'k;l' ]] - result = convert data, DEFAULT_OPTIONS - JSON.stringify(result).should.equal '[{"a":["a","b"],"b":{"a":["c","d"]},"c":{"a":{"b":["e","f"]}}},{"a":["g","h"],"b":{"a":["i","j"]},"c":{"a":{"b":["k","l"]}}}]' - - - it 'should convert text to numbers where appropriate', -> - data = [[ 'a', 'b', 'c' ], - [ '-99', 'test', '2e64']] - result = convert data, DEFAULT_OPTIONS - JSON.stringify(result).should.equal '[{"a":-99,"b":"test","c":2e+64}]' - - - it 'should not convert text that looks like numbers to numbers when directed', -> - o = - convertTextToNumber: false - - data = [[ 'a', 'b', 'c', ], - [ '-99', '00938', '02e64' ]] - result = convert data, o - result[0].should.have.property('a', '-99') - result[0].should.have.property('b', '00938') - result[0].should.have.property('c', '02e64') - - - it 'should not convert numbers to text when convertTextToNumber = false', -> - o = - convertTextToNumber: false - - data = [[ 'a', 'b', 'c', 'd' ], - [ -99, 938, 2e64, 0x4aa ]] - result = convert data, o - result[0].should.have.property('a', -99) - result[0].should.have.property('b', 938) - result[0].should.have.property('c', 2e+64) - result[0].should.have.property('d', 1194) - diff --git a/spec/convertSpec.js b/spec/convertSpec.js new file mode 100644 index 0000000..54ce735 --- /dev/null +++ b/spec/convertSpec.js @@ -0,0 +1,197 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { + convert +} = require('../src/excel-as-json'); + +// TODO: How to get chai defined in a more global way +const chai = require('chai'); +chai.should(); +const { + expect +} = chai; + +const DEFAULT_OPTIONS = { + isColOriented: false, + omitEmptyFields: false, + convertTextToNumber: true +}; + +describe('convert', function() { + + it('should convert a row to a list of object', function() { + const data = [ + ['a', 'b', 'c' ], + [ 1, 2, 'true' ]]; + const result = convert(data, DEFAULT_OPTIONS); + return JSON.stringify(result).should.equal('[{"a":1,"b":2,"c":true}]'); + }); + + + it('should convert rows to a list of objects', function() { + const data = [ + ['a', 'b', 'c'], + [ 1, 2, 3 ], + [ 4, 5, 6 ]]; + const result = convert(data, DEFAULT_OPTIONS); + return JSON.stringify(result).should.equal('[{"a":1,"b":2,"c":3},{"a":4,"b":5,"c":6}]'); + }); + + + it('should convert rows to a list of objects, omitting empty values', function() { + const o = { + isColOriented: false, + omitEmptyFields: true + }; + const data = [ + ['a', 'b', 'c'], + [ 1, '', 3 ], + [ '', 5, 6 ], + [ '', 5, '' ]]; + const result = convert(data, o); + return JSON.stringify(result).should.equal('[{"a":1,"c":3},{"b":5,"c":6},{"b":5}]'); + }); + + + it('should convert a column to list of object', function() { + const o = { + isColOriented: true, + omitEmptyFields: false + }; + const data = [['a', 1], + ['b', 2], + ['c', 3]]; + const result = convert(data, o); + return JSON.stringify(result).should.equal('[{"a":1,"b":2,"c":3}]'); + }); + + + it('should convert columns to list of objects', function() { + const o = { + isColOriented: true, + omitEmptyFields: false + }; + const data = [['a', 1, 4 ], + ['b', 2, 5 ], + ['c', 3, 6 ]]; + const result = convert(data, o); + return JSON.stringify(result).should.equal('[{"a":1,"b":2,"c":3},{"a":4,"b":5,"c":6}]'); + }); + + + it('should understand dotted key paths with 2 elements', function() { + const data = [ + ['a', 'b.a', 'b.b'], + [ 1, 2, 3 ], + [ 4, 5, 6 ]]; + const result = convert(data, DEFAULT_OPTIONS); + return JSON.stringify(result).should.equal('[{"a":1,"b":{"a":2,"b":3}},{"a":4,"b":{"a":5,"b":6}}]'); + }); + + + it('should understand dotted key paths with 2 elements and omit elements appropriately', function() { + const o = { + isColOriented: false, + omitEmptyFields: true + }; + const data = [ + ['a', 'b.a', 'b.b'], + [ 1, 2, 3 ], + [ '', 5, '' ]]; + const result = convert(data, o); + return JSON.stringify(result).should.equal('[{"a":1,"b":{"a":2,"b":3}},{"b":{"a":5}}]'); + }); + + + it('should understand dotted key paths with 3 elements', function() { + const data = [['a', 'b.a.b', 'c'], + [ 1, 2, 3 ], + [ 4, 5, 6 ]]; + const result = convert(data, DEFAULT_OPTIONS); + return JSON.stringify(result).should.equal('[{"a":1,"b":{"a":{"b":2}},"c":3},{"a":4,"b":{"a":{"b":5}},"c":6}]'); + }); + + + it('should understand indexed arrays in dotted paths', function() { + const data = [['a[0].a', 'b.a.b', 'c'], + [ 1, 2, 3 ], + [ 4, 5, 6 ]]; + const result = convert(data, DEFAULT_OPTIONS); + return JSON.stringify(result).should.equal('[{"a":[{"a":1}],"b":{"a":{"b":2}},"c":3},{"a":[{"a":4}],"b":{"a":{"b":5}},"c":6}]'); + }); + + + it('should understand indexed arrays in dotted paths', function() { + const data = [['a[0].a', 'a[0].b', 'c'], + [ 1, 2, 3 ], + [ 4, 5, 6 ]]; + const result = convert(data, DEFAULT_OPTIONS); + return JSON.stringify(result).should.equal('[{"a":[{"a":1,"b":2}],"c":3},{"a":[{"a":4,"b":5}],"c":6}]'); + }); + + + it('should understand indexed arrays when out of order', function() { + const data = [['a[1].a', 'a[0].a', 'c'], + [ 1, 2, 3 ], + [ 4, 5, 6 ]]; + const result = convert(data, DEFAULT_OPTIONS); + return JSON.stringify(result).should.equal('[{"a":[{"a":2},{"a":1}],"c":3},{"a":[{"a":5},{"a":4}],"c":6}]'); + }); + + + it('should understand indexed arrays in deep dotted paths', function() { + const data = [['a[0].a', 'b.a[0].b', 'c.a.b[0].d'], + [ 1, 2, 3 ], + [ 4, 5, 6 ]]; + const result = convert(data, DEFAULT_OPTIONS); + return JSON.stringify(result).should.equal('[{"a":[{"a":1}],"b":{"a":[{"b":2}]},"c":{"a":{"b":[{"d":3}]}}},{"a":[{"a":4}],"b":{"a":[{"b":5}]},"c":{"a":{"b":[{"d":6}]}}}]'); + }); + + + it('should understand flat arrays as terminal key names', function() { + const data = [['a[]', 'b.a[]', 'c.a.b[]'], + ['a;b', 'c;d', 'e;f' ], + ['g;h', 'i;j', 'k;l' ]]; + const result = convert(data, DEFAULT_OPTIONS); + return JSON.stringify(result).should.equal('[{"a":["a","b"],"b":{"a":["c","d"]},"c":{"a":{"b":["e","f"]}}},{"a":["g","h"],"b":{"a":["i","j"]},"c":{"a":{"b":["k","l"]}}}]'); + }); + + + it('should convert text to numbers where appropriate', function() { + const data = [[ 'a', 'b', 'c' ], + [ '-99', 'test', '2e64']]; + const result = convert(data, DEFAULT_OPTIONS); + return JSON.stringify(result).should.equal('[{"a":-99,"b":"test","c":2e+64}]'); + }); + + + it('should not convert text that looks like numbers to numbers when directed', function() { + const o = + {convertTextToNumber: false}; + + const data = [[ 'a', 'b', 'c', ], + [ '-99', '00938', '02e64' ]]; + const result = convert(data, o); + result[0].should.have.property('a', '-99'); + result[0].should.have.property('b', '00938'); + return result[0].should.have.property('c', '02e64'); + }); + + + return it('should not convert numbers to text when convertTextToNumber = false', function() { + const o = + {convertTextToNumber: false}; + + const data = [[ 'a', 'b', 'c', 'd' ], + [ -99, 938, 2e64, 0x4aa ]]; + const result = convert(data, o); + result[0].should.have.property('a', -99); + result[0].should.have.property('b', 938); + result[0].should.have.property('c', 2e+64); + return result[0].should.have.property('d', 1194); + }); +}); + diff --git a/spec/convertValueSpec.coffee b/spec/convertValueSpec.coffee deleted file mode 100644 index 0afbe4b..0000000 --- a/spec/convertValueSpec.coffee +++ /dev/null @@ -1,68 +0,0 @@ -convertValue = require('../src/excel-as-json').convertValue - -# TODO: How to get chai defined in a more global way -chai = require 'chai' -chai.should() -expect = chai.expect; - -OPTIONS = - sheet: '1' - isColOriented: false - omitEmptyFields: false - omitKeysWithEmptyValues: false - convertTextToNumber: true - - -describe 'convert value', -> - - it 'should convert text integers to literal numbers', -> - convertValue('1000', OPTIONS).should.be.a('number').and.equal(1000) - convertValue('-999', OPTIONS).should.be.a('number').and.equal(-999) - - - it 'should convert text floats to literal numbers', -> - convertValue('999.0', OPTIONS).should.be.a('number').and.equal(999.0) - convertValue('-100.0', OPTIONS).should.be.a('number').and.equal(-100.0) - - - it 'should convert text exponential numbers to literal numbers', -> - convertValue('2e32', OPTIONS).should.be.a('number').and.equal(2e+32) - - - it 'should not convert things that are not numbers', -> - convertValue('test', OPTIONS).should.be.a('string').and.equal('test') - - - it 'should convert true and false to Boolean', -> - convertValue('true', OPTIONS).should.be.a('boolean').and.equal(true) - convertValue('TRUE', OPTIONS).should.be.a('boolean').and.equal(true) - convertValue('TrUe', OPTIONS).should.be.a('boolean').and.equal(true) - convertValue('false', OPTIONS).should.be.a('boolean').and.equal(false) - convertValue('FALSE', OPTIONS).should.be.a('boolean').and.equal(false) - convertValue('fAlSe', OPTIONS).should.be.a('boolean').and.equal(false) - - - it 'should return blank strings as strings', -> - convertValue('', OPTIONS).should.be.a('string').and.equal('') - convertValue(' ', OPTIONS).should.be.a('string').and.equal(' ') - - - it 'should treat text that looks like numbers as text when directed', -> - o = - convertTextToNumber: false - - convertValue('999.0', o).should.be.a('string').and.equal('999.0') - convertValue('-100.0', o).should.be.a('string').and.equal('-100.0') - convertValue('2e32', o).should.be.a('string').and.equal('2e32') - convertValue('00956', o).should.be.a('string').and.equal('00956') - - - it 'should not convert numbers to text when convertTextToNumber = false', -> - o = - convertTextToNumber: false - - convertValue(999.0, o).should.be.a('number').and.equal(999.0) - convertValue(-100.0, o).should.be.a('number').and.equal(-100.0) - convertValue(2e+32, o).should.be.a('number').and.equal(2e+32) - convertValue(956, o).should.be.a('number').and.equal(956) - convertValue(0x4aa, o).should.be.a('number').and.equal(1194) diff --git a/spec/convertValueSpec.js b/spec/convertValueSpec.js new file mode 100644 index 0000000..ee8d7ab --- /dev/null +++ b/spec/convertValueSpec.js @@ -0,0 +1,83 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { + convertValue +} = require('../src/excel-as-json'); + +// TODO: How to get chai defined in a more global way +const chai = require('chai'); +chai.should(); +const { + expect +} = chai; + +const OPTIONS = { + sheet: '1', + isColOriented: false, + omitEmptyFields: false, + omitKeysWithEmptyValues: false, + convertTextToNumber: true +}; + + +describe('convert value', function() { + + it('should convert text integers to literal numbers', function() { + convertValue('1000', OPTIONS).should.be.a('number').and.equal(1000); + return convertValue('-999', OPTIONS).should.be.a('number').and.equal(-999); + }); + + + it('should convert text floats to literal numbers', function() { + convertValue('999.0', OPTIONS).should.be.a('number').and.equal(999.0); + return convertValue('-100.0', OPTIONS).should.be.a('number').and.equal(-100.0); + }); + + + it('should convert text exponential numbers to literal numbers', () => convertValue('2e32', OPTIONS).should.be.a('number').and.equal(2e+32)); + + + it('should not convert things that are not numbers', () => convertValue('test', OPTIONS).should.be.a('string').and.equal('test')); + + + it('should convert true and false to Boolean', function() { + convertValue('true', OPTIONS).should.be.a('boolean').and.equal(true); + convertValue('TRUE', OPTIONS).should.be.a('boolean').and.equal(true); + convertValue('TrUe', OPTIONS).should.be.a('boolean').and.equal(true); + convertValue('false', OPTIONS).should.be.a('boolean').and.equal(false); + convertValue('FALSE', OPTIONS).should.be.a('boolean').and.equal(false); + return convertValue('fAlSe', OPTIONS).should.be.a('boolean').and.equal(false); + }); + + + it('should return blank strings as strings', function() { + convertValue('', OPTIONS).should.be.a('string').and.equal(''); + return convertValue(' ', OPTIONS).should.be.a('string').and.equal(' '); + }); + + + it('should treat text that looks like numbers as text when directed', function() { + const o = + {convertTextToNumber: false}; + + convertValue('999.0', o).should.be.a('string').and.equal('999.0'); + convertValue('-100.0', o).should.be.a('string').and.equal('-100.0'); + convertValue('2e32', o).should.be.a('string').and.equal('2e32'); + return convertValue('00956', o).should.be.a('string').and.equal('00956'); + }); + + + return it('should not convert numbers to text when convertTextToNumber = false', function() { + const o = + {convertTextToNumber: false}; + + convertValue(999.0, o).should.be.a('number').and.equal(999.0); + convertValue(-100.0, o).should.be.a('number').and.equal(-100.0); + convertValue(2e+32, o).should.be.a('number').and.equal(2e+32); + convertValue(956, o).should.be.a('number').and.equal(956); + return convertValue(0x4aa, o).should.be.a('number').and.equal(1194); + }); +}); diff --git a/spec/parseKeyNameSpec.coffee b/spec/parseKeyNameSpec.coffee deleted file mode 100644 index b9f4fee..0000000 --- a/spec/parseKeyNameSpec.coffee +++ /dev/null @@ -1,31 +0,0 @@ -parseKeyName = require('../src/excel-as-json').parseKeyName - -# TODO: How to get chai defined in a more global way -chai = require 'chai' -chai.should() -expect = chai.expect; - - -describe 'parse key name', -> - - it 'should parse simple key names', -> - [keyIsList, keyName, index] = parseKeyName 'names' - keyIsList.should.equal false - keyName.should.equal 'names' - expect(index).to.be.an 'undefined' - - - it 'should parse indexed array key names like names[1]', -> - [keyIsList, keyName, index] = parseKeyName 'names[1]' - keyIsList.should.equal true - keyName.should.equal 'names' - index.should.equal 1 - - - it 'should parse array key names like names[]', -> - [keyIsList, keyName, index] = parseKeyName 'names[]' - keyIsList.should.equal true - keyName.should.equal 'names' - expect(index).to.be.an 'undefined' - - diff --git a/spec/parseKeyNameSpec.js b/spec/parseKeyNameSpec.js new file mode 100644 index 0000000..a4891ad --- /dev/null +++ b/spec/parseKeyNameSpec.js @@ -0,0 +1,45 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { + parseKeyName +} = require('../src/excel-as-json'); + +// TODO: How to get chai defined in a more global way +const chai = require('chai'); +chai.should(); +const { + expect +} = chai; + + +describe('parse key name', function() { + + it('should parse simple key names', function() { + const [keyIsList, keyName, index] = Array.from(parseKeyName('names')); + keyIsList.should.equal(false); + keyName.should.equal('names'); + return expect(index).to.be.an('undefined'); + }); + + + it('should parse indexed array key names like names[1]', function() { + const [keyIsList, keyName, index] = Array.from(parseKeyName('names[1]')); + keyIsList.should.equal(true); + keyName.should.equal('names'); + return index.should.equal(1); + }); + + + return it('should parse array key names like names[]', function() { + const [keyIsList, keyName, index] = Array.from(parseKeyName('names[]')); + keyIsList.should.equal(true); + keyName.should.equal('names'); + return expect(index).to.be.an('undefined'); + }); +}); + + diff --git a/spec/processFileSpec.coffee b/spec/processFileSpec.coffee deleted file mode 100644 index 33fb756..0000000 --- a/spec/processFileSpec.coffee +++ /dev/null @@ -1,184 +0,0 @@ -processFile = require('../src/excel-as-json').processFile -fs = require 'fs' - -# TODO: How to get chai defined in a more global way -chai = require 'chai' -chai.should() -expect = chai.expect; - -ROW_XLSX = 'data/row-oriented.xlsx' -ROW_JSON = 'build/row-oriented.json' -COL_XLSX = 'data/col-oriented.xlsx' -COL_JSON = 'build/col-oriented.json' -COL_JSON_NESTED = 'build/newDir/col-oriented.json' - -ROW_SHEET_1_JSON = '[{"firstName":"Jihad","lastName":"Saladin","address":{"street":"12 Beaver Court","city":"Snowmass","state":"CO","zip":81615}},{"firstName":"Marcus","lastName":"Rivapoli","address":{"street":"16 Vail Rd","city":"Vail","state":"CO","zip":81657}}]' -ROW_SHEET_2_JSON = '[{"firstName":"Max","lastName":"Irwin","address":{"street":"123 Fake Street","city":"Rochester","state":"NY","zip":99999}}]' -COL_SHEET_1_JSON = '[{"firstName":"Jihad","lastName":"Saladin","address":{"street":"12 Beaver Court","city":"Snowmass","state":"CO","zip":81615},"isEmployee":true,"phones":[{"type":"home","number":"123.456.7890"},{"type":"work","number":"098.765.4321"}],"aliases":["stormagedden","bob"]},{"firstName":"Marcus","lastName":"Rivapoli","address":{"street":"16 Vail Rd","city":"Vail","state":"CO","zip":81657},"isEmployee":false,"phones":[{"type":"home","number":"123.456.7891"},{"type":"work","number":"098.765.4322"}],"aliases":["mac","markie"]}]' -COL_SHEET_2_JSON = '[{"firstName":"Max","lastName":"Irwin","address":{"street":"123 Fake Street","city":"Rochester","state":"NY","zip":99999},"isEmployee":false,"phones":[{"type":"home","number":"123.456.7890"},{"type":"work","number":"505-505-1010"}],"aliases":["binarymax","arch"]}]' - -TEST_OPTIONS = - sheet: '1' - isColOriented: false - omitEmptyFields: false - - -describe 'process file', -> - - it 'should notify on file does not exist', (done) -> - processFile 'data/doesNotExist.xlsx', null, TEST_OPTIONS, (err, data) -> - err.should.be.a 'string' - expect(data).to.be.an 'undefined' - done() - - - it 'should not blow up when a file does not exist and no callback is provided', (done) -> - processFile 'data/doesNotExist.xlsx', -> - done() - - - it 'should not blow up on read error when no callback is provided', (done) -> - processFile 'data/row-oriented.csv', -> - done() - - - it 'should notify on read error', (done) -> - processFile 'data/row-oriented.csv', null, TEST_OPTIONS, (err, data) -> - err.should.be.a 'string' - expect(data).to.be.an 'undefined' - done() - - - # NOTE: current excel package impl simply times out if sheet index is OOR -# it 'should show error on invalid sheet id', (done) -> -# options = -# sheet: '20' -# isColOriented: false -# omitEmptyFields: false -# -# processFile ROW_XLSX, null, options, (err, data) -> -# err.should.be.a 'string' -# expect(data).to.be.an 'undefined' -# done() - - - it 'should use defaults when caller specifies no options', (done) -> - processFile ROW_XLSX, null, null, (err, data) -> - expect(err).to.be.an 'undefined' - JSON.stringify(data).should.equal ROW_SHEET_1_JSON - done() - - - it 'should process row oriented Excel files, write the result, and return the parsed object', (done) -> - options = - sheet:'1' - isColOriented: false - omitEmptyFields: false - - processFile ROW_XLSX, ROW_JSON, options, (err, data) -> - expect(err).to.be.an 'undefined' - result = JSON.parse(fs.readFileSync(ROW_JSON, 'utf8')) - JSON.stringify(result).should.equal ROW_SHEET_1_JSON - JSON.stringify(data).should.equal ROW_SHEET_1_JSON - done() - - - it 'should process sheet 2 of row oriented Excel files, write the result, and return the parsed object', (done) -> - options = - sheet:'2' - isColOriented: false - omitEmptyFields: false - - processFile ROW_XLSX, ROW_JSON, options, (err, data) -> - expect(err).to.be.an 'undefined' - result = JSON.parse(fs.readFileSync(ROW_JSON, 'utf8')) - JSON.stringify(result).should.equal ROW_SHEET_2_JSON - JSON.stringify(data).should.equal ROW_SHEET_2_JSON - done() - - - it 'should process col oriented Excel files, write the result, and return the parsed object', (done) -> - options = - sheet:'1' - isColOriented: true - omitEmptyFields: false - - processFile COL_XLSX, COL_JSON, options, (err, data) -> - expect(err).to.be.an 'undefined' - result = JSON.parse(fs.readFileSync(COL_JSON, 'utf8')) - JSON.stringify(result).should.equal COL_SHEET_1_JSON - JSON.stringify(data).should.equal COL_SHEET_1_JSON - done() - - - it 'should process sheet 2 of col oriented Excel files, write the result, and return the parsed object', (done) -> - options = - sheet:'2' - isColOriented: true - omitEmptyFields: false - - processFile COL_XLSX, COL_JSON, options, (err, data) -> - expect(err).to.be.an 'undefined' - result = JSON.parse(fs.readFileSync(COL_JSON, 'utf8')) - JSON.stringify(result).should.equal COL_SHEET_2_JSON - JSON.stringify(data).should.equal COL_SHEET_2_JSON - done() - - - it 'should create the destination directory if it does not exist', (done) -> - options = - sheet:'1' - isColOriented: true - omitEmptyFields: false - - processFile COL_XLSX, COL_JSON_NESTED, options, (err, data) -> - expect(err).to.be.an 'undefined' - result = JSON.parse(fs.readFileSync(COL_JSON_NESTED, 'utf8')) - JSON.stringify(result).should.equal COL_SHEET_1_JSON - JSON.stringify(data).should.equal COL_SHEET_1_JSON - done() - - - it 'should return a parsed object without writing a file', (done) -> - # Ensure result file does not exit - try fs.unlinkSync ROW_JSON - catch # ignore file does not exist - - options = - sheet:'1' - isColOriented: false - omitEmptyFields: false - - processFile ROW_XLSX, undefined, options, (err, data) -> - expect(err).to.be.an 'undefined' - fs.existsSync(ROW_JSON).should.equal false - JSON.stringify(data).should.equal ROW_SHEET_1_JSON - done() - - - it 'should not convert text that looks like a number to a number when directed', (done) -> - options = - sheet:'1' - isColOriented: false - omitEmptyFields: false - convertTextToNumber: false - - processFile ROW_XLSX, undefined, options, (err, data) -> - expect(err).to.be.an 'undefined' - data[0].address.should.have.property('zip', '81615') - data[1].address.should.have.property('zip', '81657') - done() - - - it 'should notify on write error', (done) -> - processFile ROW_XLSX, 'build', TEST_OPTIONS, (err, data) -> - expect(err).to.be.an 'string' - done() - - -#=============================== Coverage summary =============================== -# Statements : 100% ( 133/133 ) -# Branches : 100% ( 61/61 ) -# Functions : 100% ( 14/14 ) -# Lines : 100% ( 106/106 ) -#================================================================================ diff --git a/spec/processFileSpec.js b/spec/processFileSpec.js new file mode 100644 index 0000000..6ca506f --- /dev/null +++ b/spec/processFileSpec.js @@ -0,0 +1,218 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { + processFile +} = require('../src/excel-as-json'); +const fs = require('fs'); + +// TODO: How to get chai defined in a more global way +const chai = require('chai'); +chai.should(); +const { + expect +} = chai; + +const ROW_XLSX = 'data/row-oriented.xlsx'; +const ROW_JSON = 'build/row-oriented.json'; +const COL_XLSX = 'data/col-oriented.xlsx'; +const COL_JSON = 'build/col-oriented.json'; +const COL_JSON_NESTED = 'build/newDir/col-oriented.json'; + +const ROW_SHEET_1_JSON = '[{"firstName":"Jihad","lastName":"Saladin","address":{"street":"12 Beaver Court","city":"Snowmass","state":"CO","zip":81615}},{"firstName":"Marcus","lastName":"Rivapoli","address":{"street":"16 Vail Rd","city":"Vail","state":"CO","zip":81657}}]'; +const ROW_SHEET_2_JSON = '[{"firstName":"Max","lastName":"Irwin","address":{"street":"123 Fake Street","city":"Rochester","state":"NY","zip":99999}}]'; +const COL_SHEET_1_JSON = '[{"firstName":"Jihad","lastName":"Saladin","address":{"street":"12 Beaver Court","city":"Snowmass","state":"CO","zip":81615},"isEmployee":true,"phones":[{"type":"home","number":"123.456.7890"},{"type":"work","number":"098.765.4321"}],"aliases":["stormagedden","bob"]},{"firstName":"Marcus","lastName":"Rivapoli","address":{"street":"16 Vail Rd","city":"Vail","state":"CO","zip":81657},"isEmployee":false,"phones":[{"type":"home","number":"123.456.7891"},{"type":"work","number":"098.765.4322"}],"aliases":["mac","markie"]}]'; +const COL_SHEET_2_JSON = '[{"firstName":"Max","lastName":"Irwin","address":{"street":"123 Fake Street","city":"Rochester","state":"NY","zip":99999},"isEmployee":false,"phones":[{"type":"home","number":"123.456.7890"},{"type":"work","number":"505-505-1010"}],"aliases":["binarymax","arch"]}]'; + +const TEST_OPTIONS = { + sheet: '1', + isColOriented: false, + omitEmptyFields: false +}; + + +describe('process file', function() { + + it('should notify on file does not exist', done => processFile('data/doesNotExist.xlsx', null, TEST_OPTIONS, function(err, data) { + err.should.be.a('string'); + expect(data).to.be.an('undefined'); + return done(); + })); + + + it('should not blow up when a file does not exist and no callback is provided', function(done) { + processFile('data/doesNotExist.xlsx', function() {}); + return done(); + }); + + + it('should not blow up on read error when no callback is provided', function(done) { + processFile('data/row-oriented.csv', function() {}); + return done(); + }); + + + it('should notify on read error', done => processFile('data/row-oriented.csv', null, TEST_OPTIONS, function(err, data) { + err.should.be.a('string'); + expect(data).to.be.an('undefined'); + return done(); + })); + + + // NOTE: current excel package impl simply times out if sheet index is OOR +// it 'should show error on invalid sheet id', (done) -> +// options = +// sheet: '20' +// isColOriented: false +// omitEmptyFields: false +// +// processFile ROW_XLSX, null, options, (err, data) -> +// err.should.be.a 'string' +// expect(data).to.be.an 'undefined' +// done() + + + it('should use defaults when caller specifies no options', done => processFile(ROW_XLSX, null, null, function(err, data) { + expect(err).to.be.an('undefined'); + JSON.stringify(data).should.equal(ROW_SHEET_1_JSON); + return done(); + })); + + + it('should process row oriented Excel files, write the result, and return the parsed object', function(done) { + const options = { + sheet:'1', + isColOriented: false, + omitEmptyFields: false + }; + + return processFile(ROW_XLSX, ROW_JSON, options, function(err, data) { + expect(err).to.be.an('undefined'); + const result = JSON.parse(fs.readFileSync(ROW_JSON, 'utf8')); + JSON.stringify(result).should.equal(ROW_SHEET_1_JSON); + JSON.stringify(data).should.equal(ROW_SHEET_1_JSON); + return done(); + }); + }); + + + it('should process sheet 2 of row oriented Excel files, write the result, and return the parsed object', function(done) { + const options = { + sheet:'2', + isColOriented: false, + omitEmptyFields: false + }; + + return processFile(ROW_XLSX, ROW_JSON, options, function(err, data) { + expect(err).to.be.an('undefined'); + const result = JSON.parse(fs.readFileSync(ROW_JSON, 'utf8')); + JSON.stringify(result).should.equal(ROW_SHEET_2_JSON); + JSON.stringify(data).should.equal(ROW_SHEET_2_JSON); + return done(); + }); + }); + + + it('should process col oriented Excel files, write the result, and return the parsed object', function(done) { + const options = { + sheet:'1', + isColOriented: true, + omitEmptyFields: false + }; + + return processFile(COL_XLSX, COL_JSON, options, function(err, data) { + expect(err).to.be.an('undefined'); + const result = JSON.parse(fs.readFileSync(COL_JSON, 'utf8')); + JSON.stringify(result).should.equal(COL_SHEET_1_JSON); + JSON.stringify(data).should.equal(COL_SHEET_1_JSON); + return done(); + }); + }); + + + it('should process sheet 2 of col oriented Excel files, write the result, and return the parsed object', function(done) { + const options = { + sheet:'2', + isColOriented: true, + omitEmptyFields: false + }; + + return processFile(COL_XLSX, COL_JSON, options, function(err, data) { + expect(err).to.be.an('undefined'); + const result = JSON.parse(fs.readFileSync(COL_JSON, 'utf8')); + JSON.stringify(result).should.equal(COL_SHEET_2_JSON); + JSON.stringify(data).should.equal(COL_SHEET_2_JSON); + return done(); + }); + }); + + + it('should create the destination directory if it does not exist', function(done) { + const options = { + sheet:'1', + isColOriented: true, + omitEmptyFields: false + }; + + return processFile(COL_XLSX, COL_JSON_NESTED, options, function(err, data) { + expect(err).to.be.an('undefined'); + const result = JSON.parse(fs.readFileSync(COL_JSON_NESTED, 'utf8')); + JSON.stringify(result).should.equal(COL_SHEET_1_JSON); + JSON.stringify(data).should.equal(COL_SHEET_1_JSON); + return done(); + }); + }); + + + it('should return a parsed object without writing a file', function(done) { + // Ensure result file does not exit + try { fs.unlinkSync(ROW_JSON); } + catch (error) {} // ignore file does not exist + + const options = { + sheet:'1', + isColOriented: false, + omitEmptyFields: false + }; + + return processFile(ROW_XLSX, undefined, options, function(err, data) { + expect(err).to.be.an('undefined'); + fs.existsSync(ROW_JSON).should.equal(false); + JSON.stringify(data).should.equal(ROW_SHEET_1_JSON); + return done(); + }); + }); + + + it('should not convert text that looks like a number to a number when directed', function(done) { + const options = { + sheet:'1', + isColOriented: false, + omitEmptyFields: false, + convertTextToNumber: false + }; + + return processFile(ROW_XLSX, undefined, options, function(err, data) { + expect(err).to.be.an('undefined'); + data[0].address.should.have.property('zip', '81615'); + data[1].address.should.have.property('zip', '81657'); + return done(); + }); + }); + + + return it('should notify on write error', done => processFile(ROW_XLSX, 'build', TEST_OPTIONS, function(err, data) { + expect(err).to.be.an('string'); + return done(); + })); +}); + + +//=============================== Coverage summary =============================== +// Statements : 100% ( 133/133 ) +// Branches : 100% ( 61/61 ) +// Functions : 100% ( 14/14 ) +// Lines : 100% ( 106/106 ) +//================================================================================ diff --git a/spec/regressionSpec.coffee b/spec/regressionSpec.coffee deleted file mode 100644 index 8fe57bf..0000000 --- a/spec/regressionSpec.coffee +++ /dev/null @@ -1,57 +0,0 @@ -processFile = require('../src/excel-as-json').processFile -fs = require 'fs' - -# TODO: How to get chai defined in a more global way -chai = require 'chai' -chai.should() -expect = chai.expect; - -# Test constants -RGR_SRC_XLSX = 'data/regression.xlsx' - -RGR23_SHEET = 1 -RGR23_IS_COL_ORIENTED = true -RGR23_OUT_JSON = 'build/rgr23.json' - -RGR28_SHEET = 2 -RGR28_IS_COL_ORIENTED = false -RGR28_OUT_JSON = 'build/rgr28.json' - -describe 'regression 23', -> - - it 'should produce empty arrays for flat arrays without values', (done) -> - options = - sheet: RGR23_SHEET - isColOriented: RGR23_IS_COL_ORIENTED - omitEmptyFields: false - - processFile RGR_SRC_XLSX, RGR23_OUT_JSON, options, (err, data) -> - expect(err).to.be.an 'undefined' - expect(data[0]).to.have.property('emptyArray').with.lengthOf(0) - done() - - it 'should remove flat arrays when omitEmptyFields and value list is blank', (done) -> - options = - sheet: RGR23_SHEET - isColOriented: RGR23_IS_COL_ORIENTED - omitEmptyFields: true - - processFile RGR_SRC_XLSX, RGR23_OUT_JSON, options, (err, data) -> - expect(err).to.be.an 'undefined' - expect(data[0].emptyArray).to.be.an 'undefined' - done() - - -describe 'regression 28', -> - - it 'should produce an empty array when no value rows are provided', (done) -> - options = - sheet: RGR28_SHEET - isColOriented: RGR28_IS_COL_ORIENTED - omitEmptyFields: false - - processFile RGR_SRC_XLSX, RGR28_OUT_JSON, options, (err, data) -> - expect(err).to.be.an 'undefined' - expect(data).to.be.an('array').with.lengthOf(0) - done() - diff --git a/spec/regressionSpec.js b/spec/regressionSpec.js new file mode 100644 index 0000000..45d3579 --- /dev/null +++ b/spec/regressionSpec.js @@ -0,0 +1,74 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { + processFile +} = require('../src/excel-as-json'); +const fs = require('fs'); + +// TODO: How to get chai defined in a more global way +const chai = require('chai'); +chai.should(); +const { + expect +} = chai; + +// Test constants +const RGR_SRC_XLSX = 'data/regression.xlsx'; + +const RGR23_SHEET = 1; +const RGR23_IS_COL_ORIENTED = true; +const RGR23_OUT_JSON = 'build/rgr23.json'; + +const RGR28_SHEET = 2; +const RGR28_IS_COL_ORIENTED = false; +const RGR28_OUT_JSON = 'build/rgr28.json'; + +describe('regression 23', function() { + + it('should produce empty arrays for flat arrays without values', function(done) { + const options = { + sheet: RGR23_SHEET, + isColOriented: RGR23_IS_COL_ORIENTED, + omitEmptyFields: false + }; + + return processFile(RGR_SRC_XLSX, RGR23_OUT_JSON, options, function(err, data) { + expect(err).to.be.an('undefined'); + expect(data[0]).to.have.property('emptyArray').with.lengthOf(0); + return done(); + }); + }); + + return it('should remove flat arrays when omitEmptyFields and value list is blank', function(done) { + const options = { + sheet: RGR23_SHEET, + isColOriented: RGR23_IS_COL_ORIENTED, + omitEmptyFields: true + }; + + return processFile(RGR_SRC_XLSX, RGR23_OUT_JSON, options, function(err, data) { + expect(err).to.be.an('undefined'); + expect(data[0].emptyArray).to.be.an('undefined'); + return done(); + }); + }); +}); + + +describe('regression 28', () => it('should produce an empty array when no value rows are provided', function(done) { + const options = { + sheet: RGR28_SHEET, + isColOriented: RGR28_IS_COL_ORIENTED, + omitEmptyFields: false + }; + + return processFile(RGR_SRC_XLSX, RGR28_OUT_JSON, options, function(err, data) { + expect(err).to.be.an('undefined'); + expect(data).to.be.an('array').with.lengthOf(0); + return done(); + }); +})); + diff --git a/spec/transposeSpec.coffee b/spec/transposeSpec.coffee deleted file mode 100644 index 6c800e3..0000000 --- a/spec/transposeSpec.coffee +++ /dev/null @@ -1,61 +0,0 @@ -transpose = require('../src/excel-as-json').transpose - -# TODO: How to get chai defined in a more global way -chai = require 'chai' -chai.should() -expect = chai.expect; - - -_removeDuplicates = (array) -> - set = {} - set[array[key]] = array[key] for key in [0..array.length-1] - return (key for key of set) - - -describe 'transpose', -> - - square = [ - ['one', 'two', 'three'], - ['one', 'two', 'three'], - ['one', 'two', 'three'] - ] - - rectangleWide = [ - ['one', 'two', 'three'], - ['one', 'two', 'three'] - ] - - rectangleTall = [ - ['one', 'two'], - ['one', 'two'], - ['one', 'two'] - ] - - - it 'should transpose square 2D arrays', -> - result = transpose square - result.length.should.equal 3 - - for row in result - row.length.should.equal 3 - _removeDuplicates(row).length.should.equal 1 - - - it 'should transpose wide rectangular 2D arrays', -> - result = transpose rectangleWide - result.length.should.equal 3 - - for row in result - row.length.should.equal 2 - _removeDuplicates(row).length.should.equal 1 - - - it 'should transpose tall rectangular 2D arrays', -> - result = transpose rectangleTall - result.length.should.equal 2 - - for row in result - row.length.should.equal 3 - _removeDuplicates(row).length.should.equal 1 - - diff --git a/spec/transposeSpec.js b/spec/transposeSpec.js new file mode 100644 index 0000000..8026afe --- /dev/null +++ b/spec/transposeSpec.js @@ -0,0 +1,101 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS202: Simplify dynamic range loops + * DS205: Consider reworking code to avoid use of IIFEs + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { + transpose +} = require('../src/excel-as-json'); + +// TODO: How to get chai defined in a more global way +const chai = require('chai'); +chai.should(); +const { + expect +} = chai; + + +const _removeDuplicates = function(array) { + let asc, end; + let key; + const set = {}; + for (key = 0, end = array.length-1, asc = 0 <= end; asc ? key <= end : key >= end; asc ? key++ : key--) { set[array[key]] = array[key]; } + return (() => { + const result = []; + for (key in set) { + result.push(key); + } + return result; + })(); +}; + + +describe('transpose', function() { + + const square = [ + ['one', 'two', 'three'], + ['one', 'two', 'three'], + ['one', 'two', 'three'] + ]; + + const rectangleWide = [ + ['one', 'two', 'three'], + ['one', 'two', 'three'] + ]; + + const rectangleTall = [ + ['one', 'two'], + ['one', 'two'], + ['one', 'two'] + ]; + + + it('should transpose square 2D arrays', function() { + const result = transpose(square); + result.length.should.equal(3); + + return (() => { + const result1 = []; + for (let row of Array.from(result)) { + row.length.should.equal(3); + result1.push(_removeDuplicates(row).length.should.equal(1)); + } + return result1; + })(); + }); + + + it('should transpose wide rectangular 2D arrays', function() { + const result = transpose(rectangleWide); + result.length.should.equal(3); + + return (() => { + const result1 = []; + for (let row of Array.from(result)) { + row.length.should.equal(2); + result1.push(_removeDuplicates(row).length.should.equal(1)); + } + return result1; + })(); + }); + + + return it('should transpose tall rectangular 2D arrays', function() { + const result = transpose(rectangleTall); + result.length.should.equal(2); + + return (() => { + const result1 = []; + for (let row of Array.from(result)) { + row.length.should.equal(3); + result1.push(_removeDuplicates(row).length.should.equal(1)); + } + return result1; + })(); + }); +}); + + diff --git a/spec/validateOptionsSpec.coffee b/spec/validateOptionsSpec.coffee deleted file mode 100644 index 340a160..0000000 --- a/spec/validateOptionsSpec.coffee +++ /dev/null @@ -1,99 +0,0 @@ -_validateOptions = require('../src/excel-as-json')._validateOptions - -# TODO: How to get chai defined in a more global way -chai = require 'chai' -chai.should() -expect = chai.expect; - -TEST_OPTIONS = - sheet: '1' - isColOriented: false - omitEmptyFields: false - -describe 'validate options', -> - - it 'should provide default options when none are specified', (done) -> - options = _validateOptions(null) - options.sheet.should.equal '1' - options.isColOriented.should.equal false - options.omitEmptyFields.should.equal false - - options = _validateOptions(undefined) - options.sheet.should.equal '1' - options.isColOriented.should.equal false - options.omitEmptyFields.should.equal false - done() - - - it 'should fill in missing sheet id', (done) -> - o = - isColOriented: false - omitEmptyFields: false - - options = _validateOptions(o) - options.sheet.should.equal '1' - options.isColOriented.should.equal false - options.omitEmptyFields.should.equal false - done() - - - it 'should fill in missing isColOriented', (done) -> - o = - sheet: '1' - omitEmptyFields: false - - options = _validateOptions(o) - options.sheet.should.equal '1' - options.isColOriented.should.equal false - options.omitEmptyFields.should.equal false - done() - - - it 'should fill in missing omitEmptyFields', (done) -> - o = - sheet: '1' - isColOriented: false - - options = _validateOptions(o) - options.sheet.should.equal '1' - options.isColOriented.should.equal false - options.omitEmptyFields.should.equal false - done() - - - it 'should convert a numeric sheet id to text', (done) -> - o = - sheet: 3 - isColOriented: false - omitEmptyFields: true - - options = _validateOptions(o) - options.sheet.should.equal '3' - options.isColOriented.should.equal false - options.omitEmptyFields.should.equal true - done() - - - it 'should detect invalid sheet ids and replace with the default', (done) -> - o = - sheet: 'one' - isColOriented: false - omitEmptyFields: true - - options = _validateOptions(o) - options.sheet.should.equal '1' - options.isColOriented.should.equal false - options.omitEmptyFields.should.equal true - - o.sheet = 0 - options = _validateOptions(o) - options.sheet.should.equal '1' - - o.sheet = true - options = _validateOptions(o) - options.sheet.should.equal '1' - - o.sheet = isNaN - options = _validateOptions(o) - options.sheet.should.equal '1' - done() diff --git a/spec/validateOptionsSpec.js b/spec/validateOptionsSpec.js new file mode 100644 index 0000000..1b48539 --- /dev/null +++ b/spec/validateOptionsSpec.js @@ -0,0 +1,121 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { + _validateOptions +} = require('../src/excel-as-json'); + +// TODO: How to get chai defined in a more global way +const chai = require('chai'); +chai.should(); +const { + expect +} = chai; + +const TEST_OPTIONS = { + sheet: '1', + isColOriented: false, + omitEmptyFields: false +}; + +describe('validate options', function() { + + it('should provide default options when none are specified', function(done) { + let options = _validateOptions(null); + options.sheet.should.equal(1); + options.isColOriented.should.equal(false); + options.omitEmptyFields.should.equal(false); + + options = _validateOptions(undefined); + options.sheet.should.equal(1); + options.isColOriented.should.equal(false); + options.omitEmptyFields.should.equal(false); + return done(); + }); + + + it('should fill in missing sheet id', function(done) { + const o = { + isColOriented: false, + omitEmptyFields: false + }; + + const options = _validateOptions(o); + options.sheet.should.equal(1); + options.isColOriented.should.equal(false); + options.omitEmptyFields.should.equal(false); + return done(); + }); + + + it('should fill in missing isColOriented', function(done) { + const o = { + sheet: '1', + omitEmptyFields: false + }; + + const options = _validateOptions(o); + options.sheet.should.equal(1); + options.isColOriented.should.equal(false); + options.omitEmptyFields.should.equal(false); + return done(); + }); + + + it('should fill in missing omitEmptyFields', function(done) { + const o = { + sheet: '1', + isColOriented: false + }; + + const options = _validateOptions(o); + options.sheet.should.equal(1); + options.isColOriented.should.equal(false); + options.omitEmptyFields.should.equal(false); + return done(); + }); + + + it('should convert a numeric sheet id to text', function(done) { + const o = { + sheet: 3, + isColOriented: false, + omitEmptyFields: true + }; + + const options = _validateOptions(o); + options.sheet.should.equal(3); + options.isColOriented.should.equal(false); + options.omitEmptyFields.should.equal(true); + return done(); + }); + + + return it('should detect invalid sheet ids and replace with the default', function(done) { + const o = { + sheet: 'one', + isColOriented: false, + omitEmptyFields: true + }; + + let options = _validateOptions(o); + options.sheet.should.equal(1); + options.isColOriented.should.equal(false); + options.omitEmptyFields.should.equal(true); + + o.sheet = -1; + options = _validateOptions(o); + options.sheet.should.equal(1); + + o.sheet = true; + options = _validateOptions(o); + options.sheet.should.equal(1); + + o.sheet = isNaN; + options = _validateOptions(o); + options.sheet.should.equal(1); + return done(); + }); +}); diff --git a/src/excel-as-json.coffee b/src/excel-as-json.coffee deleted file mode 100644 index 7dadf76..0000000 --- a/src/excel-as-json.coffee +++ /dev/null @@ -1,221 +0,0 @@ -# Create a list of json objects; 1 object per excel sheet row -# -# Assume: Excel spreadsheet is a rectangle of data, where the first row is -# object keys and remaining rows are object values and the desired json -# is a list of objects. Alternatively, data may be column oriented with -# col 0 containing key names. -# -# Dotted notation: Key row (0) containing firstName, lastName, address.street, -# address.city, address.state, address.zip would produce, per row, a doc with -# first and last names and an embedded doc named address, with the address. -# -# Arrays: may be indexed (phones[0].number) or flat (aliases[]). Indexed -# arrays imply a list of objects. Flat arrays imply a semicolon delimited list. -# -# USE: -# From a shell -# coffee src/excel-as-json.coffee -# -fs = require 'fs' -path = require 'path' -excel = require 'excel' - -BOOLTEXT = ['true', 'false'] -BOOLVALS = {'true': true, 'false': false} - -isArray = (obj) -> - Object.prototype.toString.call(obj) is '[object Array]' - - -# Extract key name and array index from names[1] or names[] -# return [keyIsList, keyName, index] -# for names[1] return [true, keyName, index] -# for names[] return [true, keyName, undefined] -# for names return [false, keyName, undefined] -parseKeyName = (key) -> - index = key.match(/\[(\d+)\]$/) - switch - when index then [true, key.split('[')[0], Number(index[1])] - when key[-2..] is '[]' then [true, key[...-2], undefined] - else [false, key, undefined] - - -# Convert a list of values to a list of more native forms -convertValueList = (list, options) -> - (convertValue(item, options) for item in list) - - -# Convert values to native types -# Note: all values from the excel module are text -convertValue = (value, options) -> - # isFinite returns true for empty or blank strings, check for those first - if value.length == 0 || !/\S/.test(value) - value - else if isFinite(value) - if options.convertTextToNumber - Number(value) - else - value - else - testVal = value.toLowerCase() - if testVal in BOOLTEXT - BOOLVALS[testVal] - else - value - - -# Assign a value to a dotted property key - set values on sub-objects -assign = (obj, key, value, options) -> - # On first call, a key is a string. Recursed calls, a key is an array - key = key.split '.' unless typeof key is 'object' - # Array element accessors look like phones[0].type or aliases[] - [keyIsList, keyName, index] = parseKeyName key.shift() - - if key.length - if keyIsList - # if our object is already an array, ensure an object exists for this index - if isArray obj[keyName] - unless obj[keyName][index] - obj[keyName].push({}) for i in [obj[keyName].length..index] - # else set this value to an array large enough to contain this index - else - obj[keyName] = ({} for i in [0..index]) - assign obj[keyName][index], key, value, options - else - obj[keyName] ?= {} - assign obj[keyName], key, value, options - else - if keyIsList and index? - console.error "WARNING: Unexpected key path terminal containing an indexed list for <#{keyName}>" - console.error "WARNING: Indexed arrays indicate a list of objects and should not be the last element in a key path" - console.error "WARNING: The last element of a key path should be a key name or flat array. E.g. alias, aliases[]" - if (keyIsList and not index?) - if value != '' - obj[keyName] = convertValueList(value.split(';'), options) - else if !options.omitEmptyFields - obj[keyName] = [] - else - if !(options.omitEmptyFields && value == '') - obj[keyName] = convertValue(value, options) - - -# Transpose a 2D array -transpose = (matrix) -> - (t[i] for t in matrix) for i in [0...matrix[0].length] - - -# Convert 2D array to nested objects. If row oriented data, row 0 is dotted key names. -# Column oriented data is transposed -convert = (data, options) -> - data = transpose data if options.isColOriented - - keys = data[0] - rows = data[1..] - - result = [] - for row in rows - item = {} - assign(item, keys[index], value, options) for value, index in row - result.push item - return result - - -# Write JSON encoded data to file -# call back is callback(err) -write = (data, dst, callback) -> - # Create the target directory if it does not exist - dir = path.dirname(dst) - fs.mkdirSync dir if !fs.existsSync(dir) - fs.writeFile dst, JSON.stringify(data, null, 2), (err) -> - if err then callback "Error writing file #{dst}: #{err}" - else callback undefined - - -# src: xlsx file that we will read sheet 0 of -# dst: file path to write json to. If null, simply return the result -# options: see below -# callback(err, data): callback for completion notification -# -# options: -# sheet: string; 1: numeric, 1-based index of target sheet -# isColOriented: boolean: false; are objects stored in excel columns; key names in col A -# omitEmptyFields: boolean: false: do not include keys with empty values in json output. empty values are stored as '' -# TODO: this is probably better named omitKeysWithEmptyValues -# convertTextToNumber boolean: true; if text looks like a number, convert it to a number -# -# convertExcel(src, dst)
-# will write a row oriented xlsx sheet 1 to `dst` as JSON with no notification -# convertExcel(src, dst, {isColOriented: true})
-# will write a col oriented xlsx sheet 1 to file with no notification -# convertExcel(src, dst, {isColOriented: true}, callback)
-# will write a col oriented xlsx to file and notify with errors and parsed data -# convertExcel(src, null, null, callback)
-# will parse a row oriented xslx using default options and return errors and the parsed data in the callback -# -_DEFAULT_OPTIONS = - sheet: '1' - isColOriented: false - omitEmptyFields: false - convertTextToNumber: true - -# Ensure options sane, provide defaults as appropriate -_validateOptions = (options) -> - if !options - options = _DEFAULT_OPTIONS - else - if !options.hasOwnProperty('sheet') - options.sheet = '1' - else - # ensure sheet is a text representation of a number - if !isNaN(parseFloat(options.sheet)) && isFinite(options.sheet) - if options.sheet < 1 - options.sheet = '1' - else - # could be 3 or '3'; force to be '3' - options.sheet = '' + options.sheet - else - # something bizarre like true, [Function: isNaN], etc - options.sheet = '1' - if !options.hasOwnProperty('isColOriented') - options.isColOriented = false - if !options.hasOwnProperty('omitEmptyFields') - options.omitEmptyFields = false - if !options.hasOwnProperty('convertTextToNumber') - options.convertTextToNumber = true - options - - -processFile = (src, dst, options=_DEFAULT_OPTIONS, callback=undefined) -> - options = _validateOptions(options) - - # provide a callback if the user did not - if !callback then callback = (err, data) -> - - # NOTE: 'excel' does not properly bubble file not found and prints - # an ugly error we can't trap, so look for this common error first - if not fs.existsSync src - callback "Cannot find src file #{src}" - else - excel src, options.sheet, (err, data) -> - if err - callback "Error reading #{src}: #{err}" - else - result = convert data, options - if dst - write result, dst, (err) -> - if err then callback err - else callback undefined, result - else - callback undefined, result - -# This is the single expected module entry point -exports.processFile = processFile - -# Unsupported use -# Exposing remaining functionality for unexpected use cases, testing, etc. -exports.assign = assign -exports.convert = convert -exports.convertValue = convertValue -exports.parseKeyName = parseKeyName -exports._validateOptions = _validateOptions -exports.transpose = transpose diff --git a/src/excel-as-json.js b/src/excel-as-json.js new file mode 100644 index 0000000..c4769c3 --- /dev/null +++ b/src/excel-as-json.js @@ -0,0 +1,342 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS202: Simplify dynamic range loops + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +// Create a list of json objects; 1 object per excel sheet row +// +// Assume: Excel spreadsheet is a rectangle of data, where the first row is +// object keys and remaining rows are object values and the desired json +// is a list of objects. Alternatively, data may be column oriented with +// col 0 containing key names. +// +// Dotted notation: Key row (0) containing firstName, lastName, address.street, +// address.city, address.state, address.zip would produce, per row, a doc with +// first and last names and an embedded doc named address, with the address. +// +// Arrays: may be indexed (phones[0].number) or flat (aliases[]). Indexed +// arrays imply a list of objects. Flat arrays imply a semicolon delimited list. +// +// USE: +// From a shell +// coffee src/excel-as-json.coffee +// +const fs = require('fs'); +const path = require('path'); +const ExcelJS = require('exceljs'); + +const BOOLTEXT = ['true', 'false']; +const BOOLVALS = {'true': true, 'false': false}; + +const isArray = obj => Object.prototype.toString.call(obj) === '[object Array]'; + +// Extract key name and array index from names[1] or names[] +// return [keyIsList, keyName, index] +// for names[1] return [true, keyName, index] +// for names[] return [true, keyName, undefined] +// for names return [false, keyName, undefined] +const parseKeyName = function (key) { + const index = key.match(/\[(\d+)\]$/); + switch (false) { + case !index: + return [true, key.split('[')[0], Number(index[1])]; + case key.slice(-2) !== '[]': + return [true, key.slice(0, -2), undefined]; + default: + return [false, key, undefined]; + } +}; + +// Convert a list of values to a list of more native forms +const convertValueList = (list, options) => Array.from(list).map((item) => convertValue(item, options)); + +// Convert values to native types +// Note: all values from the excel module are text +var convertValue = function (value, options) { + // isFinite returns true for empty or blank strings, check for those first + if ((value.length === 0) || !/\S/.test(value)) { + return value; + } else if (isFinite(value)) { + if (options.convertTextToNumber) { + return Number(value); + } else { + return value; + } + } else { + const testVal = value.toLowerCase(); + if (Array.from(BOOLTEXT).includes(testVal)) { + return BOOLVALS[testVal]; + } else { + return value; + } + } +}; + +// Assign a value to a dotted property key - set values on sub-objects +var assign = function (obj, key, value, options) { + // On first call, a key is a string. Recursed calls, a key is an array + let i; + if (typeof key !== 'object') { + key = key.split('.'); + } + // Array element accessors look like phones[0].type or aliases[] + const [keyIsList, keyName, index] = Array.from(parseKeyName(key.shift())); + + if (key.length) { + if (keyIsList) { + // if our object is already an array, ensure an object exists for this index + if (isArray(obj[keyName])) { + if (!obj[keyName][index]) { + let asc, end; + for (i = obj[keyName].length, end = index, asc = obj[keyName].length <= end; asc ? i <= end : i >= end; asc ? i++ : i--) { + obj[keyName].push({}); + } + } + // else set this value to an array large enough to contain this index + } else { + obj[keyName] = ((() => { + let asc1, end1; + const result = []; + for (i = 0, end1 = index, asc1 = 0 <= end1; asc1 ? i <= end1 : i >= end1; asc1 ? i++ : i--) { + result.push({}); + } + return result; + })()); + } + return assign(obj[keyName][index], key, value, options); + } else { + if (obj[keyName] == null) { + obj[keyName] = {}; + } + return assign(obj[keyName], key, value, options); + } + } else { + if (keyIsList && (index != null)) { + console.error(`WARNING: Unexpected key path terminal containing an indexed list for <${keyName}>`); + console.error("WARNING: Indexed arrays indicate a list of objects and should not be the last element in a key path"); + console.error("WARNING: The last element of a key path should be a key name or flat array. E.g. alias, aliases[]"); + } + if (keyIsList && (index == null)) { + if (value != null && value !== '') { + return obj[keyName] = convertValueList(value.split(';'), options); + } else if (!options.omitEmptyFields) { + return obj[keyName] = []; + } + } else { + if (!(options.omitEmptyFields && (value === ''))) { + return obj[keyName] = convertValue(value, options); + } + } + } +}; + +// Transpose a 2D array +const transpose = matrix => __range__(0, matrix[0].length, false).map((i) => (Array.from(matrix).map((t) => t[i]))); + +// Convert 2D array to nested objects. If row oriented data, row 0 is dotted key names. +// Column oriented data is transposed +const convert = function (data, options) { + + if (options.isColOriented) { + data = transpose(data); + } + + const keys = data[0]; + const rows = data.slice(1); + + const result = []; + for (let row of Array.from(rows)) { + const item = {}; + for (let index = 0; index < row.length; index++) { + const value = row[index]; + assign(item, keys[index], value, options); + } + result.push(item); + } + return result; +}; + +const processRow = function (row, options) { + let values = []; + row.eachCell({ includeEmpty: true }, (cell) => { + let res + let value = cell.value; + if (value != null && value.formula) { + res = value.result; + } else { + res = value; + } + if (value === null) { + res = ""; + } + res = res.toString(); + values.push(res); + }); + return values; +} + +const convertWorksheet = function (ws, options) { + let data = []; + ws.eachRow(row => data.push(processRow(row, options))); + return convert(data, options); +}; + +// Write JSON encoded data to file +// call back is callback(err) +const write = function (data, dst, callback) { + // Create the target directory if it does not exist + const dir = path.dirname(dst); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + return fs.writeFile(dst, JSON.stringify(data, null, 2), function (err) { + if (err) { + return callback(`Error writing file ${dst}: ${err}`); + } else { + return callback(undefined); + } + }); +}; + +// src: xlsx file that we will read sheet 0 of +// dst: file path to write json to. If null, simply return the result +// options: see below +// callback(err, data): callback for completion notification +// +// options: +// sheet: string; 1: numeric, 1-based index of target sheet +// isColOriented: boolean: false; are objects stored in excel columns; key names in col A +// omitEmptyFields: boolean: false: do not include keys with empty values in json output. empty values are stored as '' +// TODO: this is probably better named omitKeysWithEmptyValues +// convertTextToNumber boolean: true; if text looks like a number, convert it to a number +// +// convertExcel(src, dst)
+// will write a row oriented xlsx sheet 1 to `dst` as JSON with no notification +// convertExcel(src, dst, {isColOriented: true})
+// will write a col oriented xlsx sheet 1 to file with no notification +// convertExcel(src, dst, {isColOriented: true}, callback)
+// will write a col oriented xlsx to file and notify with errors and parsed data +// convertExcel(src, null, null, callback)
+// will parse a row oriented xslx using default options and return errors and the parsed data in the callback +// +const _DEFAULT_OPTIONS = { + sheet: 1, + isColOriented: false, + omitEmptyFields: false, + convertTextToNumber: true +}; + +// Ensure options sane, provide defaults as appropriate +const _validateOptions = function (options) { + if (!options) { + options = _DEFAULT_OPTIONS; + } else { + if (!options.hasOwnProperty('sheet')) { + options.sheet = 1; + } else { + // ensure sheet is a text representation of a number + if (!isNaN(parseFloat(options.sheet)) && isFinite(options.sheet)) { + if (options.sheet < 1) { + options.sheet = 1; + } else { + // could be 3 or '3'; force to be '3' + options.sheet = Number(options.sheet); + } + } else { + // something bizarre like true, [Function: isNaN], etc + options.sheet = 1; + } + } + if (!options.hasOwnProperty('isColOriented')) { + options.isColOriented = false; + } + if (!options.hasOwnProperty('omitEmptyFields')) { + options.omitEmptyFields = false; + } + if (!options.hasOwnProperty('convertTextToNumber')) { + options.convertTextToNumber = true; + } + } + return options; +}; + +const processFile = function (src, dst, options, callback) { + if (options == null) { + options = _DEFAULT_OPTIONS; + } + if (callback == null) { + callback = undefined; + } + options = _validateOptions(options); + + // provide a callback if the user did not + if (!callback) { + callback = function (err, data) { + }; + } + + // NOTE: 'excel' does not properly bubble file not found and prints + // an ugly error we can't trap, so look for this common error first + if (!fs.existsSync(src)) { + return callback(`Cannot find src file ${src}`); + } else { + const wb = new ExcelJS.Workbook(); + let readPromise; + if (src.endsWith(".xlsx")) { + readPromise = wb.xlsx.readFile(src); + } else if (src.endsWith(".csv")) { + readPromise = wb.csv.readFile(src); + } + readPromise.catch((err) => callback(`Error reading ${src}: ${err}`)) + .then(() => { + let sheet = Number(options.sheet)-1; + let ws; + if (src.endsWith(".xlsx")) { + ws = wb.worksheets.filter(s => s.orderNo === sheet)[0]; + }else{ + ws= wb.getWorksheet(); + } + if (!ws) { + callback(`No sheet found for ${sheet} possible sheets ${wb.worksheets.map((ws) => `${ws.name}:${ws.orderNo+1}`).join(",")}`) + } + const result = convertWorksheet(ws, options); + if (dst) { + return write(result, dst, function (err) { + if (err) { + return callback(err); + } else { + return callback(undefined, result); + } + }); + } else { + return callback(undefined, result); + } + }).catch((err) => callback(`Error processing ${src}: ${err}`)) + } +}; + +// This is the single expected module entry point +exports.processFile = processFile; + +// Unsupported use +// Exposing remaining functionality for unexpected use cases, testing, etc. +exports.assign = assign; +exports.convert = convert; +exports.convertValue = convertValue; +exports.parseKeyName = parseKeyName; +exports._validateOptions = _validateOptions; +exports.transpose = transpose; + +function __range__(left, right, inclusive) { + let range = []; + let ascending = left < right; + let end = !inclusive ? right : ascending ? right + 1 : right - 1; + for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) { + range.push(i); + } + return range; +} \ No newline at end of file diff --git a/tools/build.sh b/tools/build.sh index cc2c77a..9c36f71 100755 --- a/tools/build.sh +++ b/tools/build.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash # Compile coffee src/test files -coffee -c -o lib/ src/ -coffee -c -o test/ spec/ +mkdir lib +mkdir test +cp src/* lib/ +cp spec/* test/ # Replace the CoffeeScript test file reference to CoffeeScript source with js equivalents sed -i '' -e 's/\.\.\/src\/excel-as-json/\.\.\/lib\/excel-as-json/' test/* diff --git a/tools/coffee-coverage-loader.js b/tools/coffee-coverage-loader.js deleted file mode 100644 index 45cc6c2..0000000 --- a/tools/coffee-coverage-loader.js +++ /dev/null @@ -1,15 +0,0 @@ -// A custom coffee-coverage loader to exclude non-source files -// https://github.com/benbria/coffee-coverage/blob/master/docs/HOWTO-istanbul.md -// https://github.com/benbria/coffee-coverage/blob/master/docs/HOWTO-istanbul.md#writing-a-custom-loader -var coffeeCoverage = require('coffee-coverage'); -var coverageVar = coffeeCoverage.findIstanbulVariable(); -var writeOnExit = coverageVar == null ? true : null; - -coffeeCoverage.register({ - instrumentor: 'istanbul', - basePath: process.cwd(), - exclude: ['/spec', '/node_modules', '/.git'], - coverageVar: coverageVar, - writeOnExit: writeOnExit ? ((_ref = process.env.COFFEECOV_OUT) != null ? _ref : 'coverage/coverage-coffee.json') : null, - initAll: false // ignore files in project root (Gruntfile.coffee) -}); \ No newline at end of file diff --git a/tools/test.sh b/tools/test.sh index c435038..013e5a5 100755 --- a/tools/test.sh +++ b/tools/test.sh @@ -4,9 +4,4 @@ rm -rf build # Use our custom coffee-coverage loader to generate instrumented coffee files -mocha -R spec --compilers coffee:coffeescript/register \ - --require ./tools/coffee-coverage-loader.js \ - spec/all-specs.coffee - -# Generate reports for dev and upload to Coveralls, CodeCov -istanbul report text-summary lcov \ No newline at end of file +nyc mocha -R spec spec/all-specs.js