├── .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 | [![tag:?](https://img.shields.io/github/tag/stevetarver/excel-as-json.svg)](https://github.com/stevetarver/excel-as-json/releases) 2 | [![license:mit](https://img.shields.io/badge/license-mit-green.svg)](#license) 3 | [![build:?](https://img.shields.io/travis/stevetarver/excel-as-json/master.svg)](https://travis-ci.org/stevetarver/excel-as-json) 4 | [![coverage:?](https://img.shields.io/coveralls/stevetarver/excel-as-json/master.svg?style=flat-square)](https://coveralls.io/r/stevetarver/excel-as-json) 5 | [![codecov.io](http://codecov.io/github/stevetarver/excel-as-json/coverage.svg?branch=master)](http://codecov.io/github/stevetarver/excel-as-json?branch=master) 6 |
7 | [![npm:](https://img.shields.io/npm/v/excel-as-json.svg)](https://www.npmjs.com/package/excel-as-json) 8 | [![dependencies:?](https://img.shields.io/david/stevetarver/excel-as-json.svg)](https://david-dm.org/stevetarver/excel-as-json.svg) 9 | [![devDependency Status](https://david-dm.org/stevetarver/excel-as-json/dev-status.svg)](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 --------------------------------------------------------------------------------