├── .gitignore ├── .jshintrc ├── .npmignore ├── CHANGELOG.md ├── README.md ├── examples ├── read-simple.js ├── read-stream.js ├── sample.xlsx └── write-simple.js ├── index.js ├── install.sh ├── lib ├── spreadsheet-reader.js ├── spreadsheet-writer.js └── utilities.js ├── package.json ├── python ├── excel_reader.py ├── excel_writer.py ├── excel_writer_xlsxwriter.py └── excel_writer_xlwt.py └── test ├── .jshintrc ├── input ├── sample.xls └── sample.xlsx ├── test-spreadsheet-reader.js ├── test-spreadsheet-writer.js └── test-utilities.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | node_modules 3 | deps 4 | examples/output* 5 | test/output* 6 | python/dateutil 7 | python/openpyxl 8 | python/xlrd 9 | python/xlsxwriter 10 | python/xlwt 11 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [], 3 | "browser": false, 4 | "node": true, 5 | "curly": false, 6 | "strict": false, 7 | "expr": true, 8 | "unused": "vars" 9 | } 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | deps/ 2 | python/openpyxl/ 3 | python/xlrd/ 4 | python/xlsxwriter/ 5 | python/xlwt/ 6 | examples/output* 7 | tests/output* 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.4 2 | ======= 3 | * installing with git instead of wget and curl 4 | 5 | # 0.1.3 6 | ======= 7 | * maintenance: simplified Python scripts, improved performance 8 | * vastly improved documentation 9 | 10 | # 0.1.2 11 | ======= 12 | * fixed resolving of scripts with `python-shell` 13 | * extended errors with traceback support for exceptions thrown from Python code 14 | 15 | # 0.1.1 16 | ======= 17 | * moved `python-shell` into a separate and more robust module 18 | 19 | # 0.1.0 20 | ======= 21 | * writing spreadsheet files no longer rely on a temporary file 22 | * `SpreadsheetWriter` is now an `EventEmitter` and fires `open`, `close` and `error` events 23 | * `SpreadsheetWriter.save` no longer accept a path argument, path is now assigned via the constructor instead 24 | * `SpreadsheetWriter.save` no longer return a `stream.Readable` when callback is omitted 25 | * `SpreadsheetWriter.destroy` has been removed, use `save` instead to close the underlying process 26 | * Incomplete integration of xlwt for writing native XLS files 27 | 28 | # 0.0.4 29 | ======= 30 | * JSHint linting and fixes 31 | * minor changes to coding style 32 | * refactoring for removal of dependencies (underscore and async) 33 | * improved compatibility of installation script 34 | 35 | # 0.0.3 36 | ======= 37 | * bug fixes 38 | * improved debugging with [debug](visionmedia/debug) 39 | 40 | # 0.0.2 41 | ======= 42 | * documentation 43 | 44 | # 0.0.1 45 | ======= 46 | * initial version 47 | * moved from [extrabacon/xlrd-parser](https://github.com/extrabacon/xlrd-parser) 48 | * added support for writing XLSX 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PySpreadsheet 2 | 3 | A high-performance spreadsheet library for Node, powered by Python open source libraries. PySpreadsheet can be used 4 | to read and write Excel files in both XLS and XLSX formats. 5 | 6 | IMPORTANT: PySpreadsheet is a work in progress. This is not a stable release. 7 | 8 | ## Features 9 | 10 | + Faster and more memory efficient than most JS-only alternatives 11 | + Uses child processes for isolation and parallelization (will not leak the Node process) 12 | + Support for both XLS and XLSX formats 13 | + Can stream large files with a familiar API 14 | + Native integration with Javascript objects 15 | 16 | ## Limitations 17 | 18 | + Reading does not parse formats, only data 19 | + Cannot edit existing files 20 | + Basic XLS writing capabilities 21 | + Incomplete API 22 | 23 | ## Installation 24 | ```bash 25 | npm install pyspreadsheet 26 | ``` 27 | 28 | Python dependencies are installed automatically by downloading the latest version from their repositories. These dependencies are: 29 | 30 | + [xlrd](http://github.com/python-excel/xlrd) and [xlwt](http://github.com/python-excel/xlwt), by Python Excel 31 | + [XlsxWriter](http://github.com/jmcnamara/XlsxWriter), by John McNamara 32 | 33 | ## Documentation 34 | 35 | ### Reading a file with the `SpreadsheetReader` class 36 | 37 | Use the `SpreadsheetReader` class to read spreadsheet files. It can be used for reading an entire file into memory or as a stream-like object. 38 | 39 | #### Reading a file into memory 40 | 41 | Reading a file into memory will output the entire contents of the file in an easy-to-use structure. 42 | 43 | ```js 44 | var SpreadsheetReader = require('pyspreadsheet').SpreadsheetReader; 45 | 46 | SpreadsheetReader.read('input.xlsx', function (err, workbook) { 47 | // Iterate on sheets 48 | workbook.sheets.forEach(function (sheet) { 49 | console.log('sheet: %s', sheet.name); 50 | // Iterate on rows 51 | sheet.rows.forEach(function (row) { 52 | // Iterate on cells 53 | row.forEach(function (cell) { 54 | console.log('%s: %s', cell.address, cell.value); 55 | }); 56 | }); 57 | }); 58 | }); 59 | ``` 60 | 61 | #### Streaming a large file 62 | 63 | You can use `SpreadsheetReader` just like a Node readable stream. This is the preferred method for reading larger files. 64 | 65 | ```javascript 66 | var SpreadsheetReader = require('pyspreadsheet').SpreadsheetReader; 67 | var reader = new SpreadsheetReader('examples/sample.xlsx'); 68 | 69 | reader.on('open', function (workbook) { 70 | // file is open 71 | console.log('opened ' + workbook.file); 72 | }).on('data', function (data) { 73 | // data is being received 74 | console.log('buffer contains %d rows from sheet "%s"', data.rows.length, data.sheet.name); 75 | }).on('close', function () { 76 | // file is now closed 77 | console.log('file closed'); 78 | }).on('error', function (err) { 79 | // got an error 80 | throw err; 81 | }); 82 | ``` 83 | 84 | ### Writing a file with the `SpreadsheetWriter` class 85 | 86 | Use the `SpreadsheetWriter` class to write files. It can only write new files, it cannot edit existing files. 87 | 88 | ```js 89 | var SpreadsheetWriter = require('pyspreadsheet').SpreadsheetWriter; 90 | var writer = new SpreadsheetWriter('examples/output.xlsx'); 91 | 92 | // write a string at cell A1 93 | writer.write(0, 0, 'hello world!'); 94 | 95 | writer.save(function (err) { 96 | if (err) throw err; 97 | console.log('file saved!'); 98 | }); 99 | ``` 100 | 101 | #### Adding sheets 102 | 103 | Use the `addSheet` method to add a new sheet. If data is written without adding a sheet first, a default "Sheet1" is automatically added. 104 | 105 | ```js 106 | var SpreadsheetWriter = require('pyspreadsheet').SpreadsheetWriter; 107 | var writer = new SpreadsheetWriter('examples/output.xlsx'); 108 | writer.addSheet('my sheet').write(0, 0, 'hello'); 109 | ``` 110 | 111 | #### Writing data 112 | 113 | Use the `write` method to write data to the designated cell. All Javascript built-in types are supported. 114 | 115 | ```js 116 | writer.write(0, 0, 'hello world!'); 117 | ``` 118 | 119 | ##### Formulas 120 | 121 | Formulas are parsed from strings. To write formulas, just prepend your string value with "=". 122 | 123 | ```js 124 | writer.write(2, 0, '=A1+A2'); 125 | ``` 126 | 127 | Note that values calculated from formulas cannot be obtained until the file has been opened once with a spreadsheet client (like Microsoft Excel). 128 | 129 | ##### Writing multiple cells 130 | 131 | Use arrays to write multiple cells at once horizontally. 132 | 133 | ```js 134 | writer.write(0, 0, ['a', 'b', 'c', 'd', 'e']); 135 | ``` 136 | 137 | Use two-dimensional arrays to write multiple rows at once. 138 | 139 | ```js 140 | writer.write(0, 0, [ 141 | ['a', 'b', 'c', 'd', 'e'], 142 | ['1', '2', '3', '4', '5'], 143 | ]); 144 | ``` 145 | 146 | ### Formatting 147 | 148 | Cells can be formatted by specifying format properties. 149 | 150 | ```js 151 | writer.write(0, 0, 'hello', { 152 | font: { 153 | name: 'Calibri', 154 | size: 12 155 | } 156 | }); 157 | ``` 158 | 159 | Formats can also be reused by using the `addFormat` method. 160 | 161 | ```js 162 | writer.addFormat('title', { 163 | font: { bold: true, color: '#ffffff' }, 164 | fill: '#000000' 165 | }); 166 | 167 | writer.write(0, 0, ['heading 1', 'heading 2', 'heading 3'], 'title'); 168 | ``` 169 | 170 | ## API Reference 171 | 172 | ### SpreadsheetReader 173 | 174 | The `SpreadsheetReader` is used to read spreadsheet files from various formats. 175 | 176 | #### #ctor(path, options) 177 | 178 | Creates a new `SpreadsheetReader` instance. 179 | 180 | * `path` - the path of the file to read, also accepting arrays for reading multiple files at once 181 | * `options` - the reading options (optional) 182 | * `meta` - load only workbook metadata, without iterating on rows 183 | * `sheet` || `sheets` - load sheet(s) selectively, either by name or by index 184 | * `maxRows` - the maximum number of rows to load per sheet 185 | * `bufferSize` - the maximum number of rows to accumulate in the buffer (default: 20) 186 | 187 | #### #read(path, options, callback) 188 | 189 | Reads an entire file into memory. 190 | 191 | * `path` - the path of the file to read, also accepting arrays for reading multiple files at once 192 | * `options` - the reading options (optional) 193 | * `meta` - load only workbook metadata, without iterating on rows 194 | * `sheet` || `sheets` - load sheet(s) selectively, either by name or by index 195 | * `maxRows` - the maximum number of rows to load per sheet 196 | * `callback(err, workbook)` - the callback function to invoke when the operation has completed 197 | * `err` - the error, if any 198 | * `workbook` - the parsed workbook instance, will be an array if `path` was also an array 199 | * `file` - the file used to open the workbook 200 | * `meta` - the metadata for this workbook 201 | * `user` - the owner of the file 202 | * `sheets` - an array of strings containing the name of sheets (available without iteration) 203 | * `sheets` - the array of Sheet objects that were loaded 204 | * `index` - the ordinal position of the sheet within the workbook 205 | * `name` - the name of the sheet 206 | * `bounds` - the data range for the sheet 207 | * `rows` - the largest number of rows in the sheet 208 | * `columns` - the largest number of columns in the sheet 209 | * `visibility` - the sheet visibility, possible values are `visible`, `hidden` and `very hidden` 210 | * `rows` - the array of rows that were loaded - rows are arrays of cells 211 | * `row` - the ordinal row number 212 | * `column` - the ordinal column number 213 | * `address` - the cell address ("A1", "B12", etc.) 214 | * `value` - the cell value, which can be of the following types: 215 | * `Number` - for numeric values 216 | * `Date` - for cells formatted as dates 217 | * `Error` - for cells with errors (such as #NAME?) 218 | * `Boolean` - for cells formatted as booleans 219 | * `String` - for anything else 220 | * `cell(address)` - a function returning the cell at a specific location (ex: B12), same as accessing the `rows` array 221 | 222 | #### Event: 'open' 223 | 224 | Emitted when a workbook file is open. The data included with this event includes: 225 | 226 | * `file` - the file used to open the workbook 227 | * `meta` - the metadata for this workbook 228 | * `user` - the owner of the file 229 | * `sheets` - an array of strings containing the name of sheets (available without iteration) 230 | 231 | This event can be emitted more than once if multiple files are being read. 232 | 233 | #### Event: 'data' 234 | 235 | Emitted as rows are being read from the file. The data for this event consists of: 236 | 237 | * `sheet` - the currently open sheet 238 | * `index` - the ordinal position of the sheet within the workbook 239 | * `name` - the sheet name 240 | * `bounds` - the data range for the sheet 241 | * `rows` - the largest number of rows in the sheet 242 | * `columns` - the largest number of columns in the sheet 243 | * `visibility` - the sheet visibility, possible values are `visible`, `hidden` and `very hidden` 244 | * `rows` - the array of rows that were loaded (number of rows returned depend on the buffer size) 245 | * `row` - the ordinal row number 246 | * `column` - the ordinal column number 247 | * `address` - the cell address ("A1", "B12", etc.) 248 | * `value` - the cell value, which can be of the following types: 249 | * `Number` - for numeric values 250 | * `Date` - for cells formatted as dates 251 | * `Error` - for cells with errors, such as #NAME? 252 | * `Boolean` - for cells formatted as booleans 253 | * `String` - for anything else 254 | 255 | #### Event: 'close' 256 | 257 | Emitted when a workbook file is closed. This event can be emitted more than once if multiple files are being read. 258 | 259 | #### Event: 'error' 260 | 261 | Emitted when an error is encountered. 262 | 263 | ### SpreadsheetWriter 264 | 265 | The `SpreadsheetWriter` is used to write spreadsheet files into various formats. All writer methods return the same instance, so feel free to chain your calls. 266 | 267 | #### #ctor(path, options) 268 | 269 | Creates a new `SpreadsheetWriter` instance. 270 | 271 | * `path` - the path of the file to write 272 | * `options` - the writer options (optional) 273 | * `format` - the file format 274 | * `defaultDateFormat` - the default date format (only for XLSX files) 275 | * `properties` - the workbook properties 276 | * `title` 277 | * `subject` 278 | * `author` 279 | * `manager` 280 | * `company` 281 | * `category` 282 | * `keywords` 283 | * `comments` 284 | * `status` 285 | 286 | #### .addSheet(name, options) 287 | 288 | Adds a new sheet to the workbook. 289 | 290 | * `name` - the sheet name (must be unique within the workbook) 291 | * `options` - the sheet options 292 | * `hidden` 293 | * `activated` 294 | * `selected` 295 | * `rightToLeft` 296 | * `hideZeroValues` 297 | * `selection` 298 | 299 | #### .activateSheet(sheet) 300 | 301 | Activates a previously added sheet. 302 | 303 | * `sheet` - the sheet name or index 304 | 305 | #### .addFormat(name, properties) 306 | 307 | Registers a reusable format. 308 | 309 | * `name` - the format name 310 | * `properties` - the formatting properties 311 | * `font` 312 | * `name` 313 | * `size` 314 | * `color` 315 | * `bold` 316 | * `italic` 317 | * `underline` 318 | * `strikeout` 319 | * `superscript` 320 | * `subscript` 321 | * `numberFormat` 322 | * `locked` 323 | * `hidden` 324 | * `alignment` 325 | * `rotation` 326 | * `indent` 327 | * `shrinkToFit` 328 | * `justifyLastText` 329 | * `fill` 330 | * `pattern` 331 | * `backgroundColor` 332 | * `foregroundColor` 333 | * `borders` 334 | * `top` | `left` | `right` | `bottom` 335 | * `style` 336 | * `color` 337 | 338 | #### .write(row, column, data, format) 339 | 340 | Writes data to the specified cell with an optional format. 341 | 342 | * `row` - the row index 343 | * `column` - the column index 344 | * `data` - the value to write, supported types are: String, Number, Date, Boolean and Array 345 | * `format` - the format name or properties to use (optional) 346 | 347 | #### .write(cell, data, format) 348 | 349 | Same as previous, except cell is a string such as "A1", "B2" or "1,1" 350 | 351 | #### .append(data, format) 352 | 353 | Appends data at the first column of the next row. The next row is determined by the last call to `write`. 354 | 355 | * `data` - the value to write (use arrays to write multiple cells at once) 356 | * `format` - the format name or properties to use (optional) 357 | 358 | #### .save(callback) 359 | 360 | Save and close the resulting workbook file. 361 | 362 | * `callback(err)` - the callback function to invoke when the file is saved 363 | * `err` - the error, if any 364 | 365 | #### Event: open 366 | 367 | Emitted when the file is open. 368 | 369 | #### Event: close 370 | 371 | Emitted when the file is closed. 372 | 373 | #### Event: error 374 | 375 | Emitted when an error occurs. 376 | 377 | ## Compatibility 378 | 379 | + Tested with Node 0.10.x 380 | + Tested on Mac OS X 10.8 381 | + Tested on Ubuntu Linux 12.04 382 | + Tested on Heroku 383 | 384 | ## Dependencies 385 | 386 | + Python version 2.7+ 387 | + [xlrd](http://www.python-excel.org/) version 0.7.4+ 388 | + [xlwt](http://www.python-excel.org/) version 0.7.5+ 389 | + [XlsxWriter](http://xlsxwriter.readthedocs.org/en/latest/index.html) version 0.3.6+ 390 | + bash (installation script) 391 | + git (installation script) 392 | 393 | ## License 394 | 395 | The MIT License (MIT) 396 | 397 | Copyright (c) 2013 Nicolas Mercier 398 | 399 | Permission is hereby granted, free of charge, to any person obtaining a copy 400 | of this software and associated documentation files (the "Software"), to deal 401 | in the Software without restriction, including without limitation the rights 402 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 403 | copies of the Software, and to permit persons to whom the Software is 404 | furnished to do so, subject to the following conditions: 405 | 406 | The above copyright notice and this permission notice shall be included in 407 | all copies or substantial portions of the Software. 408 | 409 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 410 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 411 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 412 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 413 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 414 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 415 | THE SOFTWARE. 416 | -------------------------------------------------------------------------------- /examples/read-simple.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This example demonstrates how to read a simple file from memory 3 | */ 4 | 5 | var SpreadsheetReader = require('../lib').SpreadsheetReader; 6 | var util = require('util'); 7 | 8 | SpreadsheetReader.read('examples/sample.xlsx', function (err, workbook) { 9 | if (err) throw err; 10 | console.log(util.inspect(workbook, { depth: null, colors: true })); 11 | }); 12 | -------------------------------------------------------------------------------- /examples/read-stream.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This example demonstrates how to read a large file as a stream, without loading everything in memory. 3 | * The reader triggers events as data is being received. 4 | */ 5 | 6 | var SpreadsheetReader = require('../lib').SpreadsheetReader; 7 | 8 | new SpreadsheetReader('examples/sample.xlsx').on('open', function (workbook) { 9 | // file is open 10 | console.log('opened ' + workbook.file); 11 | }).on('data', function (data) { 12 | // data is being received 13 | console.log('buffer contains %d rows from sheet "%s"', data.rows.length, data.sheet.name); 14 | }).on('close', function () { 15 | // file is now closed 16 | console.log('file closed'); 17 | }).on('error', function (err) { 18 | throw err; 19 | }); 20 | -------------------------------------------------------------------------------- /examples/sample.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extrabacon/pyspreadsheet/e8f83eaff1e5cb7dfbd774fa1764dd28326a6a5f/examples/sample.xlsx -------------------------------------------------------------------------------- /examples/write-simple.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This example writes a simple spreadsheet 3 | */ 4 | 5 | var SpreadsheetWriter = require('../lib').SpreadsheetWriter; 6 | var writer = new SpreadsheetWriter('examples/output.xlsx'); 7 | 8 | writer.addFormat('heading', { font: { bold: true } }); 9 | writer.write(0, 0, ['ID', 'Name', 'Age', 'Birthdate', 'Balance', 'Active?'], 'heading'); 10 | 11 | writer.append([ 12 | ['1', 'John Doe', 31, new Date('2012-01-01T01:35:33Z'), 55.34, true], 13 | ['2', 'John Dow', 34, new Date('2013-02-03T01:35:33Z'), 12.0002, false], 14 | ['3', 'John Doh', 33, new Date('2014-03-06T01:35:33Z'), 78.901, true] 15 | ]); 16 | 17 | writer.write('D5', ['Total:', '=SUM(E2:E4)'], { font: { bold: true }, alignment: 'right' }); 18 | 19 | writer.save(function (err) { 20 | if (err) throw err; 21 | console.log('file saved'); 22 | }); 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var PythonShell = require('python-shell'); 3 | PythonShell.defaultOptions = { 4 | scriptPath: path.join(__dirname, 'python') 5 | }; 6 | 7 | exports.SpreadsheetReader = require('./lib/spreadsheet-reader'); 8 | exports.SpreadsheetWriter = require('./lib/spreadsheet-writer'); 9 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf .deps 4 | mkdir .deps 5 | cd .deps 6 | 7 | echo Downloading xlrd dependency from Github 8 | git clone https://github.com/python-excel/xlrd xlrd 9 | rm -rf ../python/xlrd 10 | cp -R xlrd/xlrd ../python/xlrd 11 | 12 | echo Downloading xlwt dependency from Github 13 | git clone https://github.com/python-excel/xlwt xlwt 14 | rm -rf ../python/xlwt 15 | cp -R xlwt/xlwt ../python/xlwt 16 | 17 | echo Downloading XlsxWriter dependency from Github 18 | git clone https://github.com/jmcnamara/XlsxWriter xlsxwriter 19 | rm -rf ../python/xlsxwriter 20 | cp -R xlsxwriter/xlsxwriter ../python/xlsxwriter 21 | 22 | cd .. 23 | rm -rf .deps 24 | -------------------------------------------------------------------------------- /lib/spreadsheet-reader.js: -------------------------------------------------------------------------------- 1 | var _ = require('./utilities'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var util = require('util'); 4 | var PythonShell = require('python-shell'); 5 | 6 | /** 7 | * The spreadsheet reader API 8 | * @param path The path of the file to read 9 | * @param [options] The reading options 10 | * @this SpreadsheetReader 11 | * @constructor 12 | */ 13 | var SpreadsheetReader = function (path, options) { 14 | var self = this; 15 | var workbook; 16 | var sheet; 17 | var currentRow; 18 | var rowIndex = -1; 19 | var rowBuffer = []; 20 | options = _.extend({}, SpreadsheetReader.defaultOptions, options); 21 | 22 | EventEmitter.call(this); 23 | 24 | function flushRows() { 25 | // if no accumulated rows, skip 26 | if (!rowBuffer.length) return; 27 | 28 | // emit an event with the accumulated rows 29 | self.emit('data', { 30 | workbook: workbook, 31 | sheet: sheet, 32 | rows: rowBuffer 33 | }); 34 | 35 | // reset buffer for the next iteration 36 | rowBuffer = []; 37 | } 38 | 39 | this.pyshell = new PythonShell('excel_reader.py', { 40 | args: formatArgs(path, options), 41 | mode: 'json' 42 | }); 43 | 44 | this.pyshell.on('message', function (message) { 45 | var value = message[1]; 46 | switch (message[0]) { 47 | case 'w': 48 | workbook = { 49 | file: value.file, 50 | meta: { 51 | user: value.user, 52 | sheets: value.sheets 53 | } 54 | }; 55 | self.emit('open', workbook); 56 | break; 57 | case 's': 58 | // if we are changing sheets, flush rows immediately 59 | rowIndex = -1; 60 | sheet && flushRows(); 61 | sheet = { 62 | index: value.index, 63 | name: value.name, 64 | bounds: { 65 | rows: value.rows, 66 | columns: value.columns 67 | }, 68 | visibility: (function () { 69 | switch (value.visibility) { 70 | case 1: return 'hidden'; 71 | case 2: return 'very hidden'; 72 | default: return 'visible'; 73 | } 74 | })() 75 | }; 76 | break; 77 | case 'c': 78 | // switching rows 79 | if (value[0] !== rowIndex) { 80 | // flush accumulated rows if the buffer limit has been reached 81 | rowBuffer.length >= options.bufferSize && flushRows(); 82 | // append a new row to the buffer 83 | currentRow = []; 84 | rowIndex = value[0]; 85 | rowBuffer.push(currentRow); 86 | } 87 | // append cell to current row 88 | currentRow.push({ 89 | row: value[0], 90 | column: value[1], 91 | address: value[2], 92 | value: parseValue(value[3]) 93 | }); 94 | break; 95 | case 'error': 96 | self.emit('error', _.extend(self.pyshell.parseError(value.traceback), value)); 97 | break; 98 | } 99 | }).on('error', function (err) { 100 | self.emit('error', err); 101 | }).on('close', function () { 102 | // flush remaining rows, then close 103 | flushRows(); 104 | self.emit('close'); 105 | }); 106 | }; 107 | util.inherits(SpreadsheetReader, EventEmitter); 108 | 109 | SpreadsheetReader.defaultOptions = { 110 | bufferSize: 20 111 | }; 112 | 113 | function formatArgs(path, options) { 114 | var args = []; 115 | 116 | if (options.meta) { 117 | args.push('-m'); 118 | } 119 | if (options.hasOwnProperty('sheets') && Array.isArray(options.sheets)) { 120 | args.push(options.sheets.map(function (s) { 121 | return ['-s', s]; 122 | })); 123 | } else if (options.hasOwnProperty('sheet')) { 124 | args.push(['-s', options.sheet]); 125 | } 126 | if (options.hasOwnProperty('maxRows')) { 127 | args.push(['-r', options.maxRows]); 128 | } 129 | 130 | args.push(path); 131 | return _.flatten(args); 132 | } 133 | 134 | function parseValue(value) { 135 | if (Array.isArray(value)) { 136 | // parse non-native data types 137 | if (value[0] === 'date') { 138 | return new Date( 139 | value[1], value[2] - 1, value[3], 140 | value[4], value[5], value[6] 141 | ); 142 | } else if (value[0] === 'error') { 143 | return _.extend(new Error(value[1]), { id: 'cell_error', errorCode: value[1] }); 144 | } else if (value[0] === 'empty') { 145 | return null; 146 | } 147 | } 148 | return value; 149 | } 150 | 151 | /** 152 | * Reads an entire spreadsheet file into a workbook object 153 | * @param path The path of the spreadsheet file to read 154 | * @param {Object} [options] The reading options 155 | * @param {Function} callback The callback function to invoke with the resulting workbook object 156 | * @returns {SpreadsheetReader} The SpreadsheetReader instance to use to read the file 157 | */ 158 | SpreadsheetReader.read = function (path, options, callback) { 159 | if (typeof options === 'function') { 160 | callback = options; 161 | options = null; 162 | } 163 | 164 | var reader = new SpreadsheetReader(path, options); 165 | var currentWorkbook; 166 | var workbooks = []; 167 | var errors; 168 | 169 | function getCell(address) { 170 | var pos = _.cellToRowCol(address); 171 | return this.rows[pos[0]][pos[1]]; 172 | } 173 | 174 | reader.on('open', function (workbook) { 175 | currentWorkbook = workbook; 176 | workbooks.push(workbook); 177 | }).on('data', function (data) { 178 | currentWorkbook.sheets = currentWorkbook.sheets || []; 179 | 180 | var sheet = _.find(currentWorkbook.sheets, function (s) { 181 | return s.index === data.sheet.index; 182 | }); 183 | 184 | if (!sheet) { 185 | sheet = _.extend({ rows: [] }, data.sheet); 186 | sheet.cell = getCell.bind(sheet); 187 | currentWorkbook.sheets.push(sheet); 188 | currentWorkbook.sheets[sheet.name] = sheet; 189 | } 190 | 191 | data.rows.forEach(function (row) { 192 | sheet.rows.push(row); 193 | }); 194 | 195 | }).on('error', function (err) { 196 | if (!errors) { 197 | errors = err; 198 | } else { 199 | errors = [errors]; 200 | errors.push(err); 201 | } 202 | }).on('close', function () { 203 | callback = callback || function () {}; 204 | 205 | if (!errors && !workbooks.length) { 206 | errors = _.extend(new Error('file not found: ' + path), { id: 'file_not_found' }); 207 | } 208 | 209 | if (!workbooks.length) { 210 | return callback(errors, null); 211 | } else if (workbooks.length === 1) { 212 | return callback(errors, workbooks[0]); 213 | } 214 | return callback(errors, workbooks); 215 | }); 216 | 217 | return reader; 218 | }; 219 | 220 | module.exports = SpreadsheetReader; 221 | -------------------------------------------------------------------------------- /lib/spreadsheet-writer.js: -------------------------------------------------------------------------------- 1 | var _ = require('./utilities'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var os = require('os'); 4 | var path = require('path'); 5 | var util = require('util'); 6 | var PythonShell = require('python-shell'); 7 | 8 | /** 9 | * The spreadsheet writer API 10 | * @param {String} [filePath] The path of the file to write 11 | * @param {Object} [options] The workbook options 12 | * @this SpreadsheetWriter 13 | * @constructor 14 | */ 15 | var SpreadsheetWriter = function (filePath, options) { 16 | var self = this; 17 | if (filePath && filePath.constructor === Object) { 18 | options = filePath; 19 | filePath = null; 20 | } 21 | 22 | EventEmitter.call(this); 23 | 24 | this.state = { 25 | status: 'new', 26 | sheets: [], 27 | anonymousFormatCount: 0, 28 | currentSheetIndex: -1, 29 | lastError: null 30 | }; 31 | this.filePath = filePath || path.join(os.tmpdir(), _.randomString(8)); 32 | 33 | var format = 'xlsx'; 34 | if (options && options.format) { 35 | format = options.format.toLowerCase(); 36 | } else if (filePath) { 37 | format = path.extname(filePath); 38 | if (format) { 39 | format = format.substr(1).toLowerCase(); 40 | } 41 | } 42 | 43 | var pythonShellOptions = { 44 | args : ['-o', this.filePath, '-m'], 45 | mode: 'json' 46 | }; 47 | 48 | switch (format) { 49 | case 'xls': 50 | // use the xlwt module for legacy XLS files 51 | pythonShellOptions.args.push('xlwt'); 52 | break; 53 | case 'xlsx': 54 | // use the xlsxwriter module for OpenOffice files 55 | pythonShellOptions.args.push('xlsxwriter'); 56 | break; 57 | default: 58 | throw new Error('unsupported format: ' + format); 59 | } 60 | 61 | // the Python shell process we use to render Excel files 62 | this.pyshell = new PythonShell('excel_writer.py', pythonShellOptions); 63 | 64 | this.pyshell.on('message', function (message) { 65 | var type = message[0]; 66 | var value = message[1]; 67 | switch (type) { 68 | case 'open': 69 | self.state = 'open'; 70 | self.filePath = value; 71 | self.emit('open', value); 72 | break; 73 | case 'close': 74 | self.state = 'closed'; 75 | self.emit('close', value); 76 | break; 77 | case 'error': 78 | var error = _.extend(self.pyshell.parseError(value.traceback), value); 79 | self.emit('error', error); 80 | self.state.lastError = error; 81 | break; 82 | } 83 | }).on('error', function (err) { 84 | self.emit('error', err); 85 | self.state.lastError = err; 86 | }); 87 | 88 | this.pyshell.send(['create_workbook', options]); 89 | }; 90 | util.inherits(SpreadsheetWriter, EventEmitter); 91 | 92 | /** 93 | * Adds a sheet to the workbook 94 | * @param {String} [name] The name of the sheet to add 95 | * @param {Object} [options] The sheet options 96 | * @returns {SpreadsheetWriter} The same writer instance for chaining calls 97 | */ 98 | SpreadsheetWriter.prototype.addSheet = function (name, options) { 99 | this.state.sheets.push({ 100 | name: name, 101 | index: this.state.sheets.length, 102 | currentRow: -1 103 | }); 104 | this.state.currentSheetIndex = this.state.sheets.length - 1; 105 | this.pyshell.send(['add_sheet', name]); 106 | options && this.pyshell.send(['set_sheet_settings', null, options]); 107 | return this; 108 | }; 109 | 110 | /** 111 | * Activates a previously added sheet 112 | * @param {Number|String} sheet The name or the index of the sheet to activate 113 | * @returns {SpreadsheetWriter} The same writer instance for chaining calls 114 | */ 115 | SpreadsheetWriter.prototype.activateSheet = function (sheet) { 116 | var sheetToActivate = _.find(this.state.sheets, function (s) { 117 | if (typeof sheet === 'number') { 118 | return s.index === sheet; 119 | } else { 120 | return s.name === sheet; 121 | } 122 | }); 123 | 124 | if (sheetToActivate) { 125 | this.state.currentSheetIndex = sheetToActivate.index; 126 | this.pyshell.send(['activate_sheet', sheet]); 127 | } else { 128 | throw new Error('sheet not found: ' + sheet); 129 | } 130 | 131 | return this; 132 | }; 133 | 134 | /** 135 | * Registers a reusable format which can be used with "write" 136 | * @param {String} id The format ID to use as the reference 137 | * @param {Object} properties The formatting properties 138 | * @returns {SpreadsheetWriter} The same writer instance for chaining calls 139 | */ 140 | SpreadsheetWriter.prototype.addFormat = function (id, properties) { 141 | this.pyshell.send(['format', id, properties]); 142 | return this; 143 | }; 144 | 145 | /** 146 | * Writes data to the current sheet 147 | * @param {Number|String} row The row index to write to - or a cell address such as "A1" 148 | * @param {Number} [col] The column index to write to - ignore if using a cell address 149 | * @param {*} data The data to write - use an array to write a row, or a 2-D array for multiple rows 150 | * @param {String|Object} [format] The format to apply to written cells - can be a format ID or a format object 151 | * @returns {SpreadsheetWriter} The same writer instance for chaining calls 152 | */ 153 | SpreadsheetWriter.prototype.write = function (row, col, data, format) { 154 | 155 | // look for a cell notation instead of row index (A1) 156 | if (typeof row === 'string') { 157 | var address = _.cellToRowCol(row); 158 | return this.write(address[0], address[1], col, data); 159 | } 160 | 161 | if (data === null || typeof data === 'undefined') return this; // nothing to write 162 | 163 | if (this.state.currentSheetIndex == -1) { 164 | // writing without a sheet - create a sheet now 165 | this.addSheet(); 166 | } 167 | 168 | var sheet = this.state.sheets[this.state.currentSheetIndex]; 169 | 170 | // if the row is not specified, use the next row from the last write 171 | if (row === -1) { 172 | row = sheet.currentRow + 1; 173 | } 174 | if (!col) { 175 | col = 0; 176 | } 177 | 178 | // keep track of where data is being written 179 | if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0])) { 180 | // writing a jagged array, count rows in array 181 | sheet.currentRow = row + data.length - 1; 182 | } else { 183 | sheet.currentRow = row; 184 | } 185 | 186 | // look for an anonymous format 187 | if (format && format.constructor === Object) { 188 | var formatName = 'untitled_format_' + (++this.state.anonymousFormatCount); 189 | this.addFormat(formatName, format); 190 | format = formatName; 191 | } 192 | 193 | // convert values into transport-friendly JSON in order to avoid loss of fidelity 194 | function translate(v) { 195 | if (Array.isArray(v)) { 196 | return v.map(translate); 197 | } else if (v instanceof Date) { 198 | return { $date: v.getTime() }; 199 | } else if (v === true) { 200 | return '=TRUE'; 201 | } else if (v === false) { 202 | return '=FALSE'; 203 | } 204 | return v; 205 | } 206 | data = translate(data); 207 | 208 | this.pyshell.send(['write', row, col, data, format]); 209 | return this; 210 | }; 211 | 212 | /** 213 | * Appends data to the current sheet 214 | * @param {Array} data The data to append - a 2-D array will append multiple rows at once 215 | * @param {String|Object} [format] The format to apply, using its name or an anonymous format object 216 | * @returns {SpreadsheetWriter} The same writer instance for chaining calls 217 | */ 218 | SpreadsheetWriter.prototype.append = function (data, format) { 219 | return this.write(-1, 0, data, format); 220 | }; 221 | 222 | /** 223 | * Saves this workbook, committing all changes 224 | * @param {Function} callback The callback function 225 | * @returns {SpreadsheetWriter} The same writer instance for chaining calls 226 | */ 227 | SpreadsheetWriter.prototype.save = function (callback) { 228 | 229 | var self = this; 230 | callback = callback || function () {}; 231 | 232 | this.pyshell.on('close', function () { 233 | if (self.state.lastError) { 234 | return callback(self.state.lastError); 235 | } 236 | return callback(); 237 | }); 238 | 239 | process.nextTick(function () { 240 | self.pyshell.end(); 241 | }); 242 | 243 | return this; 244 | }; 245 | 246 | module.exports = SpreadsheetWriter; 247 | -------------------------------------------------------------------------------- /lib/utilities.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | 3 | var letters = ['', 4 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 5 | 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' 6 | ]; 7 | 8 | exports.extend = function (obj) { 9 | Array.prototype.slice.call(arguments, 1).forEach(function (source) { 10 | if (source) { 11 | for (var key in source) { 12 | obj[key] = source[key]; 13 | } 14 | } 15 | }); 16 | return obj; 17 | }; 18 | 19 | exports.flatten = function (array) { 20 | var results = []; 21 | var self = arguments.callee; 22 | array.forEach(function(item) { 23 | Array.prototype.push.apply(results, Array.isArray(item) ? self(item) : [item]); 24 | }); 25 | return results; 26 | }; 27 | 28 | exports.find = function (array, test) { 29 | for (var i = 0, len = array.length; i < len; i++) { 30 | if (test(array[i])) { 31 | return array[i]; 32 | } 33 | } 34 | return null; 35 | }; 36 | 37 | exports.randomString = function(length) { 38 | var chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 39 | var randomBytes = crypto.pseudoRandomBytes(length); 40 | var i = 0; 41 | var result = ''; 42 | var remainder; 43 | var value; 44 | 45 | while (i < randomBytes.length) { 46 | value = randomBytes[i]; 47 | i++; 48 | while (true) { 49 | remainder = value % chars.length; 50 | result += chars[remainder]; 51 | value = (value - remainder) / chars.length; 52 | if (value === 0) break; 53 | } 54 | } 55 | 56 | return result.substr(1, length); 57 | }; 58 | 59 | exports.cellToRowCol = function (cell) { 60 | var match; 61 | if (!cell) { 62 | return [0, 0]; 63 | } else if (~cell.indexOf(',')) { 64 | // 'row,col' notation 65 | match = /(\d+),(\d+)/.exec(cell); 66 | if (match) { 67 | return [parseInt(match[1], 10), parseInt(match[2], 10)]; 68 | } else { 69 | throw new Error('invalid cell reference: ' + cell); 70 | } 71 | } else { 72 | // 'A1' notation 73 | match = /(\$?)([A-Z]{1,3})(\$?)(\d+)/.exec(cell); 74 | if (match) { 75 | return [parseInt(match[4], 10) - 1, this.colToInt(match[2]) - 1]; 76 | } else { 77 | throw new Error('invalid cell reference: ' + cell); 78 | } 79 | } 80 | }; 81 | 82 | /** 83 | * Converts a column letter into a column number (A -> 1, B -> 2, etc.) 84 | * @param col The column letter 85 | * @returns {number} The column number 86 | */ 87 | exports.colToInt = function (col) { 88 | var n = 0; 89 | col = col.trim().split(''); 90 | for (var i = 0; i < col.length; i++) { 91 | n *= 26; 92 | n += letters.indexOf(col[i]); 93 | } 94 | return n; 95 | }; 96 | 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pyspreadsheet", 3 | "version": "0.1.5", 4 | "description": "Python-based high performance spreadsheet API for Node", 5 | "keywords": [ 6 | "spreadsheet", 7 | "excel", 8 | "xls", 9 | "xlsx", 10 | "xlrd", 11 | "xlwt", 12 | "xlsxwriter" 13 | ], 14 | "scripts": { 15 | "test": "mocha -r should -R spec", 16 | "install": "bash install.sh" 17 | }, 18 | "dependencies": { 19 | "python-shell": "^0.4.0" 20 | }, 21 | "devDependencies": { 22 | "async": "^0.2.10", 23 | "should": "^3.1.3", 24 | "mocha": "^1.17.1" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "http://github.com/extrabacon/pyspreadsheet" 29 | }, 30 | "homepage": "http://github.com/extrabacon/pyspreadsheet", 31 | "bugs": "http://github.com/extrabacon/pyspreadsheet/issues", 32 | "author": { 33 | "name": "Nicolas Mercier", 34 | "email": "nicolas@extrabacon.net" 35 | }, 36 | "engines": { 37 | "node": ">=0.10" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /python/excel_reader.py: -------------------------------------------------------------------------------- 1 | import sys, json, traceback, datetime, glob, xlrd 2 | from xlrd import open_workbook, cellname, xldate_as_tuple, error_text_from_code 3 | 4 | def dump_record(record_type, values): 5 | print(json.dumps([record_type, values])); 6 | 7 | def parse_cell_value(sheet, cell): 8 | if cell.ctype == xlrd.XL_CELL_DATE: 9 | year, month, day, hour, minute, second = xldate_as_tuple(cell.value, sheet.book.datemode) 10 | return ['date', year, month, day, hour, minute, second] 11 | elif cell.ctype == xlrd.XL_CELL_ERROR: 12 | return ['error', error_text_from_code[cell.value]] 13 | elif cell.ctype == xlrd.XL_CELL_BOOLEAN: 14 | return False if cell.value == 0 else True 15 | elif cell.ctype == xlrd.XL_CELL_EMPTY: 16 | return None 17 | return cell.value 18 | 19 | def dump_sheet(sheet, sheet_index, max_rows): 20 | dump_record("s", { 21 | "index": sheet_index, 22 | "name": sheet.name, 23 | "rows": sheet.nrows, 24 | "columns": sheet.ncols, 25 | "visibility": sheet.visibility 26 | }) 27 | for rowx in range(max_rows or sheet.nrows): 28 | for colx in range(sheet.ncols): 29 | cell = sheet.cell(rowx, colx) 30 | dump_record("c", [rowx, colx, cellname(rowx, colx), parse_cell_value(sheet, cell)]) 31 | 32 | def main(cmd_args): 33 | import optparse 34 | usage = "\n%prog [options] [file1] [file2] ..." 35 | oparser = optparse.OptionParser(usage) 36 | oparser.add_option( 37 | "-m", "--meta", 38 | dest = "iterate_sheets", 39 | action = "store_false", 40 | default = True, 41 | help = "dumps only the workbook record, does not load any worksheet") 42 | oparser.add_option( 43 | "-s", "--sheet", 44 | dest = "sheets", 45 | action = "append", 46 | help = "names of the sheets to load - if omitted, all sheets are loaded") 47 | oparser.add_option( 48 | "-r", "--rows", 49 | dest = "max_rows", 50 | default = None, 51 | action = "store", 52 | type = "int", 53 | help = "maximum number of rows to load") 54 | options, args = oparser.parse_args(cmd_args) 55 | 56 | # loop on all input files 57 | for file in args: 58 | try: 59 | wb = open_workbook(filename=file, on_demand=True) 60 | sheet_names = wb.sheet_names() 61 | 62 | dump_record("w", { 63 | "file": file, 64 | "sheets": sheet_names, 65 | "user": wb.user_name 66 | }) 67 | 68 | if options.iterate_sheets: 69 | if options.sheets: 70 | for sheet_to_load in options.sheets: 71 | try: 72 | sheet_name = sheet_to_load 73 | if sheet_to_load.isdigit(): 74 | sheet = wb.sheet_by_index(int(sheet_to_load)) 75 | sheet_name = sheet.name 76 | else: 77 | sheet = wb.sheet_by_name(sheet_to_load) 78 | dump_sheet(sheet, sheet_names.index(sheet_name), options.max_rows) 79 | wb.unload_sheet(sheet_name) 80 | except: 81 | dump_record("error", { 82 | "id": "load_sheet_failed", 83 | "file": file, 84 | "sheet": sheet_name, 85 | "traceback": traceback.format_exc() 86 | }) 87 | else: 88 | for sheet_index in range(len(sheet_names)): 89 | try: 90 | sheet = wb.sheet_by_index(sheet_index) 91 | dump_sheet(sheet, sheet_index, options.max_rows) 92 | wb.unload_sheet(sheet_index) 93 | except: 94 | dump_record("error", { 95 | "id": "load_sheet_failed", 96 | "file": file, 97 | "sheet": sheet_index, 98 | "traceback": traceback.format_exc() 99 | }) 100 | except: 101 | dump_record("error", { 102 | "id": "open_workbook_failed", 103 | "file": file, 104 | "traceback": traceback.format_exc() 105 | }) 106 | 107 | sys.exit() 108 | 109 | main(sys.argv[1:]) 110 | -------------------------------------------------------------------------------- /python/excel_writer.py: -------------------------------------------------------------------------------- 1 | import sys, datetime, json, uuid, traceback 2 | import excel_writer_xlsxwriter, excel_writer_xlwt 3 | 4 | def dump_record(record_type, values = None): 5 | if values != None: 6 | print(json.dumps([record_type, values], default = json_extended_handler)) 7 | else: 8 | print(json.dumps([record_type], default = json_extended_handler)) 9 | 10 | def json_extended_handler(obj): 11 | if hasattr(obj, 'isoformat'): 12 | return obj.isoformat() 13 | return obj 14 | 15 | # extended JSON parser for handling dates 16 | def json_extended_parser(dct): 17 | for k, v in dct.items(): 18 | if k == "$date": 19 | return datetime.datetime.fromtimestamp(v / 1000) 20 | return dct 21 | 22 | class SpreadsheetWriter: 23 | def __init__(self, filename, module): 24 | self.filename = filename 25 | self.workbook = None 26 | self.current_sheet = None 27 | self.current_cell = None 28 | self.formats = dict() 29 | # inject module functions into writer instance 30 | self.__dict__.update(module.__dict__) 31 | self.dump_record = dump_record 32 | 33 | def main(cmd_args): 34 | import optparse 35 | usage = "\n%prog -m [module] -o [output]" 36 | oparser = optparse.OptionParser(usage) 37 | oparser.add_option( 38 | "-m", "--module", 39 | dest = "module_name", 40 | action = "store", 41 | default = "xlsxwriter", 42 | help = "the name of the writer module to load") 43 | oparser.add_option( 44 | "-o", "--output", 45 | dest = "output_path", 46 | action = "store", 47 | help = "the path of the output file to write") 48 | options, args = oparser.parse_args(cmd_args) 49 | 50 | # load the specified module and use it to create a writer 51 | module = sys.modules["excel_writer_" + options.module_name] 52 | writer = SpreadsheetWriter(options.output_path, module) 53 | 54 | # read JSON commands from stdin and forward them to the writer 55 | for line in sys.stdin: 56 | try: 57 | command = json.loads(line, object_hook = json_extended_parser) 58 | method_name = command[0] 59 | args = command[1:] 60 | getattr(writer, method_name)(writer, *args) 61 | except: 62 | dump_record("error", { 63 | "method_name": method_name, 64 | "args": args, 65 | "traceback": traceback.format_exc() 66 | }) 67 | 68 | writer.close(writer) 69 | sys.exit() 70 | 71 | main(sys.argv[1:]) 72 | -------------------------------------------------------------------------------- /python/excel_writer_xlsxwriter.py: -------------------------------------------------------------------------------- 1 | import sys, json, datetime, xlsxwriter 2 | from xlsxwriter.workbook import Workbook 3 | 4 | def create_workbook(self, options = None): 5 | 6 | if options and "defaultDateFormat" in options: 7 | default_date_format = options["defaultDateFormat"] 8 | else: 9 | default_date_format = "yyyy-mm-dd" 10 | 11 | self.workbook = Workbook(self.filename, { "default_date_format": default_date_format, "constant_memory": True }) 12 | self.dump_record("open", self.filename) 13 | 14 | if options and "properties" in options: 15 | self.workbook.set_properties(options["properties"]) 16 | 17 | def add_sheet(self, name = None): 18 | self.current_sheet = self.workbook.add_worksheet(name) 19 | 20 | def write(self, row, col, data, format_name = None): 21 | format = self.formats[format_name] if format_name != None else None 22 | 23 | if isinstance(data, list): 24 | row_index = row 25 | col_index = col 26 | for v1 in data: 27 | if isinstance(v1, list): 28 | col_index = col 29 | for v2 in v1: 30 | self.current_sheet.write(row_index, col_index, v2, format) 31 | col_index += 1 32 | row_index += 1 33 | else: 34 | self.current_sheet.write(row_index, col_index, v1, format) 35 | col_index += 1 36 | else: 37 | self.current_sheet.write(row, col, data, format) 38 | 39 | def format(self, name, properties): 40 | 41 | format = self.workbook.add_format() 42 | 43 | if "font" in properties: 44 | font = properties["font"] 45 | if "name" in font: 46 | format.set_font_name(font["name"]) 47 | if "size" in font: 48 | format.set_font_size(int(font["size"])) 49 | if "color" in font: 50 | format.set_font_color(font["color"]) 51 | if font.get("bold", False): 52 | format.set_bold() 53 | if font.get("italic", False): 54 | format.set_italic() 55 | if "underline" in font: 56 | if font["underline"] == True or font["underline"] == "single": 57 | format.set_underline = 1 58 | elif font["underline"] == "double": 59 | format.set_underline = 2 60 | elif font["underline"] == "single accounting": 61 | format.set_underline = 33 62 | elif font["underline"] == "double accounting": 63 | format.set_underline = 34 64 | if font.get("strikeout", False): 65 | format.set_strikeout() 66 | if font.get("superscript", False): 67 | format.set_font_script(1) 68 | elif font.get("subscript", False): 69 | format.set_font_script(2) 70 | 71 | if "numberFormat" in properties: 72 | format.set_num_format(properties["numberFormat"]) 73 | if "locked" in properties: 74 | format.set_locked(properties["locked"]) 75 | if properties.get("hidden", False): 76 | format.set_hidden(); 77 | if "alignment" in properties: 78 | aligns = properties["alignment"].split() 79 | for alignment in aligns: 80 | alignment = "vcenter" if alignment == "middle" else alignment 81 | format.set_align(alignment) 82 | if properties.get("textWrap", False): 83 | format.set_text_wrap() 84 | if "rotation" in properties: 85 | format.set_rotation(int(properties["rotation"])) 86 | if "indent" in properties: 87 | format.set_indent(int(properties["indent"])) 88 | if properties.get("shrinkToFit", False): 89 | format.set_shrink() 90 | if properties.get("justifyLastText", False): 91 | format.set_text_justlast() 92 | 93 | if "fill" in properties: 94 | fill = properties["fill"] 95 | if isinstance(fill, basestring): 96 | format.set_pattern(1) 97 | format.set_bg_color(fill) 98 | else: 99 | if "pattern" in fill: 100 | format.set_pattern(int(fill["pattern"])) 101 | else: 102 | format.set_pattern(1) 103 | if "color" in fill: 104 | fill["backgroundColor"] = fill["color"] 105 | if "backgroundColor" in fill: 106 | format.set_bg_color(fill["backgroundColor"]) 107 | if "foregroundColor" in fill: 108 | format.set_fg_color(fill["foregroundColor"]) 109 | 110 | if "borders" in properties: 111 | borders = properties["borders"] 112 | if isinstance(borders, int): 113 | format.set_border(borders) 114 | else: 115 | if "style" in borders: 116 | format.set_border(borders["style"]) 117 | if "color" in borders: 118 | format.set_border_color(borders["color"]) 119 | for border_pos in ["top", "left", "right", "bottom"]: 120 | if border_pos in borders: 121 | value = borders[border_pos] 122 | set_style = getattr(format, "set_" + border_pos) 123 | if isinstance(value, int): 124 | set_style(value) 125 | else: 126 | if "style" in value: 127 | set_style(value["style"]) 128 | if "color" in value: 129 | set_color = getattr(format, "set_" + border_pos + "_color") 130 | set_color(value["color"]) 131 | 132 | self.formats[name] = format 133 | 134 | def activate_sheet(self, id): 135 | for index, sheet in enumerate(self.workbook.worksheets()): 136 | if index == id or sheet.get_name() == id: 137 | self.current_sheet = sheet 138 | break 139 | 140 | def set_sheet_settings(self, id, settings = None): 141 | 142 | for index, sheet in enumerate(self.workbook.worksheets()): 143 | if index == id or sheet.get_name() == id: 144 | self.current_sheet = sheet 145 | break 146 | 147 | if settings != None: 148 | if settings.get("hidden", False): 149 | self.current_sheet.hide() 150 | if settings.get("activated", False): 151 | self.current_sheet.activate() 152 | if settings.get("selected", False): 153 | self.current_sheet.select() 154 | if settings.get("rightToLeft", False): 155 | self.current_sheet.right_to_left() 156 | if settings.get("hideZeroValues", False): 157 | self.current_sheet.hide_zero() 158 | if "selection" in settings: 159 | selection = settings["selection"] 160 | if isinstance(selection, basestring): 161 | self.current_sheet.set_selection(selection) 162 | else: 163 | self.current_sheet.set_selection(int(selection["top"]), int(selection["left"]), int(selection["bottom"]), int(selection["right"])) 164 | 165 | def set_row(self, index, settings): 166 | self.current_sheet.set_row(index, settings.get("height"), settings.get("format"), settings.get("options")) 167 | 168 | def set_column(self, index, settings): 169 | self.current_sheet.set_column(index, index, settings.get("width"), settings.get("format"), settings.get("options")) 170 | 171 | def close(self): 172 | self.workbook.close() 173 | self.dump_record("close") 174 | -------------------------------------------------------------------------------- /python/excel_writer_xlwt.py: -------------------------------------------------------------------------------- 1 | import sys, json, datetime, xlwt 2 | from xlwt import * 3 | 4 | def create_workbook(self, options = None): 5 | self.workbook = Workbook() 6 | self.sheet_count = 0 7 | self.dump_record("open", self.filename) 8 | 9 | if options and "properties" in options: 10 | # TODO: add support for more properties, xlwt has a ton of them 11 | prop = options["properties"] 12 | if "owner" in prop: 13 | self.workbook.owner = prop["owner"] 14 | 15 | def add_sheet(self, name = None): 16 | if name == None: 17 | name = "Sheet" + str(self.sheet_count + 1) 18 | self.current_sheet = self.workbook.add_sheet(name) 19 | self.sheet_count += 1 20 | 21 | def write(self, row, col, data, format_name = None): 22 | style = self.formats[format_name] if format_name != None else None 23 | 24 | def write_one(row, col, val): 25 | if style != None: 26 | self.current_sheet.write(row, col, val, style) 27 | else: 28 | self.current_sheet.write(row, col, val) 29 | 30 | if isinstance(data, list): 31 | row_index = row 32 | col_index = col 33 | for v1 in data: 34 | if isinstance(v1, list): 35 | col_index = col 36 | for v2 in v1: 37 | write_one(row_index, col_index, v2) 38 | col_index += 1 39 | row_index += 1 40 | else: 41 | write_one(row_index, col_index, v1) 42 | col_index += 1 43 | else: 44 | write_one(row, col, data) 45 | 46 | def format(self, name, properties): 47 | 48 | style = XFStyle() 49 | 50 | if "font" in properties: 51 | style.font = Font() 52 | font = properties["font"] 53 | if "name" in font: 54 | style.font.name = font["name"] 55 | if "size" in font: 56 | style.font.size = font["size"] 57 | if "color" in font: 58 | # TODO: need to convert color codes 59 | style.font.colour_index = font["color"] 60 | if font.get("bold", False): 61 | style.font.bold = True 62 | if font.get("italic", False): 63 | style.font.italic = True 64 | if "underline" in font: 65 | if font["underline"] == True or font["underline"] == "single": 66 | style.font.underline = Font.UNDERLINE_SINGLE 67 | elif font["underline"] == "double": 68 | style.font.underline = Font.UNDERLINE_DOUBLE 69 | elif font["underline"] == "single accounting": 70 | style.font.underline = Font.UNDERLINE_SINGLE_ACC 71 | elif font["underline"] == "double accounting": 72 | style.font.underline = Font.UNDERLINE_DOUBLE_ACC 73 | if font.get("strikeout", False): 74 | style.font.struck_out = True 75 | if font.get("superscript", False): 76 | style.font.escapement = Font.ESCAPEMENT_SUPERSCRIPT 77 | elif font.get("subscript", False): 78 | style.font.escapement = Font.ESCAPEMENT_SUBSCRIPT 79 | 80 | if "numberFormat" in properties: 81 | style.num_format_str = properties["numberFormat"] 82 | 83 | # TODO: locked 84 | # TODO: hidden 85 | # TODO: alignment 86 | # TODO: textWrap 87 | # TODO: rotation 88 | # TODO: indent 89 | # TODO: shrinkToFit 90 | # TODO: justifyLastText 91 | # TODO: fill 92 | # TODO: borders 93 | 94 | self.formats[name] = style 95 | 96 | def activate_sheet(self, id): 97 | # TODO: implement 98 | raise Exception("not implemented") 99 | 100 | def set_sheet_settings(self, id, settings = None): 101 | # TODO: implement 102 | raise Exception("not implemented") 103 | 104 | def set_row(self, index, settings): 105 | # TODO: implement 106 | raise Exception("not implemented") 107 | 108 | def set_column(self, index, settings): 109 | # TODO: implement 110 | raise Exception("not implemented") 111 | 112 | def close(self): 113 | self.workbook.save(self.filename) 114 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": ["describe", "it", "before", "after", "beforeEach", "afterEach"], 3 | "browser": false, 4 | "node": true, 5 | "curly": false, 6 | "strict": false, 7 | "expr": true, 8 | "unused": "vars" 9 | } 10 | -------------------------------------------------------------------------------- /test/input/sample.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extrabacon/pyspreadsheet/e8f83eaff1e5cb7dfbd774fa1764dd28326a6a5f/test/input/sample.xls -------------------------------------------------------------------------------- /test/input/sample.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extrabacon/pyspreadsheet/e8f83eaff1e5cb7dfbd774fa1764dd28326a6a5f/test/input/sample.xlsx -------------------------------------------------------------------------------- /test/test-spreadsheet-reader.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var async = require('async'); 3 | var SpreadsheetReader = require('../lib/spreadsheet-reader'); 4 | 5 | var sampleFiles = [ 6 | // OO XML workbook (Excel 2003 and later) 7 | 'test/input/sample.xlsx', 8 | // Legacy binary format (before Excel 2003) 9 | 'test/input/sample.xls' 10 | ]; 11 | 12 | describe('SpreadsheetReader', function () { 13 | describe('#ctor(file)', function () { 14 | 15 | it('should emit a single "open" event when file is ready', function (done) { 16 | async.eachSeries(sampleFiles, function (file, next) { 17 | new SpreadsheetReader(file).on('open', function (workbook) { 18 | workbook.should.be.an.Object.and.have.property('file', file); 19 | return next(); 20 | }).on('error', function (err) { 21 | throw err; 22 | }); 23 | }, done); 24 | }); 25 | 26 | it('should emit "data" events as data is being received', function (done) { 27 | async.eachSeries(sampleFiles, function (file, next) { 28 | var events = []; 29 | var total = {}; 30 | 31 | new SpreadsheetReader(file).on('open', function (workbook) { 32 | workbook.should.have.property('file', file); 33 | workbook.should.have.property('meta'); 34 | }).on('data', function (data) { 35 | events.push(data); 36 | }).on('error', function (err) { 37 | throw err; 38 | }).on('close', function () { 39 | events.length.should.be.above(0); 40 | events.forEach(function (e) { 41 | e.should.have.property('workbook'); 42 | e.workbook.should.have.property('file', file); 43 | e.should.have.property('sheet'); 44 | e.should.have.property('rows').and.be.an.Array; 45 | e.rows.length.should.be.above(0); 46 | total[e.sheet.name] = (total[e.sheet.name] || 0) + e.rows.length; 47 | }); 48 | 49 | total.should.have.property('Sheet1', 51); 50 | total.should.have.property('second sheet', 1384); 51 | 52 | return next(); 53 | }); 54 | 55 | }, done); 56 | }); 57 | 58 | }); 59 | 60 | describe('#read(file, callback)', function () { 61 | 62 | it('should load a workbook/sheet/row/cell structure', function (done) { 63 | async.eachSeries(sampleFiles, function (file, next) { 64 | SpreadsheetReader.read(file, function (err, workbook) { 65 | if (err) throw err; 66 | 67 | workbook.should.have.property('file', file); 68 | 69 | workbook.should.have.property('meta'); 70 | workbook.meta.should.have.property('user', 'Nicolas Mercier-Gaboury'); 71 | workbook.meta.should.have.property('sheets'); 72 | workbook.meta.sheets.should.be.an.Array.and.have.lengthOf(2); 73 | workbook.meta.sheets.should.containDeep(['Sheet1', 'second sheet']); 74 | 75 | workbook.should.have.property('sheets'); 76 | workbook.sheets.should.have.lengthOf(workbook.meta.sheets.length); 77 | 78 | workbook.sheets.forEach(function (sheet, index) { 79 | sheet.should.have.property('index', index); 80 | sheet.should.have.property('name', index === 0 ? 'Sheet1' : 'second sheet'); 81 | sheet.should.have.property('bounds'); 82 | sheet.bounds.should.be.an.Object; 83 | sheet.bounds.should.have.properties('rows', 'columns'); 84 | sheet.should.have.property('visibility', 'visible'); 85 | sheet.should.have.property('rows'); 86 | sheet.rows.should.have.lengthOf(sheet.bounds.rows); 87 | 88 | if (index === 0) { 89 | sheet.bounds.should.have.property('columns', 26); 90 | sheet.bounds.should.have.property('rows', 51); 91 | } else if (index === 1) { 92 | sheet.bounds.should.have.property('columns', 3); 93 | sheet.bounds.should.have.property('rows', 1384); 94 | } 95 | 96 | sheet.rows.forEach(function (row) { 97 | row.should.be.an.Array.and.have.lengthOf(sheet.bounds.columns); 98 | row.forEach(function (cell) { 99 | cell.should.have.properties('row', 'column', 'address', 'value'); 100 | }); 101 | }); 102 | }); 103 | 104 | return next(); 105 | }); 106 | }, done); 107 | }); 108 | 109 | it('should parse cell addresses', function (done) { 110 | async.eachSeries(sampleFiles, function (file, next) { 111 | SpreadsheetReader.read(file, function (err, workbook) { 112 | if (err) throw err; 113 | 114 | var rows = workbook.sheets[0].rows; 115 | rows[0][0].should.have.property('address', 'A1'); 116 | rows[0][4].should.have.property('address', 'E1'); 117 | rows[5][0].should.have.property('address', 'A6'); 118 | rows[5][4].should.have.property('address', 'E6'); 119 | rows[50][0].should.have.property('address', 'A51'); 120 | rows[50][4].should.have.property('address', 'E51'); 121 | 122 | return next(); 123 | }); 124 | }, done); 125 | }); 126 | 127 | it('should parse numeric values', function (done) { 128 | async.eachSeries(sampleFiles, function (file, next) { 129 | SpreadsheetReader.read(file, function (err, workbook) { 130 | 131 | if (err) throw err; 132 | 133 | var row1 = workbook.sheets[0].rows[1]; 134 | var row2 = workbook.sheets[0].rows[2]; 135 | var row3 = workbook.sheets[0].rows[3]; 136 | var row4 = workbook.sheets[0].rows[4]; 137 | var row5 = workbook.sheets[0].rows[5]; 138 | 139 | [row1, row2, row3, row4, row5].forEach(function (row) { 140 | row[0].value.should.be.a.Number; 141 | row[1].value.should.be.a.Number; 142 | }); 143 | 144 | row1[0].value.should.be.exactly(1); 145 | row1[1].value.should.be.exactly(1.0001); 146 | row2[0].value.should.be.exactly(2); 147 | row2[1].value.should.be.exactly(2); 148 | row3[0].value.should.be.exactly(3); 149 | row3[1].value.should.be.exactly(3.00967676764465); 150 | row4[0].value.should.be.exactly(4); 151 | row4[1].value.should.be.exactly(0); 152 | row5[0].value.should.be.exactly(5); 153 | row5[1].value.should.be.exactly(5.00005); 154 | 155 | return next(); 156 | }); 157 | }, done); 158 | }); 159 | 160 | it('should parse string values', function (done) { 161 | async.eachSeries(sampleFiles, function (file, next) { 162 | SpreadsheetReader.read(file, function (err, workbook) { 163 | if (err) throw err; 164 | 165 | var cell1 = workbook.sheets[0].rows[1][2]; 166 | var cell2 = workbook.sheets[0].rows[2][2]; 167 | var cell3 = workbook.sheets[0].rows[3][2]; 168 | var cell4 = workbook.sheets[0].rows[4][2]; 169 | var cell5 = workbook.sheets[0].rows[5][2]; 170 | 171 | [cell1, cell2, cell3, cell4, cell5].forEach(function (cell) { 172 | cell.value.should.be.a.String; 173 | }); 174 | 175 | cell1.value.should.be.exactly('Some text'); 176 | cell2.value.should.be.exactly('{ "property": "value" }'); 177 | cell3.value.should.be.exactly('ÉéÀàçÇùÙ'); 178 | cell4.value.should.be.exactly('some "quoted" text'); 179 | cell5.value.should.be.exactly('more \'quoted\' "text"'); 180 | 181 | return next(); 182 | }); 183 | }, done); 184 | }); 185 | 186 | it('should parse date values', function (done) { 187 | async.eachSeries(sampleFiles, function (file, next) { 188 | SpreadsheetReader.read(file, function (err, workbook) { 189 | if (err) throw err; 190 | 191 | var row1 = workbook.sheets[0].rows[1]; 192 | var row2 = workbook.sheets[0].rows[2]; 193 | var row3 = workbook.sheets[0].rows[3]; 194 | var row4 = workbook.sheets[0].rows[4]; 195 | var row5 = workbook.sheets[0].rows[5]; 196 | 197 | [row1, row2, row3, row4, row5].forEach(function (row) { 198 | row[3].value.should.be.a.Date; 199 | row[4].value.should.be.a.Date; 200 | }); 201 | 202 | row1[3].value.should.eql(new Date(2013, 0, 1, 0, 0, 0)); 203 | row1[4].value.should.eql(new Date(2013, 0, 1, 12, 54, 21)); 204 | row2[3].value.should.eql(new Date(2013, 0, 2, 0, 0, 0)); 205 | //row2[4].value.should.eql(new Date(0, 0, 0, 0, 0, 34)); 206 | row3[4].value.should.eql(new Date(2013, 0, 3, 3, 45, 20)); 207 | row4[4].value.should.eql(new Date(2013, 0, 4, 0, 0, 0)); 208 | row5[4].value.should.eql(new Date(2013, 0, 5, 16, 0, 0)); 209 | 210 | return next(); 211 | }); 212 | }, done); 213 | }); 214 | 215 | it('should parse empty cells as nulls', function (done) { 216 | async.eachSeries(sampleFiles, function (file, next) { 217 | SpreadsheetReader.read(file, function (err, workbook) { 218 | if (err) throw err; 219 | 220 | workbook.sheets[0].rows.splice(1).forEach(function (row) { 221 | assert.ok(row[5].value === null); 222 | }); 223 | 224 | return next(); 225 | }); 226 | }, done); 227 | }); 228 | 229 | it('should parse cells with errors', function (done) { 230 | async.eachSeries(sampleFiles, function (file, next) { 231 | SpreadsheetReader.read(file, function (err, workbook) { 232 | if (err) throw err; 233 | 234 | var row1 = workbook.sheets[0].rows[1]; 235 | var row2 = workbook.sheets[0].rows[2]; 236 | var row3 = workbook.sheets[0].rows[3]; 237 | 238 | row1[6].value.should.be.an.Error.and.have.property('errorCode', '#DIV/0!'); 239 | row2[6].value.should.be.an.Error.and.have.property('errorCode', '#NAME?'); 240 | row3[6].value.should.be.an.Error.and.have.property('errorCode', '#VALUE!'); 241 | 242 | return next(); 243 | }); 244 | }, done); 245 | }); 246 | 247 | it('should parse cells with booleans', function (done) { 248 | async.eachSeries(sampleFiles, function (file, next) { 249 | SpreadsheetReader.read(file, function (err, workbook) { 250 | if (err) throw err; 251 | 252 | var row1 = workbook.sheets[0].rows[1]; 253 | var row2 = workbook.sheets[0].rows[2]; 254 | 255 | row1[7].value.should.be.true; 256 | row2[7].value.should.be.false; 257 | 258 | return next(); 259 | }); 260 | }, done); 261 | }); 262 | 263 | it('should parse only the selected sheet by index', function (done) { 264 | async.eachSeries(sampleFiles, function (file, next) { 265 | SpreadsheetReader.read(file, { sheet: 0 }, function (err, workbook) { 266 | if (err) throw err; 267 | workbook.sheets.should.be.an.Array.and.have.lengthOf(1); 268 | workbook.sheets[0].should.have.properties({ index: 0, name: 'Sheet1' }); 269 | return next(); 270 | }); 271 | }, done); 272 | }); 273 | 274 | it('should parse only the selected sheet by name', function (done) { 275 | async.eachSeries(sampleFiles, function (file, next) { 276 | SpreadsheetReader.read(file, { sheet: 'Sheet1' }, function (err, workbook) { 277 | if (err) throw err; 278 | workbook.sheets.should.be.an.Array.and.have.lengthOf(1); 279 | workbook.sheets[0].should.have.properties({ index: 0, name: 'Sheet1' }); 280 | return next(); 281 | }); 282 | }, done); 283 | }); 284 | 285 | it('should parse only metadata', function (done) { 286 | async.eachSeries(sampleFiles, function (file, next) { 287 | SpreadsheetReader.read(file, { meta: true }, function (err, workbook) { 288 | if (err) throw err; 289 | if (workbook.sheets) throw new Error('workbook.sheets should be undefined'); 290 | return next(); 291 | }); 292 | }, done); 293 | }); 294 | 295 | it('should parse up to 10 rows on the first sheet', function (done) { 296 | async.eachSeries(sampleFiles, function (file, next) { 297 | SpreadsheetReader.read(file, { sheet: 0, maxRows: 10 }, function (err, workbook) { 298 | if (err) throw err; 299 | workbook.sheets[0].should.have.property('name', 'Sheet1'); 300 | workbook.sheets[0].rows.should.have.lengthOf(10); 301 | return next(); 302 | }); 303 | }, done); 304 | }); 305 | 306 | it('should fail if the file does not exist', function (done) { 307 | SpreadsheetReader.read('unknown.xlsx', function (err, workbook) { 308 | if (workbook) throw new Error('workbook should be undefined'); 309 | err.should.be.an.Error.and.have.property('id', 'open_workbook_failed'); 310 | err.stack.should.containEql('----- Python Traceback -----'); 311 | return done(); 312 | }); 313 | }); 314 | 315 | it('should fail if the file is not a valid workbook', function (done) { 316 | SpreadsheetReader.read('package.json', function (err, workbook) { 317 | if (workbook) throw new Error('workbook should be undefined'); 318 | err.should.be.an.Error.and.have.property('id', 'open_workbook_failed'); 319 | return done(); 320 | }); 321 | }); 322 | }); 323 | 324 | }); 325 | -------------------------------------------------------------------------------- /test/test-spreadsheet-writer.js: -------------------------------------------------------------------------------- 1 | var SpreadsheetReader = require('../lib/spreadsheet-reader'); 2 | var SpreadsheetWriter = require('../lib/spreadsheet-writer'); 3 | 4 | SpreadsheetWriter.prototype.saveAndRead = function (callback) { 5 | var filePath = this.filePath; 6 | this.save(function (err) { 7 | if (err) return callback(err); 8 | SpreadsheetReader.read(filePath, callback); 9 | }); 10 | }; 11 | 12 | describe('SpreadsheetWriter', function () { 13 | 14 | describe('#ctor(path, options)', function () { 15 | it('should emit "open" event', function (done) { 16 | var writer = new SpreadsheetWriter('test/output/simple.xlsx'); 17 | writer.on('open', function () { 18 | done(); 19 | }).save(); 20 | }); 21 | }); 22 | 23 | describe('.addSheet(name)', function () { 24 | it('should write a simple file with multiple sheets', function (done) { 25 | var writer = new SpreadsheetWriter('test/output/multiple_sheets.xlsx'); 26 | writer.addSheet('first').addSheet('second').addSheet('third'); 27 | writer.saveAndRead(function (err, workbook) { 28 | if (err) return done(err); 29 | workbook.meta.sheets.should.have.lengthOf(3); 30 | workbook.meta.sheets.should.containDeep(['first', 'second', 'third']); 31 | done(); 32 | }); 33 | }); 34 | }); 35 | 36 | describe('.addFormat(id, properties)', function (done) { 37 | it('should write with a simple format', function (done) { 38 | var writer = new SpreadsheetWriter('test/output/simple_format.xlsx'); 39 | writer.addFormat('my format', { font: { bold: true, color: 'red' } }); 40 | writer.write(0, 0, 'Hello World!', 'my format'); 41 | writer.saveAndRead(function (err, workbook) { 42 | if (err) return done(err); 43 | workbook.sheets[0].rows[0][0].value.should.be.exactly('Hello World!'); 44 | done(); 45 | }); 46 | }); 47 | }); 48 | 49 | describe('.write(row, col, value)', function () { 50 | it('should write to the correct cells from indexed positions', function (done) { 51 | var writer = new SpreadsheetWriter('test/output/write2.xlsx'); 52 | writer.write(0, 0, 1); 53 | writer.write(0, 2, 2); 54 | writer.write(1, 1, 3); 55 | writer.write(3, 3, 4); 56 | writer.saveAndRead(function (err, workbook) { 57 | if (err) return done(err); 58 | workbook.sheets[0].cell('A1').value.should.be.exactly(1); 59 | workbook.sheets[0].cell('C1').value.should.be.exactly(2); 60 | workbook.sheets[0].cell('B2').value.should.be.exactly(3); 61 | workbook.sheets[0].cell('D4').value.should.be.exactly(4); 62 | done(); 63 | }); 64 | }); 65 | it('should write supported data types', function (done) { 66 | var writer = new SpreadsheetWriter('test/output/write3.xlsx'); 67 | var number = 1934587.9812858; 68 | var string = 'some html tags'; 69 | var date = new Date(2014, 0, 1, 0, 0, 0, 0); 70 | 71 | writer.write(0, 0, number); 72 | writer.write(0, 1, string); 73 | writer.write(0, 2, date); 74 | 75 | writer.saveAndRead(function (err, workbook) { 76 | if (err) return done(err); 77 | var row = workbook.sheets[0].rows[0]; 78 | row[0].value.should.be.exactly(number); 79 | row[1].value.should.be.exactly(string); 80 | row[2].value.should.eql(date); 81 | done(); 82 | }); 83 | }); 84 | }); 85 | 86 | describe('.write(cell, value)', function () { 87 | it('should write to the correct cell from its address', function (done) { 88 | var writer = new SpreadsheetWriter('test/output/write1.xlsx'); 89 | writer.write('A1', 1); 90 | writer.write('C1', 2); 91 | writer.write('B2', 3); 92 | writer.write('D4', 4); 93 | writer.saveAndRead(function (err, workbook) { 94 | if (err) return done(err); 95 | workbook.sheets[0].cell('A1').value.should.be.exactly(1); 96 | workbook.sheets[0].cell('C1').value.should.be.exactly(2); 97 | workbook.sheets[0].cell('B2').value.should.be.exactly(3); 98 | workbook.sheets[0].cell('D4').value.should.be.exactly(4); 99 | done(); 100 | }); 101 | }); 102 | }); 103 | 104 | describe('.save(callback)', function () { 105 | it('should save a hello world file', function (done) { 106 | var writer = new SpreadsheetWriter('test/output/helloworld.xlsx'); 107 | writer.write(0, 0, 'Hello World!'); 108 | writer.saveAndRead(function (err, workbook) { 109 | if (err) return done(err); 110 | workbook.sheets[0].rows[0][0].value.should.be.exactly('Hello World!'); 111 | done(); 112 | }); 113 | }); 114 | it('should emit a "close" event', function (done) { 115 | var writer = new SpreadsheetWriter('test/output/helloworld.xlsx'); 116 | writer.write(0, 0, 'Hello World!'); 117 | writer.on('close', function () { 118 | done(); 119 | }).save(); 120 | }); 121 | }); 122 | 123 | describe('xlwt', function () { 124 | it('should write a simple file with xlwt', function (done) { 125 | var writer = new SpreadsheetWriter('test/output/simple.xls'); 126 | writer.write(0, 0, 'hello world'); 127 | writer.addFormat('my format', { font: { bold: true }}); 128 | writer.write(1, 0, 'this is bold', 'my format'); 129 | writer.save(done); 130 | }); 131 | }); 132 | 133 | }); 134 | -------------------------------------------------------------------------------- /test/test-utilities.js: -------------------------------------------------------------------------------- 1 | var _ = require('../lib/utilities'); 2 | 3 | describe('utilities', function () { 4 | describe('extend', function () { 5 | it('should extend an object with a source', function () { 6 | _.extend({}, { a: 'b' }).should.have.property('a', 'b'); 7 | }); 8 | it('should extend an object with multiple sources', function () { 9 | _.extend({}, { a: 'b' }, { c: 'd' }).should.have.properties({ 10 | a: 'b', 11 | c: 'd' 12 | }); 13 | }); 14 | it('should extend an error with properties', function () { 15 | _.extend(new Error('error'), { a: 'b' }, { c: 'd' }).should.be.an.Error.and.have.properties({ 16 | a: 'b', 17 | c: 'd' 18 | }); 19 | }); 20 | }); 21 | 22 | describe('flatten', function () { 23 | it('should flatten an array', function () { 24 | _.flatten([1, 2, 3, [4]]).should.containDeep([1, 2, 3, 4]); 25 | }); 26 | it('should flatten an array recursively', function () { 27 | _.flatten([[1, [2]], [[[3]]], 4]).should.containDeep([1, 2, 3, 4]); 28 | }); 29 | }); 30 | 31 | describe('find', function () { 32 | it('should return the item matching the truth test', function () { 33 | _.find([1, 2, 3], function (el) { 34 | return el === 2; 35 | }).should.be.exactly(2); 36 | }); 37 | }); 38 | 39 | describe('randomString', function () { 40 | it('should return a random string', function () { 41 | _.randomString(8).should.have.lengthOf(8).and.match(/\w+/); 42 | }); 43 | }); 44 | 45 | describe('cellToRowCol', function () { 46 | it('should return row and column indexes', function () { 47 | _.cellToRowCol('A1').should.containDeep([0, 0]); 48 | _.cellToRowCol('C3').should.containDeep([2, 2]); 49 | _.cellToRowCol('AA12').should.containDeep([11, 26]); 50 | }); 51 | }); 52 | 53 | describe('colToInt', function () { 54 | it('should convert a column letter to a zero-based index', function () { 55 | _.colToInt('A').should.be.exactly(1); 56 | _.colToInt('C').should.be.exactly(3); 57 | _.colToInt('AA').should.be.exactly(27); 58 | }); 59 | }); 60 | }); 61 | --------------------------------------------------------------------------------