Skip to content

Commit

Permalink
API Improvements
Browse files Browse the repository at this point in the history
- `aoa_to_sheet` function (fixes #314 h/t @fonzy2013 @rvdwijngaard)
- `writeFileAsync` function (fixes #396 h/t @barbalex)
- `sheet_to_json` tests + docs + blankrows (fixes #602 h/t @EEaglehouse)
- write number format scan now includes every index >= 50
- propagate SSF IE8 fixes (fixes #171 h/t @SheetJSDev)
- update shim for extendscript (see #603 h/t @firas3d)
- more flow type definitions
  • Loading branch information
SheetJSDev committed Mar 25, 2017
1 parent 60e8905 commit 70c48a7
Show file tree
Hide file tree
Showing 22 changed files with 464 additions and 137 deletions.
54 changes: 49 additions & 5 deletions README.md
Expand Up @@ -49,6 +49,7 @@ with a unified JS representation, and ES3/ES5 browser compatibility back to IE6.
* [Supported Output Formats](#supported-output-formats)
* [Output Type](#output-type)
- [Utility Functions](#utility-functions)
* [Array of Arrays Input](#array-of-arrays-input)
* [Formulae Output](#formulae-output)
* [CSV and general DSV Output](#csv-and-general-dsv-output)
* [JSON](#json)
Expand Down Expand Up @@ -98,6 +99,7 @@ CDNjs automatically pulls the latest version and makes all versions available at

The `demos` directory includes sample projects for:

- [`angular`](demos/angular/)
- [`browserify`](demos/browserify/)
- [`requirejs`](demos/requirejs/)
- [`systemjs`](demos/systemjs/)
Expand Down Expand Up @@ -377,12 +379,19 @@ Parse options are described in the [Parsing Options](#parsing-options) section.

`XLSX.writeFile(wb, filename, write_opts)` attempts to write `wb` to `filename`

`XLSX.writeFileAsync(filename, wb, o, cb)` attempts to write `wb` to `filename`.
If `o` is omitted, the writer will use the third argument as the callback.

Write options are described in the [Writing Options](#writing-options) section.

### Utilities

Utilities are available in the `XLSX.utils` object:

**Importing:**

- `aoa_to_sheet` converts an array of arrays of JS data to a worksheet.

**Exporting:**

- `sheet_to_json` converts a worksheet object to an array of JSON objects.
Expand Down Expand Up @@ -655,7 +664,7 @@ objects which have the following properties:
```typescript
type ColInfo = {
MDW?:number; // Excel's "Max Digit Width" unit, always integral
width:number; // width in Excel's "Max Digit Width", width*256 is integral
width:number; // width in Excel's "Max Digit Width", width*256 is integral
wpx?:number; // width in screen pixels
wch?:number; // intermediate character calculation
};
Expand All @@ -666,15 +675,16 @@ follow the priority order:

1) use `width` field if available
2) use `wpx` pixel width if available
2) use `wch` character count if available
3) use `wch` character count if available

## Parsing Options

The exported `read` and `readFile` functions accept an options argument:

| Option Name | Default | Description |
| :---------- | ------: | :--------------------------------------------------- |
| type | | Input data encoding (see Input Type below) |
| cellFormula | true | Save formulae to the .f field ** |
| cellFormula | true | Save formulae to the .f field |
| cellHTML | true | Parse rich text and save HTML to the .h field |
| cellNF | false | Save number format string to the .z field |
| cellStyles | false | Save style/theme info to the .s field |
Expand All @@ -689,8 +699,6 @@ The exported `read` and `readFile` functions accept an options argument:
| password | "" | If defined and file is encrypted, use password ** |
| WTF | false | If true, throw errors on unexpected file features ** |

- `cellFormula` option only applies to formats that require extra processing to
parse formulae (XLS/XLSB).
- Even if `cellNF` is false, formatted text will be generated and saved to `.w`
- In some cases, sheets may be parsed even if `bookSheets` is false.
- `bookSheets` and `bookProps` combine to give both sets of information
Expand Down Expand Up @@ -794,6 +802,8 @@ The `type` argument for `write` mirrors the `type` argument for `read`:

The `sheet_to_*` functions accept a worksheet and an optional options object.

The `*_to_sheet` functions accept a data object and an optional options object.

The examples are based on the following worksheet:

```
Expand All @@ -804,6 +814,30 @@ XXX| A | B | C | D | E | F | G |
3 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
```

### Array of Arrays Input

`XLSX.utils.aoa_to_sheet` takes an array of arrays of JS values and returns a
worksheet resembling the input data. Numbers, Booleans and Strings are stored
as the corresponding styles. Dates are stored as date or numbers. Array holes
and explicit `undefined` values are skipped. `null` values may be stubbed. All
other values are stored as strings. The function takes an options argument:

| Option Name | Default | Description |
| :---------- | :------: | :-------------------------------------------------- |
| dateNF | fmt 14 | Use specified date format in string output |
| cellDates | false | Store dates as type `d` (default is `n`) |
| sheetStubs | false | Create cell objects of type `z` for `null` values |

To generate the example sheet:

```js
var ws = XLSX.utils.aoa_to_sheet([
"SheetJS".split(""),
[1,2,3,4,5,6,7],
[2,3,4,5,6,7,8]
]);
```

### Formulae Output

`XLSX.utils.sheet_to_formulae` generates an array of commands that represent
Expand All @@ -828,8 +862,10 @@ produces CSV output. The function takes an options argument:
| RS | `"\n"` | "Record Separator" delimiter between rows |
| dateNF | fmt 14 | Use specified date format in string output |
| strip | false | Remove trailing field separators in each record ** |
| blankrows | true | Include blank lines in the CSV output |

- `strip` will remove trailing commas from each line under default `FS/RS`
- blankrows must be set to `false` to skip blank lines.

For the example sheet:

Expand Down Expand Up @@ -858,6 +894,7 @@ generate different types of JS objects. The function takes an options argument:
| header | | Control output format (see table below) |
| dateNF | fmt 14 | Use specified date format in string output |
| defval | | Use specified value in place of null or undefined |
| blankrows | ** | Include blank lines in the output ** |

- `raw` only affects cells which have a format code (`.z`) field or a formatted
text (`.w`) field.
Expand All @@ -869,6 +906,10 @@ generate different types of JS objects. The function takes an options argument:
- `null` values are returned when `raw` is true but are skipped when false.
- If `defval` is not specified, null and undefined values are skipped normally.
If specified, all null and undefined points will be filled with `defval`
- When `header` is `1`, the default is to generate blank rows. `blankrows` must
be set to `false` to skip blank rows.
- When `header` is not `1`, the default is to skip blank rows. `blankrows` must
be truthy to generate blank rows

`range` is expected to be one of:

Expand All @@ -887,6 +928,9 @@ generate different types of JS objects. The function takes an options argument:
| array of strings | Use specified strings as keys in row objects |
| (default) | Read and disambiguate first row as keys |

If header is not `1`, the row object will contain the non-enumerable property
`__rowNum__` that represents the row of the sheet corresponding to the entry.

For the example sheet:

```js
Expand Down
16 changes: 8 additions & 8 deletions bits/10_ssf.js
Expand Up @@ -332,7 +332,7 @@ function write_num_flt(type/*:string*/, fmt/*:string*/, val/*:number*/)/*:string
if(fmt.charCodeAt(fmt.length - 1) === 44) return write_num_cm(type, fmt, val);
if(fmt.indexOf('%') !== -1) return write_num_pct(type, fmt, val);
if(fmt.indexOf('E') !== -1) return write_num_exp(fmt, val);
if(fmt.charCodeAt(0) === 36) return "$"+write_num_flt(type,fmt.substr(fmt[1]==' '?2:1),val);
if(fmt.charCodeAt(0) === 36) return "$"+write_num_flt(type,fmt.substr(fmt.charAt(1)==' '?2:1),val);
var o;
var r/*:?Array<string>*/, ri, ff, aval = Math.abs(val), sign = val < 0 ? "-" : "";
if(fmt.match(/^00+$/)) return sign + pad0r(aval,fmt.length);
Expand All @@ -358,7 +358,7 @@ function write_num_flt(type/*:string*/, fmt/*:string*/, val/*:number*/)/*:string
if((r = fmt.match(/^([0#]+)(\\?-([0#]+))+$/))) {
o = _strrev(write_num_flt(type, fmt.replace(/[\\-]/g,""), val));
ri = 0;
return _strrev(_strrev(fmt.replace(/\\/g,"")).replace(/[0#]/g,function(x){return ri<o.length?o[ri++]:x==='0'?'0':"";}));
return _strrev(_strrev(fmt.replace(/\\/g,"")).replace(/[0#]/g,function(x){return ri<o.length?o.charAt(ri++):x==='0'?'0':"";}));
}
if(fmt.match(phone)) {
o = write_num_flt(type, "##########", val);
Expand All @@ -370,7 +370,7 @@ function write_num_flt(type/*:string*/, fmt/*:string*/, val/*:number*/)/*:string
ff = frac(aval, Math.pow(10,ri)-1, false);
o = "" + sign;
oa = write_num("n", /*::String(*/r[1]/*::)*/, ff[1]);
if(oa[oa.length-1] == " ") oa = oa.substr(0,oa.length-1) + "0";
if(oa.charAt(oa.length-1) == " ") oa = oa.substr(0,oa.length-1) + "0";
o += oa + /*::String(*/r[2]/*::)*/ + "/" + /*::String(*/r[3]/*::)*/;
oa = rpad_(ff[2],ri);
if(oa.length < r[4].length) oa = hashq(r[4].substr(r[4].length-oa.length)) + oa;
Expand Down Expand Up @@ -441,7 +441,7 @@ function write_num_int(type/*:string*/, fmt/*:string*/, val/*:number*/)/*:string
if(fmt.charCodeAt(fmt.length - 1) === 44) return write_num_cm2(type, fmt, val);
if(fmt.indexOf('%') !== -1) return write_num_pct2(type, fmt, val);
if(fmt.indexOf('E') !== -1) return write_num_exp2(fmt, val);
if(fmt.charCodeAt(0) === 36) return "$"+write_num_int(type,fmt.substr(fmt[1]==' '?2:1),val);
if(fmt.charCodeAt(0) === 36) return "$"+write_num_int(type,fmt.substr(fmt.charAt(1)==' '?2:1),val);
var o;
var r, ri, ff, aval = Math.abs(val), sign = val < 0 ? "-" : "";
if(fmt.match(/^00+$/)) return sign + pad0(aval,fmt.length);
Expand Down Expand Up @@ -471,7 +471,7 @@ function write_num_int(type/*:string*/, fmt/*:string*/, val/*:number*/)/*:string
if((r = fmt.match(/^([0#]+)(\\?-([0#]+))+$/))) {
o = _strrev(write_num_int(type, fmt.replace(/[\\-]/g,""), val));
ri = 0;
return _strrev(_strrev(fmt.replace(/\\/g,"")).replace(/[0#]/g,function(x){return ri<o.length?o[ri++]:x==='0'?'0':"";}));
return _strrev(_strrev(fmt.replace(/\\/g,"")).replace(/[0#]/g,function(x){return ri<o.length?o.charAt(ri++):x==='0'?'0':"";}));
}
if(fmt.match(phone)) {
o = write_num_int(type, "##########", val);
Expand All @@ -483,7 +483,7 @@ function write_num_int(type/*:string*/, fmt/*:string*/, val/*:number*/)/*:string
ff = frac(aval, Math.pow(10,ri)-1, false);
o = "" + sign;
oa = write_num("n", r[1], ff[1]);
if(oa[oa.length-1] == " ") oa = oa.substr(0,oa.length-1) + "0";
if(oa.charAt(oa.length-1) == " ") oa = oa.substr(0,oa.length-1) + "0";
o += oa + r[2] + "/" + r[3];
oa = rpad_(ff[2],ri);
if(oa.length < r[4].length) oa = hashq(r[4].substr(r[4].length-oa.length)) + oa;
Expand Down Expand Up @@ -735,7 +735,7 @@ function eval_fmt(fmt/*:string*/, v/*:any*/, opts/*:any*/, flen/*:number*/) {
j=out[i].v.indexOf(".")>-1&&i===decpt?out[i].v.indexOf(".")-1:out[i].v.length-1;
vv = out[i].v.substr(j+1);
for(; j>=0; --j) {
if(jj>=0 && (out[i].v[j] === "0" || out[i].v[j] === "#")) vv = ostr[jj--] + vv;
if(jj>=0 && (out[i].v.charAt(j) === "0" || out[i].v.charAt(j) === "#")) vv = ostr.charAt(jj--) + vv;
}
out[i].v = vv;
out[i].t = 't';
Expand All @@ -748,7 +748,7 @@ function eval_fmt(fmt/*:string*/, v/*:any*/, opts/*:any*/, flen/*:number*/) {
j=out[i].v.indexOf(".")>-1&&i===decpt?out[i].v.indexOf(".")+1:0;
vv = out[i].v.substr(0,j);
for(; j<out[i].v.length; ++j) {
if(jj<ostr.length) vv += ostr[jj++];
if(jj<ostr.length) vv += ostr.charAt(jj++);
}
out[i].v = vv;
out[i].t = 't';
Expand Down
12 changes: 12 additions & 0 deletions bits/20_jsutils.js
Expand Up @@ -34,6 +34,18 @@ function datenum(v/*:Date*/, date1904/*:?boolean*/)/*:number*/ {
if(date1904) epoch += 1462*24*60*60*1000;
return (epoch + 2209161600000) / (24 * 60 * 60 * 1000);
}
function numdate(v/*:number*/)/*:Date*/ {
var date = SSF.parse_date_code(v);
var val = new Date();
if(date == null) throw new Error("Bad Date Code: " + v);
val.setUTCDate(date.d);
val.setUTCMonth(date.m-1);
val.setUTCFullYear(date.y);
val.setUTCHours(date.H);
val.setUTCMinutes(date.M);
val.setUTCSeconds(date.S);
return val;
}

/* ISO 8601 Duration */
function parse_isodur(s) {
Expand Down
36 changes: 36 additions & 0 deletions bits/27_csfutils.js
@@ -0,0 +1,36 @@
function sheet_to_workbook(sheet/*:Worksheet*/, opts)/*:Workbook*/ {
var n = opts && opts.sheet ? opts.sheet : "Sheet1";
var sheets = {}; sheets[n] = sheet;
return { SheetNames: [n], Sheets: sheets };
}

function aoa_to_sheet(data/*:AOA*/, opts/*:?any*/)/*:Worksheet*/ {
var o = opts || {};
var ws/*:Worksheet*/ = ({}/*:any*/);
var range/*:Range*/ = ({s: {c:10000000, r:10000000}, e: {c:0, r:0}}/*:any*/);
for(var R = 0; R != data.length; ++R) {
for(var C = 0; C != data[R].length; ++C) {
if(typeof data[R][C] === 'undefined') continue;
var cell/*:Cell*/ = ({v: data[R][C] }/*:any*/);
if(range.s.r > R) range.s.r = R;
if(range.s.c > C) range.s.c = C;
if(range.e.r < R) range.e.r = R;
if(range.e.c < C) range.e.c = C;
var cell_ref = encode_cell(({c:C,r:R}/*:any*/));
if(cell.v === null) { if(!o.cellStubs) continue; cell.t = 'z'; }
else if(typeof cell.v === 'number') cell.t = 'n';
else if(typeof cell.v === 'boolean') cell.t = 'b';
else if(cell.v instanceof Date) {
cell.z = o.dateNF || SSF._table[14];
if(o.cellDates) cell.t = 'd';
else { cell.t = 'n'; cell.v = datenum(cell.v); }
cell.w = SSF.format(cell.z, cell.v);
}
else cell.t = 's';
ws[cell_ref] = cell;
}
}
if(range.s.c < 10000000) ws['!ref'] = encode_range(range);
return ws;
}

2 changes: 1 addition & 1 deletion bits/47_styxml.js
Expand Up @@ -222,7 +222,7 @@ function parse_numFmts(t, styles, opts) {

function write_numFmts(NF/*:{[n:number]:string}*/, opts) {
var o = ["<numFmts>"];
[[5,8],[23,26],[41,44],[63,66],[164,392]].forEach(function(r) {
[[5,8],[23,26],[41,44],[/*63*/50,/*66],[164,*/392]].forEach(function(r) {
for(var i = r[0]; i <= r[1]; ++i) if(NF[i] != null) o[o.length] = (writextag('numFmt',null,{numFmtId:i,formatCode:escapexml(NF[i])}));
});
if(o.length === 1) return "";
Expand Down
6 changes: 3 additions & 3 deletions bits/62_fxls.js
Expand Up @@ -806,18 +806,18 @@ function stringify_formula(formula/*Array<any>*/, range, cell/*:any*/, supbooks,
/* 2.5.198.31 TODO */
case 'PtgAreaN':
type = f[1][0]; r = shift_range_xls(f[1][1], _range, opts);
stack.push(encode_range_xls(r, opts));
stack.push(encode_range_xls((r/*:any*/), opts));
break;
/* 2.5.198.27 TODO: fixed points */
case 'PtgArea':
type = f[1][0]; r = shift_range_xls(f[1][1], _range, opts);
stack.push(encode_range_xls(r, opts));
stack.push(encode_range_xls((r/*:any*/), opts));
break;
/* 2.5.198.28 */
case 'PtgArea3d': // TODO: lots of stuff
type = f[1][0]; ixti = /*::Number(*/f[1][1]/*::)*/; r = f[1][2];
sname = (supbooks && supbooks[1] ? supbooks[1][ixti+1] : "**MISSING**");
stack.push(sname + "!" + encode_range(r));
stack.push(sname + "!" + encode_range((r/*:any*/)));
break;
/* 2.5.198.41 */
case 'PtgAttrSum':
Expand Down
6 changes: 5 additions & 1 deletion bits/75_xlml.js
Expand Up @@ -167,6 +167,7 @@ function xlml_normalize(d)/*:string*/ {
var xlmlregex = /<(\/?)([^\s?>!\/:]*:|)([^\s?>]*[^\s?>\/])[^>]*>/mg;
//var xlmlregex = /<(\/?)([a-z0-9]*:|)(\w+)[^>]*>/mg;
function parse_xlml_xml(d, opts)/*:Workbook*/ {
make_ssf(SSF);

This comment has been minimized.

Copy link
@e-cloud

e-cloud May 1, 2017

@SheetJSDev why adding the call of make_ssf? And there are several calls inside some bits files. The copied ssf library -- 10_ssf.js should be called once, shouldn't it? Or is it for resetting the SSF instance?

This comment has been minimized.

Copy link
@SheetJSDev

SheetJSDev May 1, 2017

Author Contributor

It's to reset the string formatting table. Excel has a set of built-in formats (check the Default Number Formats table in https://github.com/sheetjs/js-xlsx#number-formats). We encountered some files that tried to change the default formats (and Excel gladly accepted them) so we have to reset. That whole logic eventually needs to be reworked: originally the format table was a global, and that wasn't an issue when you are only dealing with one file, but the table really should be a property of the workbook and passed to the formatter in every call to SSF.format.

var str = debom(xlml_normalize(d));
if(opts && opts.type == 'binary' && typeof cptable !== 'undefined') str = cptable.utils.decode(65001, char_codes(str));
if(str.substr(0,1000).indexOf("<html") >= 0) return parse_html(str, opts);
Expand Down Expand Up @@ -277,7 +278,10 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ {
break;

case 'NumberFormat':
stag.nf = xlml_parsexmltag(Rn[0]).Format || "General";
stag.nf = unescapexml(xlml_parsexmltag(Rn[0]).Format || "General");
if(XLMLFormatMap[stag.nf]) stag.nf = XLMLFormatMap[stag.nf];
for(var ssfidx = 0; ssfidx != 0x188; ++ssfidx) if(SSF._table[ssfidx] == stag.nf) break;
if(ssfidx == 0x188) for(ssfidx = 0x39; ssfidx != 0x188; ++ssfidx) if(SSF._table[ssfidx] == null) { SSF.load(stag.nf, ssfidx); break; }
break;

case 'Column':
Expand Down
2 changes: 1 addition & 1 deletion bits/76_xls.js
Expand Up @@ -85,7 +85,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ {
var out = {};
var Directory = {};
var found_sheet = false;
var range = {};
var range/*:Range*/ = ({}/*:any*/);
var last_formula = null;
var sst = [];
var cur_sheet = "";
Expand Down
18 changes: 15 additions & 3 deletions bits/88_write.js
Expand Up @@ -59,9 +59,7 @@ function writeSync(wb/*:Workbook*/, opts/*:?WriteOpts*/) {
}
}

function writeFileSync(wb/*:Workbook*/, filename/*:string*/, opts/*:?WriteFileOpts*/) {
var o = opts||{}; o.type = 'file';
o.file = filename;
function resolve_book_type(o/*?WriteFileOpts*/) {
if(!o.bookType) switch(o.file.slice(-5).toLowerCase()) {
case '.xlsx': o.bookType = 'xlsx'; break;
case '.xlsm': o.bookType = 'xlsm'; break;
Expand All @@ -74,6 +72,20 @@ function writeFileSync(wb/*:Workbook*/, filename/*:string*/, opts/*:?WriteFileOp
case '.ods': o.bookType = 'ods'; break;
case '.csv': o.bookType = 'csv'; break;
}}
}

function writeFileSync(wb/*:Workbook*/, filename/*:string*/, opts/*:?WriteFileOpts*/) {
var o = opts||{}; o.type = 'file';
o.file = filename;
resolve_book_type(o);
return writeSync(wb, o);
}

function writeFileAsync(filename/*:string*/, wb/*:Workbook*/, opts/*:?WriteFileOpts*/, cb/*:?(e?:ErrnoError)=>void*/) {
var o = opts||{}; o.type = 'file';
o.file = filename;
resolve_book_type(o);
o.type = 'buffer';
var _cb = cb; if(!(_cb instanceof Function)) _cb = (opts/*:any*/);
return _fs.writeFile(filename, writeSync(wb, o), _cb);
}

0 comments on commit 70c48a7

Please sign in to comment.