├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── data
├── col-oriented.xlsx
├── regression.xlsx
├── row-oriented.csv
└── row-oriented.xlsx
├── package.json
├── spec
├── all-specs.coffee
├── assignSpec.coffee
├── convertSpec.coffee
├── convertValueSpec.coffee
├── parseKeyNameSpec.coffee
├── processFileSpec.coffee
├── regressionSpec.coffee
├── transposeSpec.coffee
└── validateOptionsSpec.coffee
├── src
└── excel-as-json.coffee
└── tools
├── build.sh
├── clean.sh
├── coffee-coverage-loader.js
├── dist.sh
└── test.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | .idea
3 | node_modules
4 | build
5 | lib
6 | test
7 | coverage
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .gitignore
3 | src
4 | build
5 | coverage
6 | data
7 | spec
8 | Gruntfile.coffee
9 | tools/build.sh
10 | tools/coffee-coverage-loader.js
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '4.0.0'
4 | before_install:
5 | - 'npm install -g coffeescript'
6 | before_script:
7 | - 'npm run-script dist'
8 | after_success:
9 | - 'npm run-script codecov'
10 | - 'npm run-script coveralls'
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 stevetarver
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/stevetarver/excel-as-json/releases)
2 | [](#license)
3 | [](https://travis-ci.org/stevetarver/excel-as-json)
4 | [](https://coveralls.io/r/stevetarver/excel-as-json)
5 | [](http://codecov.io/github/stevetarver/excel-as-json?branch=master)
6 |
7 | [](https://www.npmjs.com/package/excel-as-json)
8 | [](https://david-dm.org/stevetarver/excel-as-json.svg)
9 | [](https://david-dm.org/stevetarver/excel-as-json#info=devDependencies)
10 |
11 |
12 | # Convert Excel Files to JSON
13 |
14 | ## What
15 |
16 | Parse Excel xlsx files into a list of javascript objects and optionally write that list as a JSON encoded file.
17 |
18 | You may organize Excel data by columns or rows where the first column or row contains object key names and the remaining columns/rows contain object values.
19 |
20 | Expected use is offline translation of Excel data to JSON files, although
21 | all methods are exported for other uses.
22 |
23 | ## Install
24 |
25 | ```$ npm install excel-as-json --save-dev```
26 |
27 | ## Use
28 |
29 | ```js
30 | convertExcel = require('excel-as-json').processFile;
31 | convertExcel(src, dst, options, callback);
32 | ```
33 |
34 | * src: path to source Excel file (xlsx only)
35 | * dst: path to destination JSON file. If null, simply return the parsed object tree
36 | * options: an object containing
37 | * sheet: 1 based sheet index as text - default '1'
38 | * isColOriented: are object values in columns with keys in column A - default false
39 | * omitEmptyFields: omit empty Excel fields from JSON output - default false
40 | * convertTextToNumber: if text looks like a number, convert it to a number - default true
41 | * callback(err, data): callback for completion notification
42 |
43 | **NOTE** If options are not specified, defaults are used.
44 |
45 | With these arguments, you can:
46 |
47 | * convertExcel(src, dst)
48 | will write a row oriented xlsx sheet 1 to `dst` as JSON with no notification
49 | * convertExcel(src, dst, {isColOriented: true})
50 | will write a col oriented xlsx sheet 1 to file with no notification
51 | * convertExcel(src, dst, {isColOriented: true}, callback)
52 | will write a col oriented xlsx to file and notify with errors and parsed data
53 | * convertExcel(src, null, null, callback)
54 | will parse a row oriented xslx using default options and return errors and the parsed data in the callback
55 |
56 | Convert a row/col oriented Excel file to JSON as a development task and
57 | log errors:
58 |
59 | ```CoffeeScript
60 | convertExcel = require('excel-as-json').processFile
61 |
62 | options =
63 | sheet:'1'
64 | isColOriented: false
65 | omitEmtpyFields: false
66 |
67 | convertExcel 'row.xlsx', 'row.json', options, (err, data) ->
68 | if err then console.log "JSON conversion failure: #{err}"
69 |
70 | options =
71 | sheet:'1'
72 | isColOriented: true
73 | omitEmtpyFields: false
74 |
75 | convertExcel 'col.xlsx', 'col.json', options, (err, data) ->
76 | if err then console.log "JSON conversion failure: #{err}"
77 | ```
78 |
79 | Convert Excel file to an object tree and use that tree. Note that
80 | properly formatted data will convert to the same object tree whether
81 | row or column oriented.
82 |
83 | ```CoffeeScript
84 | convertExcel = require('excel-as-json').processFile
85 |
86 | convertExcel 'row.xlsx', undefined, undefined, (err, data) ->
87 | if err throw err
88 | doSomethingInteresting data
89 |
90 | convertExcel 'col.xlsx', undefined, {isColOriented: true}, (err, data) ->
91 | if err throw err
92 | doSomethingInteresting data
93 | ```
94 |
95 | ### Why?
96 |
97 | * Your application serves static data obtained as Excel reports from
98 | another application
99 | * Whoever manages your static data finds Excel more pleasant than editing JSON
100 | * Your data is the result of calculations or formatting that is
101 | more simply done in Excel
102 |
103 | ### What's the challenge?
104 |
105 | Excel stores tabular data. Converting that to JSON using only
106 | a couple of assumptions is straight-forward. Most interesting
107 | JSON contains nested lists and objects. How do you map a
108 | flat data square that is easy for anyone to edit into these
109 | nested lists and objects?
110 |
111 | ### Solving the challenge
112 |
113 | - Use a key row to name JSON keys
114 | - Allow data to be stored in row or column orientation.
115 | - Use javascript notation for keys and arrays
116 | - Allow dotted key path notation
117 | - Allow arrays of objects and literals
118 |
119 | ### Excel Data
120 |
121 | What is the easiest way to organize and edit your Excel data? Lists of
122 | simple objects seem a natural fit for a row oriented sheets. Single objects
123 | with more complex structure seem more naturally presented as column
124 | oriented sheets. Doesn't really matter which orientation you use, the
125 | module allows you to speciy a row or column orientation; basically, where
126 | your keys are located: row 0 or column 0.
127 |
128 | Keys and values:
129 |
130 | * Row or column 0 contains JSON key paths
131 | * Remaining rows/columns contain values for those keys
132 | * Multiple value rows/columns represent multiple objects stored as a list
133 | * Within an object, lists of objects have keys like phones[1].type
134 | * Within an object, flat lists have keys like aliases[]
135 |
136 | ### Examples
137 |
138 | A simple, row oriented key
139 |
140 | |firstName
141 | |---------
142 | | Jihad
143 |
144 | produces
145 |
146 | ```
147 | [{
148 | "firstName": "Jihad"
149 | }]
150 | ```
151 |
152 | A dotted key name looks like
153 |
154 | | address.street
155 | |---
156 | | 12 Beaver Court
157 |
158 | and produces
159 |
160 | ```
161 | [{
162 | "address": {
163 | "street": "12 Beaver Court"
164 | }
165 | }]
166 | ```
167 |
168 | An indexed array key name looks like
169 |
170 | |phones[0].number
171 | |---
172 | |123.456.7890
173 |
174 | and produces
175 |
176 | ```
177 | [{
178 | "phones": [{
179 | "number": "123.456.7890"
180 | }]
181 | }]
182 | ```
183 |
184 | An embedded array key name looks like this and has ';' delimited values
185 |
186 | | aliases[]
187 | |---
188 | | stormagedden;bob
189 |
190 | and produces
191 |
192 | ```
193 | [{
194 | "aliases": [
195 | "stormagedden",
196 | "bob"
197 | ]
198 | }]
199 | ```
200 |
201 | A more complete row oriented example
202 |
203 | |firstName| lastName | address.street | address.city|address.state|address.zip |
204 | |---------|----------|-----------------|-------------|-------------|------------|
205 | | Jihad | Saladin | 12 Beaver Court | Snowmass | CO | 81615 |
206 | | Marcus | Rivapoli | 16 Vail Rd | Vail | CO | 81657 |
207 |
208 | would produce
209 |
210 | ```JSON
211 | [{
212 | "firstName": "Jihad",
213 | "lastName": "Saladin",
214 | "address": {
215 | "street": "12 Beaver Court",
216 | "city": "Snowmass",
217 | "state": "CO",
218 | "zip": "81615"
219 | }
220 | },
221 | {
222 | "firstName": "Marcus",
223 | "lastName": "Rivapoli",
224 | "address": {
225 | "street": "16 Vail Rd",
226 | "city": "Vail",
227 | "state": "CO",
228 | "zip": "81657"
229 | }
230 | }]
231 | ```
232 |
233 | You can do something similar in column oriented sheets. Note that indexed
234 | and flat arrays are added.
235 |
236 | |firstName | Jihad | Marcus |
237 | | :--- | :--- | :--- |
238 | |**lastName** | Saladin | Rivapoli |
239 | |**address.street** |12 Beaver Court | 16 Vail Rd
240 | |**address.city** | Snowmass | Vail
241 | |**address.state** | CO | CO
242 | |**address.zip**| 81615 | 81657
243 | |**phones[0].type**| home | home
244 | |**phones[0].number** |123.456.7890 | 123.456.7891
245 | |**phones[1].type**| work | work
246 | |**phones[1].number** | 098.765.4321 | 098.765.4322
247 | |**aliases[]** | stormagedden;bob | mac;markie
248 |
249 | would produce
250 |
251 | ```
252 | [
253 | {
254 | "firstName": "Jihad",
255 | "lastName": "Saladin",
256 | "address": {
257 | "street": "12 Beaver Court",
258 | "city": "Snowmass",
259 | "state": "CO",
260 | "zip": "81615"
261 | },
262 | "phones": [
263 | {
264 | "type": "home",
265 | "number": "123.456.7890"
266 | },
267 | {
268 | "type": "work",
269 | "number": "098.765.4321"
270 | }
271 | ],
272 | "aliases": [
273 | "stormagedden",
274 | "bob"
275 | ]
276 | },
277 | {
278 | "firstName": "Marcus",
279 | "lastName": "Rivapoli",
280 | "address": {
281 | "street": "16 Vail Rd",
282 | "city": "Vail",
283 | "state": "CO",
284 | "zip": "81657"
285 | },
286 | "phones": [
287 | {
288 | "type": "home",
289 | "number": "123.456.7891"
290 | },
291 | {
292 | "type": "work",
293 | "number": "098.765.4322"
294 | }
295 | ],
296 | "aliases": [
297 | "mac",
298 | "markie"
299 | ]
300 | }
301 | ]
302 | ```
303 | ## Data Conversions
304 |
305 | All values from the 'excel' package are returned as text. This module detects numbers and booleans and converts them to javascript types. Booleans must be text 'true' or 'false'. Excel FALSE and TRUE are provided
306 | from 'excel' as 0 and 1 - just too confusing.
307 |
308 | ## Caveats
309 |
310 | During install (mac), you may see compiler warnings while installing the
311 | excel dependency - although questionable, they appear to be benign.
312 |
313 | ## Running tests
314 |
315 | You can run tests after GitHub clone and `npm install` with:
316 |
317 | ```bash
318 | ᐅ npm run-script test
319 |
320 | > excel-as-json@2.0.1 test /Users/starver/code/makara/excel-as-json
321 | > tools/test.sh
322 |
323 | assign
324 | ✓ should assign first level properties
325 | ✓ should assign second level properties
326 | ✓ should assign third level properties
327 | #...
328 | ```
329 |
330 | ## Bug Reports
331 |
332 | To investigate bugs, we need to recreate the failure. In each bug report, please include:
333 |
334 | * Title: A succinct description of the failure
335 | * Body:
336 | * What is expected
337 | * What happened
338 | * What you did
339 | * Environment:
340 | * operating system and version
341 | * node version
342 | * npm version
343 | * excel-as-json version
344 | * Attach a small worksheet and code snippet that reproduces the error
345 |
346 | ## Contributing
347 |
348 | This project is small and simple and intends to remain that way. If you want to add functionality, please raise an issue as a place we can discuss it prior to doing any work.
349 |
350 | You are always free to fork this repo and create your own version to do with as you will, or include this functionality in your projects and modify it to your heart's content.
351 |
352 | ## TODO
353 |
354 | - provide processSync - using 'async' module
355 | - Detect and convert dates
356 | - Make 1 column values a single object?
357 |
358 |
359 | ## Change History
360 |
361 | ### 2.0.2
362 |
363 | - Fix #23 Embedded arrays contain empty string. Flaw in code inserted empty string when no text values were provided for a key like `aliases[]`.
364 | - Fix #30 not able to force numbers as strings. Added option `convertTextToNumber` defaulting to `true`. If set to false, cells containing text that looks like a number are not converted to a numeric type.
365 |
366 |
367 | ### 2.0.1
368 | - Fix creating missing destination directories to complete prior to writing file
369 |
370 |
371 | ### 2.0.0
372 |
373 | - **Breaking changes to most function signatures**
374 | - Replace single option `isColOriented` with an options object to try to stabilize the processFile signature allowing future non-breaking feature additions.
375 | - Add `sheet` option to specify a 1-based index into the Excel sheet collection - all of your data in a single Excel workbook.
376 | - Add `omitEmptyFields` option that removes an object key-value if the corresponding Excel cell is empty.
377 |
378 |
379 | ### 1.0.0
380 |
381 | - Changed process() to processFile() to avoid name collision with node's process object
382 | - Automatically convert text numbers and booleans to native values
383 | - Create destination directory if it does not exist
384 |
385 |
--------------------------------------------------------------------------------
/data/col-oriented.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevetarver/excel-as-json/440aaaa647a0521469cc01952b107ca048d34368/data/col-oriented.xlsx
--------------------------------------------------------------------------------
/data/regression.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevetarver/excel-as-json/440aaaa647a0521469cc01952b107ca048d34368/data/regression.xlsx
--------------------------------------------------------------------------------
/data/row-oriented.csv:
--------------------------------------------------------------------------------
1 | firstName,lastName,address.street,address.city,address.state,address.zip
2 | Jihad,Saladin,12 Beaver Court,Snowmass,CO,81615
3 | Marcus,Rivapoli,16 Vail Rd,Vail,CO,81657
--------------------------------------------------------------------------------
/data/row-oriented.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevetarver/excel-as-json/440aaaa647a0521469cc01952b107ca048d34368/data/row-oriented.xlsx
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "excel-as-json",
3 | "version": "2.0.2",
4 | "description": "Convert Excel data to JSON",
5 | "author": "Steve Tarver ",
6 | "license": "MIT",
7 | "main": "lib/excel-as-json.js",
8 | "scripts": {
9 | "clean": "tools/clean.sh",
10 | "build": "tools/build.sh",
11 | "test": "tools/test.sh",
12 | "dist": "tools/dist.sh",
13 | "codecov": "cat ./coverage/lcov.info | ./node_modules/.bin/codecov",
14 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
15 | "prepublish": "tools/dist.sh"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/stevetarver/excel-as-json.git"
20 | },
21 | "keywords": [
22 | "Excel",
23 | "JSON",
24 | "convert"
25 | ],
26 | "bugs": {
27 | "url": "https://github.com/stevetarver/excel-as-json/issues"
28 | },
29 | "homepage": "https://github.com/stevetarver/excel-as-json",
30 | "dependencies": {
31 | "excel": "0.1.7"
32 | },
33 | "devDependencies": {
34 | "chai": "4.1.2",
35 | "codecov.io": "0.1.6",
36 | "coffee-coverage": "3.0.0",
37 | "coffeescript": "2.2.4",
38 | "coveralls": "3.0.0",
39 | "istanbul": "0.4.5",
40 | "mocha": "5.1.0"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/spec/all-specs.coffee:
--------------------------------------------------------------------------------
1 | require './assignSpec'
2 | require './convertSpec'
3 | require './convertValueSpec'
4 | require './parseKeyNameSpec'
5 | require './transposeSpec'
6 | require './validateOptionsSpec'
7 | require './processFileSpec'
8 | require './regressionSpec'
9 |
--------------------------------------------------------------------------------
/spec/assignSpec.coffee:
--------------------------------------------------------------------------------
1 | assign = require('../src/excel-as-json').assign
2 |
3 | # TODO: How to get chai defined in a more global way
4 | chai = require 'chai'
5 | chai.should()
6 | expect = chai.expect;
7 |
8 | # NOTE: the excel package uses '' for all empty cells
9 | EMPTY_CELL = ''
10 | DEFAULT_OPTIONS =
11 | omitEmptyFields: false
12 | convertTextToNumber: true
13 |
14 |
15 | describe 'assign', ->
16 |
17 | it 'should assign first level properties', ->
18 | subject = {}
19 | assign subject, 'foo', 'clyde', DEFAULT_OPTIONS
20 | subject.foo.should.equal 'clyde'
21 |
22 |
23 | it 'should assign second level properties', ->
24 | subject = {}
25 | assign subject, 'foo.bar', 'wombat', DEFAULT_OPTIONS
26 | subject.foo.bar.should.equal 'wombat'
27 |
28 |
29 | it 'should assign third level properties', ->
30 | subject = {}
31 | assign subject, 'foo.bar.bazz', 'honey badger', DEFAULT_OPTIONS
32 | subject.foo.bar.bazz.should.equal 'honey badger'
33 |
34 |
35 | it 'should convert text to numbers', ->
36 | subject = {}
37 | assign subject, 'foo.bar.bazz', '42', DEFAULT_OPTIONS
38 | subject.foo.bar.bazz.should.equal 42
39 |
40 |
41 | it 'should convert text to booleans', ->
42 | subject = {}
43 | assign subject, 'foo.bar.bazz', 'true', DEFAULT_OPTIONS
44 | subject.foo.bar.bazz.should.equal true
45 | assign subject, 'foo.bar.bazz', 'false', DEFAULT_OPTIONS
46 | subject.foo.bar.bazz.should.equal false
47 |
48 |
49 | it 'should overwrite existing values', ->
50 | subject = {}
51 | assign subject, 'foo.bar.bazz', 'honey badger', DEFAULT_OPTIONS
52 | subject.foo.bar.bazz.should.equal 'honey badger'
53 | assign subject, 'foo.bar.bazz', "don't care", DEFAULT_OPTIONS
54 | subject.foo.bar.bazz.should.equal "don't care"
55 |
56 |
57 | it 'should assign properties to objects in a list', ->
58 | subject = {}
59 | assign subject, 'foo.bar[0].what', 'that', DEFAULT_OPTIONS
60 | subject.foo.bar[0].what.should.equal 'that'
61 |
62 |
63 | it 'should assign properties to objects in a list with first entry out of order', ->
64 | subject = {}
65 | assign subject, 'foo.bar[1].what', 'that', DEFAULT_OPTIONS
66 | assign subject, 'foo.bar[0].what', 'this', DEFAULT_OPTIONS
67 | subject.foo.bar[0].what.should.equal 'this'
68 | subject.foo.bar[1].what.should.equal 'that'
69 |
70 |
71 | it 'should assign properties to objects in a list with second entry out of order', ->
72 | subject = {}
73 | assign subject, 'foo.bar[0].what', 'this', DEFAULT_OPTIONS
74 | assign subject, 'foo.bar[2].what', 'that', DEFAULT_OPTIONS
75 | assign subject, 'foo.bar[1].what', 'other', DEFAULT_OPTIONS
76 | subject.foo.bar[0].what.should.equal 'this'
77 | subject.foo.bar[2].what.should.equal 'that'
78 | subject.foo.bar[1].what.should.equal 'other'
79 |
80 |
81 | it 'should split a semicolon delimited list for flat arrays', ->
82 | subject = {}
83 | assign subject, 'foo.bar[]', 'peter;paul;mary', DEFAULT_OPTIONS
84 | subject.foo.bar.toString().should.equal ['peter','paul','mary'].toString()
85 |
86 |
87 | it 'should convert text in a semicolon delimited list to numbers', ->
88 | subject = {}
89 | assign subject, 'foo.bar[]', 'peter;-43;mary', DEFAULT_OPTIONS
90 | subject.foo.bar.toString().should.equal ['peter',-43,'mary'].toString()
91 |
92 |
93 | it 'should convert text in a semicolon delimited list to booleans', ->
94 | subject = {}
95 | assign subject, 'foo.bar[]', 'peter;false;true', DEFAULT_OPTIONS
96 | subject.foo.bar.toString().should.equal ['peter',false,true].toString()
97 |
98 |
99 | it 'should not split a semicolon list with a terminal indexed array', ->
100 | subject = {}
101 | console.log('Note: warnings on this test expected')
102 | assign subject, 'foo.bar[0]', 'peter;paul;mary', DEFAULT_OPTIONS
103 | subject.foo.bar.should.equal 'peter;paul;mary'
104 |
105 |
106 | it 'should omit empty scalar fields when directed', ->
107 | o =
108 | omitEmptyFields: true
109 | convertTextToNumber: true
110 | subject = {}
111 | assign subject, 'foo', EMPTY_CELL, o
112 | subject.should.not.have.property 'foo'
113 |
114 |
115 | it 'should omit empty nested scalar fields when directed', ->
116 | o =
117 | omitEmptyFields: true
118 | convertTextToNumber: true
119 | subject = {}
120 | assign subject, 'foo.bar', EMPTY_CELL, o
121 | subject.should.have.property 'foo'
122 | subject.foo.should.not.have.property 'bar'
123 |
124 |
125 | it 'should omit nested array fields when directed', ->
126 | o =
127 | omitEmptyFields: true
128 | convertTextToNumber: true
129 |
130 | # specified as an entire list
131 | subject = {}
132 | console.log('Note: warnings on this test expected')
133 | assign subject, 'foo[]', EMPTY_CELL, o
134 | subject.should.not.have.property 'foo'
135 |
136 | # specified as a list
137 | subject = {}
138 | assign subject, 'foo[0]', EMPTY_CELL, o
139 | subject.should.not.have.property 'foo'
140 |
141 | # specified as a list of objects
142 | subject = {}
143 | assign subject, 'foo[0].bar', 'bazz', o
144 | assign subject, 'foo[1].bar', EMPTY_CELL, o
145 | subject.foo[1].should.not.have.property 'bar'
146 |
147 |
148 | it 'should treat text that looks like numbers as text when directed', ->
149 | o =
150 | convertTextToNumber: false
151 |
152 | subject = {}
153 | assign subject, 'part', '00938', o
154 | subject.part.should.be.a('string').and.equal('00938')
155 |
--------------------------------------------------------------------------------
/spec/convertSpec.coffee:
--------------------------------------------------------------------------------
1 | convert = require('../src/excel-as-json').convert
2 |
3 | # TODO: How to get chai defined in a more global way
4 | chai = require 'chai'
5 | chai.should()
6 | expect = chai.expect;
7 |
8 | DEFAULT_OPTIONS =
9 | isColOriented: false
10 | omitEmptyFields: false
11 | convertTextToNumber: true
12 |
13 | describe 'convert', ->
14 |
15 | it 'should convert a row to a list of object', ->
16 | data = [
17 | ['a', 'b', 'c' ],
18 | [ 1, 2, 'true' ]]
19 | result = convert data, DEFAULT_OPTIONS
20 | JSON.stringify(result).should.equal '[{"a":1,"b":2,"c":true}]'
21 |
22 |
23 | it 'should convert rows to a list of objects', ->
24 | data = [
25 | ['a', 'b', 'c'],
26 | [ 1, 2, 3 ],
27 | [ 4, 5, 6 ]]
28 | result = convert data, DEFAULT_OPTIONS
29 | JSON.stringify(result).should.equal '[{"a":1,"b":2,"c":3},{"a":4,"b":5,"c":6}]'
30 |
31 |
32 | it 'should convert rows to a list of objects, omitting empty values', ->
33 | o =
34 | isColOriented: false
35 | omitEmptyFields: true
36 | data = [
37 | ['a', 'b', 'c'],
38 | [ 1, '', 3 ],
39 | [ '', 5, 6 ],
40 | [ '', 5, '' ]]
41 | result = convert data, o
42 | JSON.stringify(result).should.equal '[{"a":1,"c":3},{"b":5,"c":6},{"b":5}]'
43 |
44 |
45 | it 'should convert a column to list of object', ->
46 | o =
47 | isColOriented: true
48 | omitEmptyFields: false
49 | data = [['a', 1],
50 | ['b', 2],
51 | ['c', 3]]
52 | result = convert data, o
53 | JSON.stringify(result).should.equal '[{"a":1,"b":2,"c":3}]'
54 |
55 |
56 | it 'should convert columns to list of objects', ->
57 | o =
58 | isColOriented: true
59 | omitEmptyFields: false
60 | data = [['a', 1, 4 ],
61 | ['b', 2, 5 ],
62 | ['c', 3, 6 ]]
63 | result = convert data, o
64 | JSON.stringify(result).should.equal '[{"a":1,"b":2,"c":3},{"a":4,"b":5,"c":6}]'
65 |
66 |
67 | it 'should understand dotted key paths with 2 elements', ->
68 | data = [
69 | ['a', 'b.a', 'b.b'],
70 | [ 1, 2, 3 ],
71 | [ 4, 5, 6 ]]
72 | result = convert data, DEFAULT_OPTIONS
73 | JSON.stringify(result).should.equal '[{"a":1,"b":{"a":2,"b":3}},{"a":4,"b":{"a":5,"b":6}}]'
74 |
75 |
76 | it 'should understand dotted key paths with 2 elements and omit elements appropriately', ->
77 | o =
78 | isColOriented: false
79 | omitEmptyFields: true
80 | data = [
81 | ['a', 'b.a', 'b.b'],
82 | [ 1, 2, 3 ],
83 | [ '', 5, '' ]]
84 | result = convert data, o
85 | JSON.stringify(result).should.equal '[{"a":1,"b":{"a":2,"b":3}},{"b":{"a":5}}]'
86 |
87 |
88 | it 'should understand dotted key paths with 3 elements', ->
89 | data = [['a', 'b.a.b', 'c'],
90 | [ 1, 2, 3 ],
91 | [ 4, 5, 6 ]]
92 | result = convert data, DEFAULT_OPTIONS
93 | JSON.stringify(result).should.equal '[{"a":1,"b":{"a":{"b":2}},"c":3},{"a":4,"b":{"a":{"b":5}},"c":6}]'
94 |
95 |
96 | it 'should understand indexed arrays in dotted paths', ->
97 | data = [['a[0].a', 'b.a.b', 'c'],
98 | [ 1, 2, 3 ],
99 | [ 4, 5, 6 ]]
100 | result = convert data, DEFAULT_OPTIONS
101 | JSON.stringify(result).should.equal '[{"a":[{"a":1}],"b":{"a":{"b":2}},"c":3},{"a":[{"a":4}],"b":{"a":{"b":5}},"c":6}]'
102 |
103 |
104 | it 'should understand indexed arrays in dotted paths', ->
105 | data = [['a[0].a', 'a[0].b', 'c'],
106 | [ 1, 2, 3 ],
107 | [ 4, 5, 6 ]]
108 | result = convert data, DEFAULT_OPTIONS
109 | JSON.stringify(result).should.equal '[{"a":[{"a":1,"b":2}],"c":3},{"a":[{"a":4,"b":5}],"c":6}]'
110 |
111 |
112 | it 'should understand indexed arrays when out of order', ->
113 | data = [['a[1].a', 'a[0].a', 'c'],
114 | [ 1, 2, 3 ],
115 | [ 4, 5, 6 ]]
116 | result = convert data, DEFAULT_OPTIONS
117 | JSON.stringify(result).should.equal '[{"a":[{"a":2},{"a":1}],"c":3},{"a":[{"a":5},{"a":4}],"c":6}]'
118 |
119 |
120 | it 'should understand indexed arrays in deep dotted paths', ->
121 | data = [['a[0].a', 'b.a[0].b', 'c.a.b[0].d'],
122 | [ 1, 2, 3 ],
123 | [ 4, 5, 6 ]]
124 | result = convert data, DEFAULT_OPTIONS
125 | 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}]}}}]'
126 |
127 |
128 | it 'should understand flat arrays as terminal key names', ->
129 | data = [['a[]', 'b.a[]', 'c.a.b[]'],
130 | ['a;b', 'c;d', 'e;f' ],
131 | ['g;h', 'i;j', 'k;l' ]]
132 | result = convert data, DEFAULT_OPTIONS
133 | 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"]}}}]'
134 |
135 |
136 | it 'should convert text to numbers where appropriate', ->
137 | data = [[ 'a', 'b', 'c' ],
138 | [ '-99', 'test', '2e64']]
139 | result = convert data, DEFAULT_OPTIONS
140 | JSON.stringify(result).should.equal '[{"a":-99,"b":"test","c":2e+64}]'
141 |
142 |
143 | it 'should not convert text that looks like numbers to numbers when directed', ->
144 | o =
145 | convertTextToNumber: false
146 |
147 | data = [[ 'a', 'b', 'c', ],
148 | [ '-99', '00938', '02e64' ]]
149 | result = convert data, o
150 | result[0].should.have.property('a', '-99')
151 | result[0].should.have.property('b', '00938')
152 | result[0].should.have.property('c', '02e64')
153 |
154 |
155 | it 'should not convert numbers to text when convertTextToNumber = false', ->
156 | o =
157 | convertTextToNumber: false
158 |
159 | data = [[ 'a', 'b', 'c', 'd' ],
160 | [ -99, 938, 2e64, 0x4aa ]]
161 | result = convert data, o
162 | result[0].should.have.property('a', -99)
163 | result[0].should.have.property('b', 938)
164 | result[0].should.have.property('c', 2e+64)
165 | result[0].should.have.property('d', 1194)
166 |
167 |
--------------------------------------------------------------------------------
/spec/convertValueSpec.coffee:
--------------------------------------------------------------------------------
1 | convertValue = require('../src/excel-as-json').convertValue
2 |
3 | # TODO: How to get chai defined in a more global way
4 | chai = require 'chai'
5 | chai.should()
6 | expect = chai.expect;
7 |
8 | OPTIONS =
9 | sheet: '1'
10 | isColOriented: false
11 | omitEmptyFields: false
12 | omitKeysWithEmptyValues: false
13 | convertTextToNumber: true
14 |
15 |
16 | describe 'convert value', ->
17 |
18 | it 'should convert text integers to literal numbers', ->
19 | convertValue('1000', OPTIONS).should.be.a('number').and.equal(1000)
20 | convertValue('-999', OPTIONS).should.be.a('number').and.equal(-999)
21 |
22 |
23 | it 'should convert text floats to literal numbers', ->
24 | convertValue('999.0', OPTIONS).should.be.a('number').and.equal(999.0)
25 | convertValue('-100.0', OPTIONS).should.be.a('number').and.equal(-100.0)
26 |
27 |
28 | it 'should convert text exponential numbers to literal numbers', ->
29 | convertValue('2e32', OPTIONS).should.be.a('number').and.equal(2e+32)
30 |
31 |
32 | it 'should not convert things that are not numbers', ->
33 | convertValue('test', OPTIONS).should.be.a('string').and.equal('test')
34 |
35 |
36 | it 'should convert true and false to Boolean', ->
37 | convertValue('true', OPTIONS).should.be.a('boolean').and.equal(true)
38 | convertValue('TRUE', OPTIONS).should.be.a('boolean').and.equal(true)
39 | convertValue('TrUe', OPTIONS).should.be.a('boolean').and.equal(true)
40 | convertValue('false', OPTIONS).should.be.a('boolean').and.equal(false)
41 | convertValue('FALSE', OPTIONS).should.be.a('boolean').and.equal(false)
42 | convertValue('fAlSe', OPTIONS).should.be.a('boolean').and.equal(false)
43 |
44 |
45 | it 'should return blank strings as strings', ->
46 | convertValue('', OPTIONS).should.be.a('string').and.equal('')
47 | convertValue(' ', OPTIONS).should.be.a('string').and.equal(' ')
48 |
49 |
50 | it 'should treat text that looks like numbers as text when directed', ->
51 | o =
52 | convertTextToNumber: false
53 |
54 | convertValue('999.0', o).should.be.a('string').and.equal('999.0')
55 | convertValue('-100.0', o).should.be.a('string').and.equal('-100.0')
56 | convertValue('2e32', o).should.be.a('string').and.equal('2e32')
57 | convertValue('00956', o).should.be.a('string').and.equal('00956')
58 |
59 |
60 | it 'should not convert numbers to text when convertTextToNumber = false', ->
61 | o =
62 | convertTextToNumber: false
63 |
64 | convertValue(999.0, o).should.be.a('number').and.equal(999.0)
65 | convertValue(-100.0, o).should.be.a('number').and.equal(-100.0)
66 | convertValue(2e+32, o).should.be.a('number').and.equal(2e+32)
67 | convertValue(956, o).should.be.a('number').and.equal(956)
68 | convertValue(0x4aa, o).should.be.a('number').and.equal(1194)
69 |
--------------------------------------------------------------------------------
/spec/parseKeyNameSpec.coffee:
--------------------------------------------------------------------------------
1 | parseKeyName = require('../src/excel-as-json').parseKeyName
2 |
3 | # TODO: How to get chai defined in a more global way
4 | chai = require 'chai'
5 | chai.should()
6 | expect = chai.expect;
7 |
8 |
9 | describe 'parse key name', ->
10 |
11 | it 'should parse simple key names', ->
12 | [keyIsList, keyName, index] = parseKeyName 'names'
13 | keyIsList.should.equal false
14 | keyName.should.equal 'names'
15 | expect(index).to.be.an 'undefined'
16 |
17 |
18 | it 'should parse indexed array key names like names[1]', ->
19 | [keyIsList, keyName, index] = parseKeyName 'names[1]'
20 | keyIsList.should.equal true
21 | keyName.should.equal 'names'
22 | index.should.equal 1
23 |
24 |
25 | it 'should parse array key names like names[]', ->
26 | [keyIsList, keyName, index] = parseKeyName 'names[]'
27 | keyIsList.should.equal true
28 | keyName.should.equal 'names'
29 | expect(index).to.be.an 'undefined'
30 |
31 |
32 |
--------------------------------------------------------------------------------
/spec/processFileSpec.coffee:
--------------------------------------------------------------------------------
1 | processFile = require('../src/excel-as-json').processFile
2 | fs = require 'fs'
3 |
4 | # TODO: How to get chai defined in a more global way
5 | chai = require 'chai'
6 | chai.should()
7 | expect = chai.expect;
8 |
9 | ROW_XLSX = 'data/row-oriented.xlsx'
10 | ROW_JSON = 'build/row-oriented.json'
11 | COL_XLSX = 'data/col-oriented.xlsx'
12 | COL_JSON = 'build/col-oriented.json'
13 | COL_JSON_NESTED = 'build/newDir/col-oriented.json'
14 |
15 | 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}}]'
16 | ROW_SHEET_2_JSON = '[{"firstName":"Max","lastName":"Irwin","address":{"street":"123 Fake Street","city":"Rochester","state":"NY","zip":99999}}]'
17 | 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"]}]'
18 | 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"]}]'
19 |
20 | TEST_OPTIONS =
21 | sheet: '1'
22 | isColOriented: false
23 | omitEmptyFields: false
24 |
25 |
26 | describe 'process file', ->
27 |
28 | it 'should notify on file does not exist', (done) ->
29 | processFile 'data/doesNotExist.xlsx', null, TEST_OPTIONS, (err, data) ->
30 | err.should.be.a 'string'
31 | expect(data).to.be.an 'undefined'
32 | done()
33 |
34 |
35 | it 'should not blow up when a file does not exist and no callback is provided', (done) ->
36 | processFile 'data/doesNotExist.xlsx', ->
37 | done()
38 |
39 |
40 | it 'should not blow up on read error when no callback is provided', (done) ->
41 | processFile 'data/row-oriented.csv', ->
42 | done()
43 |
44 |
45 | it 'should notify on read error', (done) ->
46 | processFile 'data/row-oriented.csv', null, TEST_OPTIONS, (err, data) ->
47 | err.should.be.a 'string'
48 | expect(data).to.be.an 'undefined'
49 | done()
50 |
51 |
52 | # NOTE: current excel package impl simply times out if sheet index is OOR
53 | # it 'should show error on invalid sheet id', (done) ->
54 | # options =
55 | # sheet: '20'
56 | # isColOriented: false
57 | # omitEmptyFields: false
58 | #
59 | # processFile ROW_XLSX, null, options, (err, data) ->
60 | # err.should.be.a 'string'
61 | # expect(data).to.be.an 'undefined'
62 | # done()
63 |
64 |
65 | it 'should use defaults when caller specifies no options', (done) ->
66 | processFile ROW_XLSX, null, null, (err, data) ->
67 | expect(err).to.be.an 'undefined'
68 | JSON.stringify(data).should.equal ROW_SHEET_1_JSON
69 | done()
70 |
71 |
72 | it 'should process row oriented Excel files, write the result, and return the parsed object', (done) ->
73 | options =
74 | sheet:'1'
75 | isColOriented: false
76 | omitEmptyFields: false
77 |
78 | processFile ROW_XLSX, ROW_JSON, options, (err, data) ->
79 | expect(err).to.be.an 'undefined'
80 | result = JSON.parse(fs.readFileSync(ROW_JSON, 'utf8'))
81 | JSON.stringify(result).should.equal ROW_SHEET_1_JSON
82 | JSON.stringify(data).should.equal ROW_SHEET_1_JSON
83 | done()
84 |
85 |
86 | it 'should process sheet 2 of row oriented Excel files, write the result, and return the parsed object', (done) ->
87 | options =
88 | sheet:'2'
89 | isColOriented: false
90 | omitEmptyFields: false
91 |
92 | processFile ROW_XLSX, ROW_JSON, options, (err, data) ->
93 | expect(err).to.be.an 'undefined'
94 | result = JSON.parse(fs.readFileSync(ROW_JSON, 'utf8'))
95 | JSON.stringify(result).should.equal ROW_SHEET_2_JSON
96 | JSON.stringify(data).should.equal ROW_SHEET_2_JSON
97 | done()
98 |
99 |
100 | it 'should process col oriented Excel files, write the result, and return the parsed object', (done) ->
101 | options =
102 | sheet:'1'
103 | isColOriented: true
104 | omitEmptyFields: false
105 |
106 | processFile COL_XLSX, COL_JSON, options, (err, data) ->
107 | expect(err).to.be.an 'undefined'
108 | result = JSON.parse(fs.readFileSync(COL_JSON, 'utf8'))
109 | JSON.stringify(result).should.equal COL_SHEET_1_JSON
110 | JSON.stringify(data).should.equal COL_SHEET_1_JSON
111 | done()
112 |
113 |
114 | it 'should process sheet 2 of col oriented Excel files, write the result, and return the parsed object', (done) ->
115 | options =
116 | sheet:'2'
117 | isColOriented: true
118 | omitEmptyFields: false
119 |
120 | processFile COL_XLSX, COL_JSON, options, (err, data) ->
121 | expect(err).to.be.an 'undefined'
122 | result = JSON.parse(fs.readFileSync(COL_JSON, 'utf8'))
123 | JSON.stringify(result).should.equal COL_SHEET_2_JSON
124 | JSON.stringify(data).should.equal COL_SHEET_2_JSON
125 | done()
126 |
127 |
128 | it 'should create the destination directory if it does not exist', (done) ->
129 | options =
130 | sheet:'1'
131 | isColOriented: true
132 | omitEmptyFields: false
133 |
134 | processFile COL_XLSX, COL_JSON_NESTED, options, (err, data) ->
135 | expect(err).to.be.an 'undefined'
136 | result = JSON.parse(fs.readFileSync(COL_JSON_NESTED, 'utf8'))
137 | JSON.stringify(result).should.equal COL_SHEET_1_JSON
138 | JSON.stringify(data).should.equal COL_SHEET_1_JSON
139 | done()
140 |
141 |
142 | it 'should return a parsed object without writing a file', (done) ->
143 | # Ensure result file does not exit
144 | try fs.unlinkSync ROW_JSON
145 | catch # ignore file does not exist
146 |
147 | options =
148 | sheet:'1'
149 | isColOriented: false
150 | omitEmptyFields: false
151 |
152 | processFile ROW_XLSX, undefined, options, (err, data) ->
153 | expect(err).to.be.an 'undefined'
154 | fs.existsSync(ROW_JSON).should.equal false
155 | JSON.stringify(data).should.equal ROW_SHEET_1_JSON
156 | done()
157 |
158 |
159 | it 'should not convert text that looks like a number to a number when directed', (done) ->
160 | options =
161 | sheet:'1'
162 | isColOriented: false
163 | omitEmptyFields: false
164 | convertTextToNumber: false
165 |
166 | processFile ROW_XLSX, undefined, options, (err, data) ->
167 | expect(err).to.be.an 'undefined'
168 | data[0].address.should.have.property('zip', '81615')
169 | data[1].address.should.have.property('zip', '81657')
170 | done()
171 |
172 |
173 | it 'should notify on write error', (done) ->
174 | processFile ROW_XLSX, 'build', TEST_OPTIONS, (err, data) ->
175 | expect(err).to.be.an 'string'
176 | done()
177 |
178 |
179 | #=============================== Coverage summary ===============================
180 | # Statements : 100% ( 133/133 )
181 | # Branches : 100% ( 61/61 )
182 | # Functions : 100% ( 14/14 )
183 | # Lines : 100% ( 106/106 )
184 | #================================================================================
185 |
--------------------------------------------------------------------------------
/spec/regressionSpec.coffee:
--------------------------------------------------------------------------------
1 | processFile = require('../src/excel-as-json').processFile
2 | fs = require 'fs'
3 |
4 | # TODO: How to get chai defined in a more global way
5 | chai = require 'chai'
6 | chai.should()
7 | expect = chai.expect;
8 |
9 | # Test constants
10 | RGR_SRC_XLSX = 'data/regression.xlsx'
11 |
12 | RGR23_SHEET = 1
13 | RGR23_IS_COL_ORIENTED = true
14 | RGR23_OUT_JSON = 'build/rgr23.json'
15 |
16 | RGR28_SHEET = 2
17 | RGR28_IS_COL_ORIENTED = false
18 | RGR28_OUT_JSON = 'build/rgr28.json'
19 |
20 | describe 'regression 23', ->
21 |
22 | it 'should produce empty arrays for flat arrays without values', (done) ->
23 | options =
24 | sheet: RGR23_SHEET
25 | isColOriented: RGR23_IS_COL_ORIENTED
26 | omitEmptyFields: false
27 |
28 | processFile RGR_SRC_XLSX, RGR23_OUT_JSON, options, (err, data) ->
29 | expect(err).to.be.an 'undefined'
30 | expect(data[0]).to.have.property('emptyArray').with.lengthOf(0)
31 | done()
32 |
33 | it 'should remove flat arrays when omitEmptyFields and value list is blank', (done) ->
34 | options =
35 | sheet: RGR23_SHEET
36 | isColOriented: RGR23_IS_COL_ORIENTED
37 | omitEmptyFields: true
38 |
39 | processFile RGR_SRC_XLSX, RGR23_OUT_JSON, options, (err, data) ->
40 | expect(err).to.be.an 'undefined'
41 | expect(data[0].emptyArray).to.be.an 'undefined'
42 | done()
43 |
44 |
45 | describe 'regression 28', ->
46 |
47 | it 'should produce an empty array when no value rows are provided', (done) ->
48 | options =
49 | sheet: RGR28_SHEET
50 | isColOriented: RGR28_IS_COL_ORIENTED
51 | omitEmptyFields: false
52 |
53 | processFile RGR_SRC_XLSX, RGR28_OUT_JSON, options, (err, data) ->
54 | expect(err).to.be.an 'undefined'
55 | expect(data).to.be.an('array').with.lengthOf(0)
56 | done()
57 |
58 |
--------------------------------------------------------------------------------
/spec/transposeSpec.coffee:
--------------------------------------------------------------------------------
1 | transpose = require('../src/excel-as-json').transpose
2 |
3 | # TODO: How to get chai defined in a more global way
4 | chai = require 'chai'
5 | chai.should()
6 | expect = chai.expect;
7 |
8 |
9 | _removeDuplicates = (array) ->
10 | set = {}
11 | set[array[key]] = array[key] for key in [0..array.length-1]
12 | return (key for key of set)
13 |
14 |
15 | describe 'transpose', ->
16 |
17 | square = [
18 | ['one', 'two', 'three'],
19 | ['one', 'two', 'three'],
20 | ['one', 'two', 'three']
21 | ]
22 |
23 | rectangleWide = [
24 | ['one', 'two', 'three'],
25 | ['one', 'two', 'three']
26 | ]
27 |
28 | rectangleTall = [
29 | ['one', 'two'],
30 | ['one', 'two'],
31 | ['one', 'two']
32 | ]
33 |
34 |
35 | it 'should transpose square 2D arrays', ->
36 | result = transpose square
37 | result.length.should.equal 3
38 |
39 | for row in result
40 | row.length.should.equal 3
41 | _removeDuplicates(row).length.should.equal 1
42 |
43 |
44 | it 'should transpose wide rectangular 2D arrays', ->
45 | result = transpose rectangleWide
46 | result.length.should.equal 3
47 |
48 | for row in result
49 | row.length.should.equal 2
50 | _removeDuplicates(row).length.should.equal 1
51 |
52 |
53 | it 'should transpose tall rectangular 2D arrays', ->
54 | result = transpose rectangleTall
55 | result.length.should.equal 2
56 |
57 | for row in result
58 | row.length.should.equal 3
59 | _removeDuplicates(row).length.should.equal 1
60 |
61 |
62 |
--------------------------------------------------------------------------------
/spec/validateOptionsSpec.coffee:
--------------------------------------------------------------------------------
1 | _validateOptions = require('../src/excel-as-json')._validateOptions
2 |
3 | # TODO: How to get chai defined in a more global way
4 | chai = require 'chai'
5 | chai.should()
6 | expect = chai.expect;
7 |
8 | TEST_OPTIONS =
9 | sheet: '1'
10 | isColOriented: false
11 | omitEmptyFields: false
12 |
13 | describe 'validate options', ->
14 |
15 | it 'should provide default options when none are specified', (done) ->
16 | options = _validateOptions(null)
17 | options.sheet.should.equal '1'
18 | options.isColOriented.should.equal false
19 | options.omitEmptyFields.should.equal false
20 |
21 | options = _validateOptions(undefined)
22 | options.sheet.should.equal '1'
23 | options.isColOriented.should.equal false
24 | options.omitEmptyFields.should.equal false
25 | done()
26 |
27 |
28 | it 'should fill in missing sheet id', (done) ->
29 | o =
30 | isColOriented: false
31 | omitEmptyFields: false
32 |
33 | options = _validateOptions(o)
34 | options.sheet.should.equal '1'
35 | options.isColOriented.should.equal false
36 | options.omitEmptyFields.should.equal false
37 | done()
38 |
39 |
40 | it 'should fill in missing isColOriented', (done) ->
41 | o =
42 | sheet: '1'
43 | omitEmptyFields: false
44 |
45 | options = _validateOptions(o)
46 | options.sheet.should.equal '1'
47 | options.isColOriented.should.equal false
48 | options.omitEmptyFields.should.equal false
49 | done()
50 |
51 |
52 | it 'should fill in missing omitEmptyFields', (done) ->
53 | o =
54 | sheet: '1'
55 | isColOriented: false
56 |
57 | options = _validateOptions(o)
58 | options.sheet.should.equal '1'
59 | options.isColOriented.should.equal false
60 | options.omitEmptyFields.should.equal false
61 | done()
62 |
63 |
64 | it 'should convert a numeric sheet id to text', (done) ->
65 | o =
66 | sheet: 3
67 | isColOriented: false
68 | omitEmptyFields: true
69 |
70 | options = _validateOptions(o)
71 | options.sheet.should.equal '3'
72 | options.isColOriented.should.equal false
73 | options.omitEmptyFields.should.equal true
74 | done()
75 |
76 |
77 | it 'should detect invalid sheet ids and replace with the default', (done) ->
78 | o =
79 | sheet: 'one'
80 | isColOriented: false
81 | omitEmptyFields: true
82 |
83 | options = _validateOptions(o)
84 | options.sheet.should.equal '1'
85 | options.isColOriented.should.equal false
86 | options.omitEmptyFields.should.equal true
87 |
88 | o.sheet = 0
89 | options = _validateOptions(o)
90 | options.sheet.should.equal '1'
91 |
92 | o.sheet = true
93 | options = _validateOptions(o)
94 | options.sheet.should.equal '1'
95 |
96 | o.sheet = isNaN
97 | options = _validateOptions(o)
98 | options.sheet.should.equal '1'
99 | done()
100 |
--------------------------------------------------------------------------------
/src/excel-as-json.coffee:
--------------------------------------------------------------------------------
1 | # Create a list of json objects; 1 object per excel sheet row
2 | #
3 | # Assume: Excel spreadsheet is a rectangle of data, where the first row is
4 | # object keys and remaining rows are object values and the desired json
5 | # is a list of objects. Alternatively, data may be column oriented with
6 | # col 0 containing key names.
7 | #
8 | # Dotted notation: Key row (0) containing firstName, lastName, address.street,
9 | # address.city, address.state, address.zip would produce, per row, a doc with
10 | # first and last names and an embedded doc named address, with the address.
11 | #
12 | # Arrays: may be indexed (phones[0].number) or flat (aliases[]). Indexed
13 | # arrays imply a list of objects. Flat arrays imply a semicolon delimited list.
14 | #
15 | # USE:
16 | # From a shell
17 | # coffee src/excel-as-json.coffee
18 | #
19 | fs = require 'fs'
20 | path = require 'path'
21 | excel = require 'excel'
22 |
23 | BOOLTEXT = ['true', 'false']
24 | BOOLVALS = {'true': true, 'false': false}
25 |
26 | isArray = (obj) ->
27 | Object.prototype.toString.call(obj) is '[object Array]'
28 |
29 |
30 | # Extract key name and array index from names[1] or names[]
31 | # return [keyIsList, keyName, index]
32 | # for names[1] return [true, keyName, index]
33 | # for names[] return [true, keyName, undefined]
34 | # for names return [false, keyName, undefined]
35 | parseKeyName = (key) ->
36 | index = key.match(/\[(\d+)\]$/)
37 | switch
38 | when index then [true, key.split('[')[0], Number(index[1])]
39 | when key[-2..] is '[]' then [true, key[...-2], undefined]
40 | else [false, key, undefined]
41 |
42 |
43 | # Convert a list of values to a list of more native forms
44 | convertValueList = (list, options) ->
45 | (convertValue(item, options) for item in list)
46 |
47 |
48 | # Convert values to native types
49 | # Note: all values from the excel module are text
50 | convertValue = (value, options) ->
51 | # isFinite returns true for empty or blank strings, check for those first
52 | if value.length == 0 || !/\S/.test(value)
53 | value
54 | else if isFinite(value)
55 | if options.convertTextToNumber
56 | Number(value)
57 | else
58 | value
59 | else
60 | testVal = value.toLowerCase()
61 | if testVal in BOOLTEXT
62 | BOOLVALS[testVal]
63 | else
64 | value
65 |
66 |
67 | # Assign a value to a dotted property key - set values on sub-objects
68 | assign = (obj, key, value, options) ->
69 | # On first call, a key is a string. Recursed calls, a key is an array
70 | key = key.split '.' unless typeof key is 'object'
71 | # Array element accessors look like phones[0].type or aliases[]
72 | [keyIsList, keyName, index] = parseKeyName key.shift()
73 |
74 | if key.length
75 | if keyIsList
76 | # if our object is already an array, ensure an object exists for this index
77 | if isArray obj[keyName]
78 | unless obj[keyName][index]
79 | obj[keyName].push({}) for i in [obj[keyName].length..index]
80 | # else set this value to an array large enough to contain this index
81 | else
82 | obj[keyName] = ({} for i in [0..index])
83 | assign obj[keyName][index], key, value, options
84 | else
85 | obj[keyName] ?= {}
86 | assign obj[keyName], key, value, options
87 | else
88 | if keyIsList and index?
89 | console.error "WARNING: Unexpected key path terminal containing an indexed list for <#{keyName}>"
90 | console.error "WARNING: Indexed arrays indicate a list of objects and should not be the last element in a key path"
91 | console.error "WARNING: The last element of a key path should be a key name or flat array. E.g. alias, aliases[]"
92 | if (keyIsList and not index?)
93 | if value != ''
94 | obj[keyName] = convertValueList(value.split(';'), options)
95 | else if !options.omitEmptyFields
96 | obj[keyName] = []
97 | else
98 | if !(options.omitEmptyFields && value == '')
99 | obj[keyName] = convertValue(value, options)
100 |
101 |
102 | # Transpose a 2D array
103 | transpose = (matrix) ->
104 | (t[i] for t in matrix) for i in [0...matrix[0].length]
105 |
106 |
107 | # Convert 2D array to nested objects. If row oriented data, row 0 is dotted key names.
108 | # Column oriented data is transposed
109 | convert = (data, options) ->
110 | data = transpose data if options.isColOriented
111 |
112 | keys = data[0]
113 | rows = data[1..]
114 |
115 | result = []
116 | for row in rows
117 | item = {}
118 | assign(item, keys[index], value, options) for value, index in row
119 | result.push item
120 | return result
121 |
122 |
123 | # Write JSON encoded data to file
124 | # call back is callback(err)
125 | write = (data, dst, callback) ->
126 | # Create the target directory if it does not exist
127 | dir = path.dirname(dst)
128 | fs.mkdirSync dir if !fs.existsSync(dir)
129 | fs.writeFile dst, JSON.stringify(data, null, 2), (err) ->
130 | if err then callback "Error writing file #{dst}: #{err}"
131 | else callback undefined
132 |
133 |
134 | # src: xlsx file that we will read sheet 0 of
135 | # dst: file path to write json to. If null, simply return the result
136 | # options: see below
137 | # callback(err, data): callback for completion notification
138 | #
139 | # options:
140 | # sheet: string; 1: numeric, 1-based index of target sheet
141 | # isColOriented: boolean: false; are objects stored in excel columns; key names in col A
142 | # omitEmptyFields: boolean: false: do not include keys with empty values in json output. empty values are stored as ''
143 | # TODO: this is probably better named omitKeysWithEmptyValues
144 | # convertTextToNumber boolean: true; if text looks like a number, convert it to a number
145 | #
146 | # convertExcel(src, dst)
147 | # will write a row oriented xlsx sheet 1 to `dst` as JSON with no notification
148 | # convertExcel(src, dst, {isColOriented: true})
149 | # will write a col oriented xlsx sheet 1 to file with no notification
150 | # convertExcel(src, dst, {isColOriented: true}, callback)
151 | # will write a col oriented xlsx to file and notify with errors and parsed data
152 | # convertExcel(src, null, null, callback)
153 | # will parse a row oriented xslx using default options and return errors and the parsed data in the callback
154 | #
155 | _DEFAULT_OPTIONS =
156 | sheet: '1'
157 | isColOriented: false
158 | omitEmptyFields: false
159 | convertTextToNumber: true
160 |
161 | # Ensure options sane, provide defaults as appropriate
162 | _validateOptions = (options) ->
163 | if !options
164 | options = _DEFAULT_OPTIONS
165 | else
166 | if !options.hasOwnProperty('sheet')
167 | options.sheet = '1'
168 | else
169 | # ensure sheet is a text representation of a number
170 | if !isNaN(parseFloat(options.sheet)) && isFinite(options.sheet)
171 | if options.sheet < 1
172 | options.sheet = '1'
173 | else
174 | # could be 3 or '3'; force to be '3'
175 | options.sheet = '' + options.sheet
176 | else
177 | # something bizarre like true, [Function: isNaN], etc
178 | options.sheet = '1'
179 | if !options.hasOwnProperty('isColOriented')
180 | options.isColOriented = false
181 | if !options.hasOwnProperty('omitEmptyFields')
182 | options.omitEmptyFields = false
183 | if !options.hasOwnProperty('convertTextToNumber')
184 | options.convertTextToNumber = true
185 | options
186 |
187 |
188 | processFile = (src, dst, options=_DEFAULT_OPTIONS, callback=undefined) ->
189 | options = _validateOptions(options)
190 |
191 | # provide a callback if the user did not
192 | if !callback then callback = (err, data) ->
193 |
194 | # NOTE: 'excel' does not properly bubble file not found and prints
195 | # an ugly error we can't trap, so look for this common error first
196 | if not fs.existsSync src
197 | callback "Cannot find src file #{src}"
198 | else
199 | excel src, options.sheet, (err, data) ->
200 | if err
201 | callback "Error reading #{src}: #{err}"
202 | else
203 | result = convert data, options
204 | if dst
205 | write result, dst, (err) ->
206 | if err then callback err
207 | else callback undefined, result
208 | else
209 | callback undefined, result
210 |
211 | # This is the single expected module entry point
212 | exports.processFile = processFile
213 |
214 | # Unsupported use
215 | # Exposing remaining functionality for unexpected use cases, testing, etc.
216 | exports.assign = assign
217 | exports.convert = convert
218 | exports.convertValue = convertValue
219 | exports.parseKeyName = parseKeyName
220 | exports._validateOptions = _validateOptions
221 | exports.transpose = transpose
222 |
--------------------------------------------------------------------------------
/tools/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Compile coffee src/test files
4 | coffee -c -o lib/ src/
5 | coffee -c -o test/ spec/
6 |
7 | # Replace the CoffeeScript test file reference to CoffeeScript source with js equivalents
8 | sed -i '' -e 's/\.\.\/src\/excel-as-json/\.\.\/lib\/excel-as-json/' test/*
9 |
--------------------------------------------------------------------------------
/tools/clean.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | rm -rf build
3 | rm -rf coverage
4 | rm -rf lib
5 | rm -rf test
6 |
--------------------------------------------------------------------------------
/tools/coffee-coverage-loader.js:
--------------------------------------------------------------------------------
1 | // A custom coffee-coverage loader to exclude non-source files
2 | // https://github.com/benbria/coffee-coverage/blob/master/docs/HOWTO-istanbul.md
3 | // https://github.com/benbria/coffee-coverage/blob/master/docs/HOWTO-istanbul.md#writing-a-custom-loader
4 | var coffeeCoverage = require('coffee-coverage');
5 | var coverageVar = coffeeCoverage.findIstanbulVariable();
6 | var writeOnExit = coverageVar == null ? true : null;
7 |
8 | coffeeCoverage.register({
9 | instrumentor: 'istanbul',
10 | basePath: process.cwd(),
11 | exclude: ['/spec', '/node_modules', '/.git'],
12 | coverageVar: coverageVar,
13 | writeOnExit: writeOnExit ? ((_ref = process.env.COFFEECOV_OUT) != null ? _ref : 'coverage/coverage-coffee.json') : null,
14 | initAll: false // ignore files in project root (Gruntfile.coffee)
15 | });
--------------------------------------------------------------------------------
/tools/dist.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | ./tools/clean.sh
3 | ./tools/build.sh
4 | ./tools/test.sh
--------------------------------------------------------------------------------
/tools/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Clean this one temp dir to ensure accurate code coverage
4 | rm -rf build
5 |
6 | # Use our custom coffee-coverage loader to generate instrumented coffee files
7 | mocha -R spec --compilers coffee:coffeescript/register \
8 | --require ./tools/coffee-coverage-loader.js \
9 | spec/all-specs.coffee
10 |
11 | # Generate reports for dev and upload to Coveralls, CodeCov
12 | istanbul report text-summary lcov
--------------------------------------------------------------------------------