From 5a3774e36ec9d93a0d3aaf356b29697821e50f72 Mon Sep 17 00:00:00 2001 From: SheetJS Date: Fri, 17 Mar 2017 20:45:06 -0400 Subject: [PATCH] Formula write support - XLSX/XLML/ODS/CSV formula write - XLS Array/Formula record parsing - ODS to/from CSF formula conversion - XLML parse negative dates - formula multiformat and roundtrip tests - update SSF Closes issues: - closes #549 h/t @omershelef - closes #306 h/t @akaustel - closes #300 h/t @wawanopoulos - closes #152 h/t @florf - closes #103 h/t @christocracy - closes #112 h/t @johnyesberg - closes #307 h/t @alexanderknapstein --- README.md | 2 +- bits/10_ssf.js | 18 +++- bits/65_fods.js | 12 ++- bits/67_wsxml.js | 11 +- bits/75_xlml.js | 13 ++- bits/76_xls.js | 67 +++++++----- bits/78_writebiff.js | 10 +- bits/80_parseods.js | 1 + bits/81_writeods.js | 21 +++- bits/90_utils.js | 12 ++- multiformat.lst | 8 +- test.js | 34 ++++-- test_files | 2 +- tests.lst | 245 ++++++++++++++++++++++++++++++++----------- tests/write.js | 25 ++++- xlsx.flow.js | 165 ++++++++++++++++++++--------- xlsx.js | 163 +++++++++++++++++++--------- 17 files changed, 576 insertions(+), 233 deletions(-) diff --git a/README.md b/README.md index 3b171ab..73fd798 100644 --- a/README.md +++ b/README.md @@ -484,7 +484,7 @@ will not be generated; the parser `sheetStubs` option must be set to `true`. ### Formulae The A1-style formula string is stored in the `f` field. Even though different -file formats store the formulae in different ways, the formats are converted. +file formats store the formulae in different ways, the formats are translated. Shared formulae are decompressed and each cell has the correct formula. diff --git a/bits/10_ssf.js b/bits/10_ssf.js index bca57a7..715a869 100644 --- a/bits/10_ssf.js +++ b/bits/10_ssf.js @@ -310,7 +310,18 @@ function hashq(str/*:string*/)/*:string*/ { return o; } function rnd(val/*:number*/, d/*:number*/)/*:string*/ { var dd = Math.pow(10,d); return ""+(Math.round(val * dd)/dd); } -function dec(val/*:number*/, d/*:number*/)/*:number*/ { return Math.round((val-Math.floor(val))*Math.pow(10,d)); } +function dec(val/*:number*/, d/*:number*/)/*:number*/ { + if (d < ('' + Math.round((val-Math.floor(val))*Math.pow(10,d))).length) { + return 0; + } + return Math.round((val-Math.floor(val))*Math.pow(10,d)); +} +function carry(val/*:number*/, d/*:number*/)/*:number*/ { + if (d < ('' + Math.round((val-Math.floor(val))*Math.pow(10,d))).length) { + return 1; + } + return 0; +} function flr(val/*:number*/)/*:string*/ { if(val < 2147483647 && val > -2147483648) return ""+(val >= 0 ? (val|0) : (val-1|0)); return ""+Math.floor(val); } function write_num_flt(type/*:string*/, fmt/*:string*/, val/*:number*/)/*:string*/ { if(type.charCodeAt(0) === 40 && !fmt.match(closeparen)) { @@ -332,8 +343,7 @@ function write_num_flt(type/*:string*/, fmt/*:string*/, val/*:number*/)/*:string if((r = fmt.match(frac1))) return write_num_f1(r, aval, sign); if(fmt.match(/^#+0+$/)) return sign + pad0r(aval,fmt.length - fmt.indexOf("0")); if((r = fmt.match(dec1))) { - // $FlowIgnore - o = rnd(val, r[1].length).replace(/^([^\.]+)$/,"$1."+r[1]).replace(/\.$/,"."+r[1]).replace(/\.(\d*)$/,function($$, $1) { return "." + $1 + fill("0", r[1].length-$1.length); }); + o = rnd(val, r[1].length).replace(/^([^\.]+)$/,"$1."+r[1]).replace(/\.$/,"."+r[1]).replace(/\.(\d*)$/,function($$, $1) { return "." + $1 + fill("0", /*::(*/r/*::||[""])*/[1].length-$1.length); }); return fmt.indexOf("0.") !== -1 ? o : o.replace(/^0\./,"."); } fmt = fmt.replace(/^#+([0.])/, "$1"); @@ -342,7 +352,7 @@ function write_num_flt(type/*:string*/, fmt/*:string*/, val/*:number*/)/*:string } if((r = fmt.match(/^#,##0(\.?)$/))) return sign + commaify(pad0r(aval,0)); if((r = fmt.match(/^#,##0\.([#0]*0)$/))) { - return val < 0 ? "-" + write_num_flt(type, fmt, -val) : commaify(""+(Math.floor(val))) + "." + pad0(dec(val, r[1].length),r[1].length); + return val < 0 ? "-" + write_num_flt(type, fmt, -val) : commaify(""+(Math.floor(val) + carry(val, r[1].length))) + "." + pad0(dec(val, r[1].length),r[1].length); } if((r = fmt.match(/^#,#*,#0/))) return write_num_flt(type,fmt.replace(/^#,#*,/,""),val); if((r = fmt.match(/^([0#]+)(\\?-([0#]+))+$/))) { diff --git a/bits/65_fods.js b/bits/65_fods.js index fee62c9..ec9bdf1 100644 --- a/bits/65_fods.js +++ b/bits/65_fods.js @@ -6,6 +6,16 @@ function ods_to_csf_formula(f/*:string*/)/*:string*/ { f = f.substr(1); if(f.charCodeAt(0) == 61) f = f.substr(1); } + f = f.replace(/COM\.MICROSOFT\./g, ""); /* Part 3 Section 5.8 References */ - return f.replace(/\[((?:\.[A-Z]+[0-9]+)(?::\.[A-Z]+[0-9]+)?)\]/g, "$1").replace(/\./g, ""); + f = f.replace(/\[((?:\.[A-Z]+[0-9]+)(?::\.[A-Z]+[0-9]+)?)\]/g, function($$, $1) { return $1.replace(/\./g,""); }); + /* TODO: something other than this */ + f = f.replace(/\[.(#[A-Z]*[?!])\]/g, "$1"); + return f.replace(/[;~]/g,",").replace(/\|/g,";"); +} + +function csf_to_ods_formula(f/*:string*/)/*:string*/ { + var o = "of:=" + f.replace(crefregex, "$1[.$2$3$4$5]").replace(/\]:\[/g,":"); + /* TODO: something other than this */ + return o.replace(/;/g, "|").replace(/,/g,";"); } diff --git a/bits/67_wsxml.js b/bits/67_wsxml.js index 6ef817e..2218da9 100644 --- a/bits/67_wsxml.js +++ b/bits/67_wsxml.js @@ -126,7 +126,7 @@ function write_ws_xml_cols(ws, cols)/*:string*/ { } function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { - if(cell.v === undefined || cell.t === 'z') return ""; + if(cell.v === undefined && cell.f === undefined || cell.t === 'z') return ""; var vv = ""; var oldt = cell.t, oldv = cell.v; switch(cell.t) { @@ -152,7 +152,7 @@ function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { case 'd': o.t = "d"; break; case 'b': o.t = "b"; break; case 'e': o.t = "e"; break; - default: + default: if(cell.v == null) { delete cell.t; break; } if(opts.bookSST) { v = writetag('v', ''+get_sst_id(opts.Strings, cell.v)); o.t = "s"; break; @@ -160,6 +160,10 @@ function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { o.t = "str"; break; } if(cell.t != oldt) { cell.t = oldt; cell.v = oldv; } + if(cell.f) { + var ff = cell.F && cell.F.substr(0, ref.length) == ref ? {t:"array", ref:cell.F} : null; + v = writextag('f', escapexml(cell.f), ff) + (cell.v != null ? v : ""); + } return writextag('c', v, o); } @@ -215,7 +219,8 @@ return function parse_ws_xml_data(sdata, s, opts, guess) { if((cref=d.match(match_v))!= null && /*::cref != null && */cref[1] !== '') p.v=unescapexml(cref[1]); if(opts.cellFormula) { if((cref=d.match(match_f))!= null && /*::cref != null && */cref[1] !== '') { - p.f=unescapexml(utf8read(cref[1])); + /* TODO: match against XLSXFutureFunctions */ + p.f=unescapexml(utf8read(cref[1])).replace(/_xlfn\./,""); if(/*::cref != null && cref[0] != null && */cref[0].indexOf('t="array"') > -1) { p.F = (d.match(refregex)||[])[1]; if(p.F.indexOf(":") > -1) arrayf.push([safe_decode_range(p.F), p.F]); diff --git a/bits/75_xlml.js b/bits/75_xlml.js index 351d4fb..992c9a8 100644 --- a/bits/75_xlml.js +++ b/bits/75_xlml.js @@ -108,7 +108,7 @@ function parse_xlml_data(xml, ss, data, cell/*:any*/, base, styles, csty, row, a case 'DateTime': cell.v = (Date.parse(xml) - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000); if(cell.v !== cell.v) cell.v = unescapexml(xml); - else if(cell.v >= 1 && cell.v<60) cell.v = cell.v -1; + else if(cell.v<60) cell.v = cell.v -1; if(!nf || nf == "General") nf = "yyyy-mm-dd"; /* falls through */ case 'Number': @@ -250,6 +250,7 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { sheetname = unescapexml(tmp.Name); cursheet = {}; mergecells = []; + arrayf = []; } break; case 'Table': @@ -759,10 +760,14 @@ function write_sty_xlml(wb, opts)/*:string*/ { } /* TODO */ function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr)/*:string*/{ - if(!cell || cell.v === undefined) return ""; + if(!cell || cell.v == undefined && cell.f == undefined) return ""; var attr = {}; if(cell.f) attr["ss:Formula"] = "=" + escapexml(a1_to_rc(cell.f, addr)); + if(cell.F && cell.F.substr(0, ref.length) == ref) { + var end = decode_cell(cell.F.substr(ref.length + 1)); + attr["ss:ArrayRange"] = "RC:R" + (end.r == addr.r ? "" : "[" + (end.r - addr.r) + "]") + "C" + (end.c == addr.c ? "" : "[" + (end.c - addr.c) + "]"); + } if(ws['!merges']) { var marr = ws['!merges']; @@ -780,9 +785,9 @@ function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr)/*:string*/{ case 'b': t = 'Boolean'; p = (cell.v ? "1" : "0"); break; case 'e': t = 'Error'; p = BErr[cell.v]; break; case 'd': t = 'DateTime'; p = new Date(cell.v).toISOString(); break; - case 's': t = 'String'; p = escapexml(cell.v||""); break; + case 's': t = 'String'; p = escapexml(cell.v||""); break; } - var m = '' + p + ''; + var m = '' + (cell.v != null ? p : "") + ''; return writextag("Cell", m, attr); } diff --git a/bits/76_xls.js b/bits/76_xls.js index 370c8ca..5fff527 100644 --- a/bits/76_xls.js +++ b/bits/76_xls.js @@ -122,6 +122,19 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { if(cell.r + 1 > range.e.r) range.e.r = cell.r + 1; if(cell.c + 1 > range.e.c) range.e.c = cell.c + 1; } + if(options.cellFormula && line.f) { + for(var afi = 0; afi < array_formulae.length; ++afi) { + if(array_formulae[afi][0].s.c > cell.c) continue; + if(array_formulae[afi][0].s.r > cell.r) continue; + if(array_formulae[afi][0].e.c < cell.c) continue; + if(array_formulae[afi][0].e.r < cell.r) continue; + line.F = encode_range(array_formulae[afi][0]); + if(array_formulae[afi][0].s.c != cell.c) delete line.f; + if(array_formulae[afi][0].s.r != cell.r) delete line.f; + if(line.f) line.f = "" + stringify_formula(array_formulae[afi][1], range, cell, supbooks, opts); + break; + } + } if(options.sheetRows && lastcell.r >= options.sheetRows) cell_valid = false; else out[last_cell] = line; }; @@ -274,7 +287,9 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { else cur_sheet = (Directory[s] || {name:""}).name; mergecells = []; objects = []; + array_formulae = []; opts.arrayf = array_formulae; } break; + case 'Number': case 'BIFF2NUM': case 'BIFF2INT': { temp_val = {ixfe: val.ixfe, XF: XFs[val.ixfe], v:val.val, t:'n'}; safe_format_xf(temp_val, options, wb.opts.Date1904); @@ -299,44 +314,43 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { } } break; case 'Formula': { - switch(val.val) { - case 'String': last_formula = val; break; - case 'Array Formula': throw "Array Formula unsupported"; - default: - temp_val = ({v:val.val, ixfe:val.cell.ixfe, t:val.tt}/*:any*/); - temp_val.XF = XFs[temp_val.ixfe]; - if(options.cellFormula) { - var _f = val.formula; - if(_f && _f[0] && _f[0][0] && _f[0][0][0] == 'PtgExp') { - var _fr = _f[0][0][1][0], _fc = _f[0][0][1][1]; - var _fe = encode_cell({r:_fr, c:_fc}); - if(shared_formulae[_fe]) temp_val.f = ""+stringify_formula(val.formula,range,val.cell,supbooks, opts); - else temp_val.F = (out[_fe] || {}).F; - } else temp_val.f = ""+stringify_formula(val.formula,range,val.cell,supbooks, opts); - } - safe_format_xf(temp_val, options, wb.opts.Date1904); - addcell(val.cell, temp_val, options); - last_formula = val; + if(val.val == 'String') { last_formula = val; break; } + temp_val = ({v:val.val, ixfe:val.cell.ixfe, t:val.tt}/*:any*/); + temp_val.XF = XFs[temp_val.ixfe]; + if(options.cellFormula) { + var _f = val.formula; + if(_f && _f[0] && _f[0][0] && _f[0][0][0] == 'PtgExp') { + var _fr = _f[0][0][1][0], _fc = _f[0][0][1][1]; + var _fe = encode_cell({r:_fr, c:_fc}); + if(shared_formulae[_fe]) temp_val.f = ""+stringify_formula(val.formula,range,val.cell,supbooks, opts); + else temp_val.F = (out[_fe] || {}).F; + } else temp_val.f = ""+stringify_formula(val.formula,range,val.cell,supbooks, opts); } + safe_format_xf(temp_val, options, wb.opts.Date1904); + addcell(val.cell, temp_val, options); + last_formula = val; } break; case 'String': { - if(last_formula) { + if(last_formula) { /* technically always true */ last_formula.val = val; - temp_val = ({v:last_formula.val, ixfe:last_formula.cell.ixfe, t:'s'}/*:any*/); + temp_val = ({v:val, ixfe:last_formula.cell.ixfe, t:'s'}/*:any*/); temp_val.XF = XFs[temp_val.ixfe]; - if(options.cellFormula) temp_val.f = ""+stringify_formula(last_formula.formula, range, last_formula.cell, supbooks, opts); + if(options.cellFormula) { + temp_val.f = ""+stringify_formula(last_formula.formula, range, last_formula.cell, supbooks, opts); + } safe_format_xf(temp_val, options, wb.opts.Date1904); addcell(last_formula.cell, temp_val, options); last_formula = null; - } + } else throw new Error("String record expects Formula"); } break; case 'Array': { array_formulae.push(val); - if(options.cellFormula && out[last_cell]) { + var _arraystart = encode_cell(val[0].s); + if(options.cellFormula && out[_arraystart]) { if(!last_formula) break; /* technically unreachable */ - if(!last_cell || !out[last_cell]) break; /* technically unreachable */ - out[last_cell].f = ""+stringify_formula(last_formula.formula, range, last_formula.cell, supbooks, opts); - out[last_cell].F = encode_range(val[0]); + if(!_arraystart || !out[_arraystart]) break; + out[_arraystart].f = ""+stringify_formula(val[1], range, val[0], supbooks, opts); + out[_arraystart].F = encode_range(val[0]); } } break; case 'ShrFmla': { @@ -375,6 +389,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { safe_format_xf(temp_val, options, wb.opts.Date1904); addcell({c:val.c, r:val.r}, temp_val, options); break; + case 'Dimensions': { if(file_depth === 1) range = val; /* TODO: stack */ } break; diff --git a/bits/78_writebiff.js b/bits/78_writebiff.js index 4b67c2c..6e4b439 100644 --- a/bits/78_writebiff.js +++ b/bits/78_writebiff.js @@ -57,20 +57,20 @@ function write_BIFF2LABEL(r, c, val) { } function write_ws_biff_cell(ba/*:BufArray*/, cell/*:Cell*/, R/*:number*/, C/*:number*/, opts) { - switch(cell.t) { + if(cell.v != null) switch(cell.t) { case 'n': if((cell.v == (cell.v|0)) && (cell.v >= 0) && (cell.v < 65536)) write_biff_rec(ba, 0x0002, write_BIFF2INT(R, C, cell.v)); else write_biff_rec(ba, 0x0003, write_BIFF2NUMBER(R,C, cell.v)); - break; - case 'b': case 'e': write_biff_rec(ba, 0x0005, write_BIFF2BERR(R, C, cell.v, cell.t)); break; + return; + case 'b': case 'e': write_biff_rec(ba, 0x0005, write_BIFF2BERR(R, C, cell.v, cell.t)); return; /* TODO: codepage, sst */ case 's': case 'str': write_biff_rec(ba, 0x0004, write_BIFF2LABEL(R, C, cell.v)); - break; - default: write_biff_rec(ba, 0x0001, write_BIFF2Cell(null, R, C)); + return; } + write_biff_rec(ba, 0x0001, write_BIFF2Cell(null, R, C)); } function write_biff_ws(ba/*:BufArray*/, ws/*:Worksheet*/, idx/*:number*/, opts, wb/*:Workbook*/) { diff --git a/bits/80_parseods.js b/bits/80_parseods.js index 71cdbc0..6a6af23 100644 --- a/bits/80_parseods.js +++ b/bits/80_parseods.js @@ -78,6 +78,7 @@ var parse_content_xml = (function() { ctag = parsexmltag(Rn[0], false); q = ({t:ctag['数据类型'] || ctag['value-type'], v:null/*:: , z:null, w:""*/}/*:any*/); if(opts.cellFormula) { + if(ctag.formula) ctag.formula = unescapexml(ctag.formula); if(ctag['number-matrix-columns-spanned'] && ctag['number-matrix-rows-spanned']) { mR = parseInt(ctag['number-matrix-rows-spanned'],10) || 0; mC = parseInt(ctag['number-matrix-columns-spanned'],10) || 0; diff --git a/bits/81_writeods.js b/bits/81_writeods.js index 7d47f3a..cd23d56 100644 --- a/bits/81_writeods.js +++ b/bits/81_writeods.js @@ -27,11 +27,22 @@ var write_content_xml/*:{(wb:any, opts:any):string}*/ = (function() { } if(skip) { o.push(covered_cell_xml); continue; } var ref = encode_cell({r:R, c:C}), cell = ws[ref]; + var fmla = ""; + if(cell && cell.f) { + fmla = ' table:formula="' + escapexml(csf_to_ods_formula(cell.f)) + '"'; + if(cell.F) { + if(cell.F.substr(0, ref.length) == ref) { + var _Fref = decode_range(cell.F); + fmla += ' table:number-matrix-columns-spanned="' + (_Fref.e.c - _Fref.s.c + 1)+ '"'; + fmla += ' table:number-matrix-rows-spanned="' + (_Fref.e.r - _Fref.s.r + 1) + '"'; + } else fmla = ""; + } + } if(cell) switch(cell.t) { - case 'b': o.push(cell_begin + mxml + vt + '"boolean" office:boolean-value="' + (cell.v ? 'true' : 'false') + '">' + p_begin + (cell.v ? 'TRUE' : 'FALSE') + p_end + cell_end); break; - case 'n': o.push(cell_begin + mxml + vt + '"float" office:value="' + cell.v + '">' + p_begin + (cell.w||cell.v) + p_end + cell_end); break; - case 's': case 'str': o.push(cell_begin + mxml + vt + '"string">' + p_begin + escapexml(cell.v) + p_end + cell_end); break; - case 'd': o.push(cell_begin + mxml + vt + '"date" office:date-value="' + (new Date(cell.v).toISOString()) + '">' + p_begin + (cell.w||(new Date(cell.v).toISOString())) + p_end + cell_end); break; + case 'b': o.push(cell_begin + mxml + vt + '"boolean" office:boolean-value="' + (cell.v ? 'true' : 'false') + '"' + fmla + '>' + p_begin + (cell.v ? 'TRUE' : 'FALSE') + p_end + cell_end); break; + case 'n': o.push(cell_begin + mxml + vt + '"float" office:value="' + cell.v + '"' + fmla + '>' + p_begin + (cell.w||cell.v) + p_end + cell_end); break; + case 's': case 'str': o.push(cell_begin + mxml + vt + '"string"' + fmla + '>' + p_begin + escapexml(cell.v) + p_end + cell_end); break; + case 'd': o.push(cell_begin + mxml + vt + '"date" office:date-value="' + (new Date(cell.v).toISOString()) + '"' + fmla + '>' + p_begin + (cell.w||(new Date(cell.v).toISOString())) + p_end + cell_end); break; //case 'e': default: o.push(null_cell_xml); } else o.push(null_cell_xml); @@ -46,7 +57,7 @@ var write_content_xml/*:{(wb:any, opts:any):string}*/ = (function() { var o = [XML_HEADER]; /* 3.1.3.2 */ if(opts.bookType == "fods") o.push(''); - else o.push('\n'); // TODO + else o.push('\n'); // TODO o.push(' \n'); o.push(' \n'); for(var i = 0; i != wb.SheetNames.length; ++i) o.push(write_ws(wb.Sheets[wb.SheetNames[i]], wb, i, opts)); diff --git a/bits/90_utils.js b/bits/90_utils.js index b1f3b89..fccdd0c 100644 --- a/bits/90_utils.js +++ b/bits/90_utils.js @@ -156,9 +156,15 @@ function sheet_to_csv(sheet/*:Worksheet*/, opts/*:?Sheet2CSVOpts*/) { rr = encode_row(R); for(C = r.s.c; C <= r.e.c; ++C) { val = sheet[cols[C] + rr]; - txt = val !== undefined ? ''+format_cell(val) : ""; - for(i = 0, cc = 0; i !== txt.length; ++i) if((cc = txt.charCodeAt(i)) === fs || cc === rs || cc === 34) { - txt = "\"" + txt.replace(qreg, '""') + "\""; break; } + if(val == null) txt = ""; + else if(val.v != null) { + txt = ''+format_cell(val); + for(i = 0, cc = 0; i !== txt.length; ++i) if((cc = txt.charCodeAt(i)) === fs || cc === rs || cc === 34) { + txt = "\"" + txt.replace(qreg, '""') + "\""; break; } + } else if(val.f != null && !val.F) { + txt = '=' + val.f; if(txt.indexOf(",") >= 0) txt = '"' + txt.replace(qreg, '""') + '"'; + } else txt = ""; + /* NOTE: Excel CSV does not support array formulae */ row += (C === r.s.c ? "" : FS) + txt; } out += row + RS; diff --git a/multiformat.lst b/multiformat.lst index 1befc01..64d2900 100644 --- a/multiformat.lst +++ b/multiformat.lst @@ -10,13 +10,19 @@ cell_style_simple .xls .xlsb .xlsx .xml comments_stress_test .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml # yes-csv custom_properties .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml +# no-formula (defined names) defined_names_simple .xls .xlsb .xlsx .xml -#formula_stress_test .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml +# yes-formula +# no-csv (randbetween) note: ODS does not support many XLSX functions +formula_stress_test .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml +# yes-csv formulae_test_simple .xls .xlsb .xlsx .xml hyperlink_stress_test_2011 .xls .xlsb .xlsx .xml #large_strings .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml merge_cells .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml +# no-formula (defined names) named_ranges_2011 .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml +# yes-formula # no-csv (macro serialization in xml) number_format .xls .xlsb .xlsm .xls.xml .xlsb.xml .xlsm.xml number_format_entities .xls .xlsb .xlsx .xml diff --git a/test.js b/test.js index 30b72e5..4c55568 100644 --- a/test.js +++ b/test.js @@ -838,9 +838,7 @@ function seq(end, start) { } describe('roundtrip features', function() { - var bef = (function() { - X = require(modp); - }); + var bef = (function() { X = require(modp); }); if(typeof before != 'undefined') before(bef); else it('before', bef); describe('should parse core properties and custom properties', function() { @@ -907,14 +905,18 @@ describe('roundtrip features', function() { }); }); - describe('xls to xlsx conversions', function() { [ - ['XLS', 'formula_stress_test.xls'], - ['XML', 'formula_stress_test.xls.xml'] + describe('should preserve formulae', function() { [ + ['xlml', paths.fstxml], + ['xlsx', paths.fstxlsx], + ['ods', paths.fstods] ].forEach(function(w) { - it('should be able to write ' + w[0] + ' files', function() { - var xls = X.readFile('./test_files/' + w[1], {cellNF:true}); - X.writeFile(xls, './tmp/' + w[1] + '.xlsx', {bookSST:true}); - X.writeFile(xls, './tmp/' + w[1] + '.xlsb', {bookSST:true}); + it(w[0], function() { + var wb1 = X.readFile(w[1], {cellFormula:true}); + if(w[0] == 'ods') X.writeFile(wb1, "./tmp/_.ods", {bookType:"ods"}); + var wb2 = X.read(X.write(wb1, {bookType:w[0], type:"buffer"}), {cellFormula:true, type:"buffer"}); + wb1.SheetNames.forEach(function(n) { + assert.equal( X.utils.sheet_to_formulae(wb1.Sheets[n]).sort().join("\n"), X.utils.sheet_to_formulae(wb2.Sheets[n]).sort().join("\n") ); + }); }); }); }); @@ -1209,7 +1211,7 @@ describe('encryption', function() { describe('multiformat tests', function() { var mfopts = opts; var mft = fs.readFileSync('multiformat.lst','utf-8').split("\n"); -var csv = true; +var csv = true, formulae = false; mft.forEach(function(x) { if(x[0]!="#") describe('MFT ' + x, function() { var fil = {}, f = [], r = x.split(/\s+/); @@ -1239,10 +1241,20 @@ mft.forEach(function(x) { cmparr(f.map(function(x) { return X.utils.sheet_to_csv(x.Sheets[name]); })); }); } : null); + it('should have the same formulae', formulae ? function() { + cmparr(f.map(function(x) { return x.SheetNames; })); + var names = f[0].SheetNames; + names.forEach(function(name) { + cmparr(f.map(function(x) { return X.utils.sheet_to_formulae(x.Sheets[name]).sort(); })); + }); + } : null); + }); else x.split(/\s+/).forEach(function(w) { switch(w) { case "no-csv": csv = false; break; case "yes-csv": csv = true; break; + case "no-formula": formulae = false; break; + case "yes-formula": formulae = true; break; }}); }); }); diff --git a/test_files b/test_files index 8ab3308..e0b7060 160000 --- a/test_files +++ b/test_files @@ -1 +1 @@ -Subproject commit 8ab3308622f83ac47a0ee584d61e11357b01f925 +Subproject commit e0b7060b10ef972ad48869faa4efffd089e1a428 diff --git a/tests.lst b/tests.lst index 1e096d3..691e217 100644 --- a/tests.lst +++ b/tests.lst @@ -29,17 +29,6 @@ sushi.xlsb text_and_numbers.xlsb time_stress_test_1.xlsb.pending xlsx-stream-d-date-cell.xlsb -2013/apachepoi_29982.xls.xlsb -2013/apachepoi_43251.xls.xlsb -2013/apachepoi_44593.xls.xlsb ## xlsb loop timeout -2013/apachepoi_44643.xls.xlsb -2013/apachepoi_44958.xls.xlsb -2013/apachepoi_46136-NoWarnings.xls.xlsb -2013/apachepoi_48968.xls.xlsb -2013/apachepoi_50939.xls.xlsb -2013/apachepoi_54016.xls.xlsb -2013/apachepoi_ReadOnlyRecommended.xls.xlsb -2013/apachepoi_testArraysAndTables.xls.xlsb AutoFilter.xlsx ErrorTypes.xlsx LONumbers-2010.xlsx @@ -199,6 +188,7 @@ apachepoi_DataTableCities.xlsx apachepoi_DataValidationEvaluations.xlsx apachepoi_DataValidations-49244.xlsx # apachepoi_DateFormatTests.xlsx # xlml +apachepoi_DateFormatTests.xlsx.xlsx apachepoi_ElapsedFormatTests.xlsx apachepoi_ExcelTables.xlsx apachepoi_ForShifting.xlsx @@ -249,6 +239,7 @@ apachepoi_WithTwoCharts.xlsx apachepoi_WithVariousData.xlsx apachepoi_XSSFSheet.copyRows.xlsx apachepoi_atp.xlsx +apachepoi_bug60858.xlsx apachepoi_chartTitle_noTitle.xlsx apachepoi_chartTitle_withTitle.xlsx apachepoi_chart_sheet.xlsx.pending @@ -362,6 +353,8 @@ openpyxl_r_null_file.xlsx.pending phonetic_text.xlsx pivot_table_named_range.xlsx rich_text_stress.xlsx +roo-xls_numbers1.xlsx +roo-xls_type_excel.xlsx roo_1900_base.xlsx roo_1904_base.xlsx roo_Bibelbund.xlsx @@ -404,6 +397,44 @@ roo_whitespace.xlsx roo_x000D.xlsx roo_zero-padded-number.xlsx smart_tags_2007.xlsx +spout-xlsx_attack_billion_laughs.xlsx +spout-xlsx_attack_quadratic_blowup.xlsx +# spout-xlsx_file_corrupted.xlsx +spout-xlsx_file_with_no_sheets_in_workbook_xml.xlsx +# spout-xlsx_file_with_sheet_xml_not_matching_content_types.xlsx +spout-xlsx_one_sheet_with_inline_strings.xlsx +spout-xlsx_one_sheet_with_invalid_xml_characters.xlsx +spout-xlsx_one_sheet_with_shared_multiline_strings.xlsx +spout-xlsx_one_sheet_with_shared_strings.xlsx +spout-xlsx_one_sheet_with_shared_strings_containing_text_and_hyperlink_in_same_cell.xlsx +spout-xlsx_one_sheet_with_shared_strings_missing_unique_count.xlsx +spout-xlsx_one_sheet_with_shared_strings_missing_unique_count_and_count.xlsx +spout-xlsx_sheet_with_all_cell_types.xlsx +spout-xlsx_sheet_with_custom_date_formats_and_no_apply_number_format.xlsx +spout-xlsx_sheet_with_dates_and_times.xlsx +spout-xlsx_sheet_with_different_numeric_value_dates.xlsx +spout-xlsx_sheet_with_different_numeric_value_times.xlsx +spout-xlsx_sheet_with_dimensions_and_empty_cells.xlsx +spout-xlsx_sheet_with_empty_cells.xlsx +spout-xlsx_sheet_with_empty_rows_and_missing_row_index.xlsx +spout-xlsx_sheet_with_empty_shared_string.xlsx +spout-xlsx_sheet_with_formulas.xlsx +spout-xlsx_sheet_with_lots_of_shared_strings.xlsx +spout-xlsx_sheet_with_missing_cell_reference.xlsx +spout-xlsx_sheet_with_no_cells.xlsx +spout-xlsx_sheet_with_no_shared_strings_file.xlsx +spout-xlsx_sheet_with_prefixed_xml_files.xlsx +spout-xlsx_sheet_with_preserve_space_shared_strings.xlsx +spout-xlsx_sheet_with_pronunciation.xlsx +spout-xlsx_sheet_with_same_numeric_value_date_formatted_differently.xlsx +spout-xlsx_sheet_with_untrimmed_inline_strings.xlsx +spout-xlsx_sheet_with_zeros_in_row.xlsx +spout-xlsx_sheet_without_dimensions_and_empty_cells.xlsx +spout-xlsx_sheet_without_dimensions_but_spans_and_empty_cells.xlsx +spout-xlsx_two_sheets_with_custom_names.xlsx +spout-xlsx_two_sheets_with_inline_strings.xlsx +spout-xlsx_two_sheets_with_shared_strings.xlsx +spout-xlsx_two_sheets_with_sheets_definition_in_reverse_order.xlsx spreadsheet-parsexlsx_Test.xlsx spreadsheet-parsexlsx_bug-10.xlsx spreadsheet-parsexlsx_bug-11.xlsx @@ -426,7 +457,7 @@ spreadsheet-parsexlsx_bug-5.xlsx spreadsheet-parsexlsx_bug-57.xlsx spreadsheet-parsexlsx_bug-6-2.xlsx spreadsheet-parsexlsx_bug-6.xlsx -spreadsheet-parsexlsx_bug-61.xlsx +# spreadsheet-parsexlsx_bug-61.xlsx spreadsheet-parsexlsx_bug-7.xlsx spreadsheet-parsexlsx_bug-8.xlsx spreadsheet-parsexlsx_bug-lock.xlsx @@ -469,9 +500,9 @@ hyperlink_no_rels.xlsm number_format.xlsm number_format_russian.xlsm numfmt_1_russian.xlsm -openpyxl_r_vba+comments.xlsm -openpyxl_r_vba-comments-saved.xlsm -openpyxl_r_vba-test.xlsm +# openpyxl_r_vba+comments.xlsm +# openpyxl_r_vba-comments-saved.xlsm +# openpyxl_r_vba-test.xlsm # pivot_table_test.xlsm # xlml roo_1900_base.xlsm roo_1904_base.xlsm @@ -507,6 +538,8 @@ formula_stress_test.ods merge_cells.ods number_format.ods rich_text_stress.ods +roo-xls_numbers1.ods +roo-xls_type_excel.ods roo_Bibelbund.ods roo_Bibelbund1.ods roo_advanced_header.ods @@ -534,6 +567,32 @@ roo_time-test.ods roo_type_excel.ods roo_type_excelx.ods roo_whitespace.ods +spout-ods_attack_billion_laughs.ods +spout-ods_attack_quadratic_blowup.ods +# spout-ods_file_corrupted.ods +spout-ods_file_generated_by_excel_2010_windows.ods +spout-ods_file_generated_by_excel_office_online.ods +spout-ods_file_generated_by_libre_office.ods +spout-ods_one_sheet_with_strings.ods +spout-ods_sheet_with_all_cell_types.ods +spout-ods_sheet_with_dates_and_times.ods +spout-ods_sheet_with_empty_cells.ods +spout-ods_sheet_with_empty_rows.ods +spout-ods_sheet_with_formulas.ods +spout-ods_sheet_with_hyperlinks.ods +spout-ods_sheet_with_inline_font_formatting.ods +spout-ods_sheet_with_invalid_date_time.ods.pending +spout-ods_sheet_with_multiline_string.ods +spout-ods_sheet_with_no_cells.ods +spout-ods_sheet_with_number_columns_repeated.ods +spout-ods_sheet_with_number_rows_repeated.ods +spout-ods_sheet_with_only_one_cell.ods +spout-ods_sheet_with_undefined_value_type.ods +spout-ods_sheet_with_untrimmed_strings.ods +spout-ods_sheet_with_various_spaces.ods +spout-ods_sheet_with_zeros_in_row.ods +spout-ods_two_sheets_with_custom_names.ods +spout-ods_two_sheets_with_strings.ods sushi.ods biff5/NumberFormatCondition.xls biff5/RkNumber.xls @@ -612,6 +671,7 @@ apachepoi_33082.xls apachepoi_34775.xls apachepoi_35564.xls apachepoi_35565.xls +# apachepoi_35897-type4.xls ## password apachepoi_36947.xls apachepoi_37376.xls apachepoi_37630.xls @@ -649,6 +709,7 @@ apachepoi_44840.xls apachepoi_44861.xls apachepoi_44891.xls apachepoi_44958.xls +apachepoi_44958_1.xls apachepoi_45129.xls apachepoi_45290.xls apachepoi_45322.xls @@ -659,6 +720,7 @@ apachepoi_45538_classic_Footer.xls apachepoi_45538_classic_Header.xls apachepoi_45538_form_Footer.xls apachepoi_45538_form_Header.xls +apachepoi_45565.xls apachepoi_45672.xls apachepoi_45720.xls apachepoi_45761.xls @@ -669,12 +731,14 @@ apachepoi_46137.xls apachepoi_46250.xls apachepoi_46368.xls apachepoi_46445.xls +apachepoi_46515.xls apachepoi_46670_http.xls apachepoi_46670_local.xls apachepoi_46670_ref_airline.xls apachepoi_46904.xls apachepoi_47034.xls apachepoi_47154.xls +apachepoi_47245_test.xls apachepoi_47251.xls apachepoi_47251_1.xls apachepoi_47701.xls @@ -690,6 +754,7 @@ apachepoi_49096.xls apachepoi_49185.xls apachepoi_49219.xls apachepoi_49237.xls +apachepoi_49423.xls apachepoi_49524.xls apachepoi_49529.xls apachepoi_49581.xls @@ -717,6 +782,7 @@ apachepoi_51832.xls.pending apachepoi_52527.xls apachepoi_52575_main.xls apachepoi_52575_source.xls +apachepoi_53109.xls apachepoi_53404.xls apachepoi_53433.xls apachepoi_53446.xls @@ -730,24 +796,44 @@ apachepoi_54206.xls apachepoi_54500.xls apachepoi_54686_fraction_formats.xls apachepoi_55341_CellStyleBorder.xls +apachepoi_55668.xls apachepoi_55906-MultiSheetRefs.xls +apachepoi_55982.xls apachepoi_56325.xls +apachepoi_56325a.xls apachepoi_56450.xls apachepoi_56482.xls apachepoi_56563a.xls apachepoi_56563b.xls apachepoi_56737.xls +apachepoi_57003-FixedFunctionTestCaseData.xls +apachepoi_57074.xls +apachepoi_57163.xls +apachepoi_57231_MixedGasReport.xls.pending +apachepoi_57456.xls.pending +apachepoi_57798.xls +apachepoi_57925.xls +apachepoi_59074.xls +apachepoi_59264.xls +apachepoi_59830.xls +apachepoi_59858.xls +apachepoi_60273.xls +# apachepoi_60284.xls apachepoi_AbnormalSharedFormulaFlag.xls apachepoi_AreaErrPtg.xls apachepoi_BOOK_in_capitals.xls +apachepoi_Basic_Expense_Template_2011.xls apachepoi_CodeFunctionTestCaseData.xls apachepoi_ColumnStyle1dp.xls apachepoi_ColumnStyle1dpColoured.xls apachepoi_ColumnStyleNone.xls apachepoi_ComplexFunctionTestCaseData.xls +apachepoi_ConditionalFormattingSamples.xls apachepoi_ContinueRecordProblem.xls apachepoi_DBCSHeader.xls apachepoi_DBCSSheetName.xls +apachepoi_DGet.xls +apachepoi_DStar.xls apachepoi_DateFormats.xls apachepoi_DeltaFunctionTestCaseData.xls apachepoi_DrawingAndComments.xls @@ -758,6 +844,7 @@ apachepoi_ErrPtg.xls apachepoi_FactDoubleFunctionTestCaseData.xls apachepoi_ForShifting.xls apachepoi_FormatChoiceTests.xls +apachepoi_FormatKM.xls apachepoi_Formatting.xls apachepoi_FormulaEvalTestData.xls apachepoi_FormulaRefs.xls @@ -775,6 +862,7 @@ apachepoi_IrrNpvTestCaseData.xls apachepoi_MRExtraLines.xls apachepoi_MatchFunctionTestCaseData.xls apachepoi_MissingBits.xls +apachepoi_NewStyleConditionalFormattings.xls apachepoi_NoGutsRecords.xls apachepoi_OddStyleRecord.xls apachepoi_PercentPtg.xls @@ -791,7 +879,9 @@ apachepoi_SheetWithDrawing.xls apachepoi_ShrinkToFit.xls apachepoi_Simple.xls apachepoi_SimpleChart.xls +apachepoi_SimpleMacro.xls apachepoi_SimpleMultiCell.xls +apachepoi_SimpleScatterChart.xls apachepoi_SimpleWithAutofilter.xls apachepoi_SimpleWithChoose.xls apachepoi_SimpleWithColours.xls @@ -811,6 +901,7 @@ apachepoi_StringContinueRecords.xls apachepoi_StringFormulas.xls apachepoi_SubtotalsNested.xls apachepoi_TestRandBetween.xls +apachepoi_Themes2.xls apachepoi_TwoSheetsNoneHidden.xls apachepoi_TwoSheetsOneHidden.xls # apachepoi_UncalcedRecord.xls # xlml @@ -831,10 +922,13 @@ apachepoi_WithTwoHyperLinks.xls apachepoi_WrongFormulaRecordType.xls apachepoi_XRefCalc.xls apachepoi_XRefCalcData.xls +apachepoi_ar.org.apsme.www_Form%20Inscripcion%20Curso%20NO%20Socios.xls +apachepoi_at.gv.land-oberoesterreich.www_cps_rde_xbcr_SID-4A1B954F-5C07F98E_ooe_stat_download_bp10.xls apachepoi_atp.xls apachepoi_blankworkbook.xls apachepoi_bug_42794.xls apachepoi_colwidth.xls +apachepoi_com.aida-tour.www_SPO_files_maldives%20august%20october.xls apachepoi_comments.xls apachepoi_countblankExamples.xls apachepoi_countifExamples.xls @@ -861,24 +955,32 @@ apachepoi_excel_with_embeded.xls apachepoi_excelant.xls.pending apachepoi_externalFunctionExample.xls # apachepoi_finance.xls # xlml +apachepoi_florida_data.ashx.xls apachepoi_intercept.xls apachepoi_mirrTest.xls apachepoi_missingFuncs44675.xls apachepoi_mortgage-calculation.xls apachepoi_multibookFormulaA.xls apachepoi_multibookFormulaB.xls +apachepoi_named-cell-in-formula-test.xls +apachepoi_named-cell-test.xls apachepoi_namedinput.xls apachepoi_noHeaderFooter47244.xls apachepoi_ole2-embedding.xls apachepoi_overlapSharedFormula.xls apachepoi_password.xls.pending apachepoi_rank.xls +apachepoi_resize_compare.xls apachepoi_rk.xls apachepoi_shared_formulas.xls apachepoi_sumifformula.xls # apachepoi_sumifs.xls # xlml apachepoi_templateExcelWithAutofilter.xls apachepoi_testArraysAndTables.xls +apachepoi_testEXCEL_3.xls +apachepoi_testEXCEL_4.xls +apachepoi_testEXCEL_5.xls +apachepoi_testEXCEL_95.xls apachepoi_testNames.xls apachepoi_testRRaC.xls apachepoi_testRVA.xls @@ -1091,40 +1193,41 @@ pyExcelerator_mini-mini.xls pyExcelerator_mini.xls pyExcelerator_oo14.xls rich_text_stress.xls -roo_1900_base.xls -roo_1904_base.xls -roo_Bibelbund.xls -roo_bad_excel_date.xls -roo_bbu.xls -roo_boolean.xls -roo_borders.xls -roo_bug-row-column-fixnum-float.xls -roo_comments.xls -roo_datetime.xls -roo_datetime_floatconv.xls -roo_emptysheets.xls -roo_false_encoding.xls -roo_formula.xls -roo_formula_parse_error.xls -roo_link.xls -roo_matrix.xls -roo_named_cells.xls -roo_numbers1.xls -roo_only_one_sheet.xls -roo_paragraph.xls -roo_prova.xls -roo_simple_spreadsheet.xls -roo_simple_spreadsheet_from_italo.xls -roo_style.xls -roo_time-test.xls -roo_type_excelx.xls -roo_type_openoffice.xls +roo-xls_1900_base.xls +roo-xls_1904_base.xls +roo-xls_Bibelbund.xls +roo-xls_bad_excel_date.xls +roo-xls_bbu.xls +roo-xls_boolean.xls +roo-xls_borders.xls +roo-xls_bug-row-column-fixnum-float.xls +roo-xls_comments.xls +roo-xls_datetime.xls +roo-xls_datetime_floatconv.xls +roo-xls_emptysheets.xls +roo-xls_false_encoding.xls +roo-xls_formula.xls +roo-xls_formula_parse_error.xls +roo-xls_link.xls +roo-xls_matrix.xls +roo-xls_named_cells.xls +roo-xls_numbers1.xls +roo-xls_only_one_sheet.xls +roo-xls_paragraph.xls +roo-xls_prova.xls +roo-xls_simple_spreadsheet.xls +roo-xls_simple_spreadsheet_from_italo.xls +roo-xls_style.xls +roo-xls_time-test.xls +roo-xls_type_excelx.xls +roo-xls_type_openoffice.xls roo_whitespace.xls smart_tags_2007.xls sushi.xls text_and_numbers.xls write.xls xlrd_Formate.xls +# xlrd_biff4_no_format_no_window2.xls xlrd_formula_test_names.xls xlrd_formula_test_sjmachin.xls xlrd_issue20.xls @@ -1135,7 +1238,6 @@ xlrd_xf_class.xls xlsx-stream-d-date-cell.xls AutoFilter.xml BlankSheetTypes.xml -ErrorTypes.xml LONumbers-2010.xls.xml LONumbers-2010.xlsx.xml LONumbers-2011.xls.xml @@ -1145,6 +1247,7 @@ NumberFormatCondition.xml RkNumber.xls.xml RkNumber.xlsb.xml RkNumber.xlsx.xml +apachepoi_SampleSS.xml calendar_stress_test.xml.pending cell_style_simple.xml comments_stress_test.xls.xml @@ -1197,26 +1300,28 @@ protect_stress_test_xml.xml rich_text_stress.xls.xml rich_text_stress.xlsb.xml rich_text_stress.xlsx.xml -roo_Bibelbund.xml -roo_bbu.xml -roo_boolean.xml -roo_borders.xml -roo_bug-row-column-fixnum-float.xml -roo_datetime.xml -roo_datetime_floatconv.xml -roo_emptysheets.xml -roo_excel2003.xml -roo_false_encoding.xml -roo_formula.xml -roo_formula_parse_error.xml -roo_numbers1.xml -roo_only_one_sheet.xml -roo_paragraph.xml -roo_simple_spreadsheet.xml -roo_simple_spreadsheet_from_italo.xml -roo_style.xml -roo_time-test.xml -roo_whitespace.xml +roo-xls_Bibelbund.xml +roo-xls_bbu.xml +roo-xls_boolean.xml +roo-xls_borders.xml +roo-xls_bug-row-column-fixnum-float.xml +roo-xls_datetime.xml +roo-xls_datetime_floatconv.xml +roo-xls_emptysheets.xml +roo-xls_excel2003.xml +roo-xls_excel2003_namespace.xml +roo-xls_false_encoding.xml +roo-xls_formula.xml +roo-xls_formula_parse_error.xml +roo-xls_numbers1.xml +roo-xls_only_one_sheet.xml +roo-xls_paragraph.xml +roo-xls_simple_spreadsheet.xml +roo-xls_simple_spreadsheet_from_italo.xml +roo-xls_style.xml +roo-xls_time-test.xml +roo-xls_whitespace.xml +# roo_sheet1.xml smart_tags_2007.xml sushi.xml text_and_numbers.xml @@ -1229,3 +1334,15 @@ xlsx-stream-d-date-cell.xlsx.xml 2011/apachepoi_styles.xlsx.xml 2011/openpyxl_r_conditional-formatting.xlsx.xls 2011/roo_file_item_error.xlsx.xml +2013/apachepoi_29982.xls.xlsb +2013/apachepoi_43251.xls.xlsb +2013/apachepoi_44593.xls.xlsb ## xlsb loop timeout +2013/apachepoi_44643.xls.xlsb +2013/apachepoi_44958.xls.xlsb +2013/apachepoi_46136-NoWarnings.xls.xlsb +2013/apachepoi_48968.xls.xlsb +2013/apachepoi_50939.xls.xlsb +2013/apachepoi_54016.xls.xlsb +2013/apachepoi_ReadOnlyRecommended.xls.xlsb +2013/apachepoi_testArraysAndTables.xls.xlsb + diff --git a/tests/write.js b/tests/write.js index 73a267b..08cff7a 100644 --- a/tests/write.js +++ b/tests/write.js @@ -79,14 +79,32 @@ var ws = sheet_from_array_of_arrays(data); wb.SheetNames.push(ws_name); wb.Sheets[ws_name] = ws; +/* TEST: simple formula */ +ws['C1'].f = "A1+B1"; +ws['C2'] = {t:'n', f:"A1+B1"}; + +/* TEST: single-cell array formula */ +ws['D1'] = {t:'n', f:"SUM(A1:C1*A1:C1)", F:"D1:D1"}; + +/* TEST: multi-cell array formula */ +ws['E1'] = {t:'n', f:"TRANSPOSE(A1:D1)", F:"E1:E4"}; +ws['E2'] = {t:'n', F:"E1:E4"}; +ws['E3'] = {t:'n', F:"E1:E4"}; +ws['E4'] = {t:'n', F:"E1:E4"}; +ws["!ref"] = "A1:E4"; + /* TEST: column widths */ ws['!cols'] = wscols; +console.log("JSON Data: "); console.log(XLSX.utils.sheet_to_json(ws, {header:1})); + + /* write file */ -XLSX.writeFile(wb, 'sheetjs.xlsx'); +XLSX.writeFile(wb, 'sheetjs.xlsx', {bookSST:true}); XLSX.writeFile(wb, 'sheetjs.xlsm'); -XLSX.writeFile(wb, 'sheetjs.xlsb'); -XLSX.writeFile(wb, 'sheetjs.xls', {bookType:'biff2'}); +XLSX.writeFile(wb, 'sheetjs.xlsb'); // no formula +XLSX.writeFile(wb, 'sheetjs.xls', {bookType:'biff2'}); // no formula +XLSX.writeFile(wb, 'sheetjs.xml.xls', {bookType:'xlml'}); XLSX.writeFile(wb, 'sheetjs.ods'); XLSX.writeFile(wb, 'sheetjs.fods'); XLSX.writeFile(wb, 'sheetjs.csv'); @@ -96,6 +114,7 @@ XLSX.readFile('sheetjs.xlsx'); XLSX.readFile('sheetjs.xlsm'); XLSX.readFile('sheetjs.xlsb'); XLSX.readFile('sheetjs.xls'); +XLSX.readFile('sheetjs.xml.xls'); XLSX.readFile('sheetjs.ods'); XLSX.readFile('sheetjs.fods'); //XLSX.readFile('sheetjs.csv'); diff --git a/xlsx.flow.js b/xlsx.flow.js index 3f56588..bff77fc 100644 --- a/xlsx.flow.js +++ b/xlsx.flow.js @@ -422,7 +422,18 @@ function hashq(str/*:string*/)/*:string*/ { return o; } function rnd(val/*:number*/, d/*:number*/)/*:string*/ { var dd = Math.pow(10,d); return ""+(Math.round(val * dd)/dd); } -function dec(val/*:number*/, d/*:number*/)/*:number*/ { return Math.round((val-Math.floor(val))*Math.pow(10,d)); } +function dec(val/*:number*/, d/*:number*/)/*:number*/ { + if (d < ('' + Math.round((val-Math.floor(val))*Math.pow(10,d))).length) { + return 0; + } + return Math.round((val-Math.floor(val))*Math.pow(10,d)); +} +function carry(val/*:number*/, d/*:number*/)/*:number*/ { + if (d < ('' + Math.round((val-Math.floor(val))*Math.pow(10,d))).length) { + return 1; + } + return 0; +} function flr(val/*:number*/)/*:string*/ { if(val < 2147483647 && val > -2147483648) return ""+(val >= 0 ? (val|0) : (val-1|0)); return ""+Math.floor(val); } function write_num_flt(type/*:string*/, fmt/*:string*/, val/*:number*/)/*:string*/ { if(type.charCodeAt(0) === 40 && !fmt.match(closeparen)) { @@ -444,8 +455,7 @@ function write_num_flt(type/*:string*/, fmt/*:string*/, val/*:number*/)/*:string if((r = fmt.match(frac1))) return write_num_f1(r, aval, sign); if(fmt.match(/^#+0+$/)) return sign + pad0r(aval,fmt.length - fmt.indexOf("0")); if((r = fmt.match(dec1))) { - // $FlowIgnore - o = rnd(val, r[1].length).replace(/^([^\.]+)$/,"$1."+r[1]).replace(/\.$/,"."+r[1]).replace(/\.(\d*)$/,function($$, $1) { return "." + $1 + fill("0", r[1].length-$1.length); }); + o = rnd(val, r[1].length).replace(/^([^\.]+)$/,"$1."+r[1]).replace(/\.$/,"."+r[1]).replace(/\.(\d*)$/,function($$, $1) { return "." + $1 + fill("0", /*::(*/r/*::||[""])*/[1].length-$1.length); }); return fmt.indexOf("0.") !== -1 ? o : o.replace(/^0\./,"."); } fmt = fmt.replace(/^#+([0.])/, "$1"); @@ -454,7 +464,7 @@ function write_num_flt(type/*:string*/, fmt/*:string*/, val/*:number*/)/*:string } if((r = fmt.match(/^#,##0(\.?)$/))) return sign + commaify(pad0r(aval,0)); if((r = fmt.match(/^#,##0\.([#0]*0)$/))) { - return val < 0 ? "-" + write_num_flt(type, fmt, -val) : commaify(""+(Math.floor(val))) + "." + pad0(dec(val, r[1].length),r[1].length); + return val < 0 ? "-" + write_num_flt(type, fmt, -val) : commaify(""+(Math.floor(val) + carry(val, r[1].length))) + "." + pad0(dec(val, r[1].length),r[1].length); } if((r = fmt.match(/^#,#*,#0/))) return write_num_flt(type,fmt.replace(/^#,#*,/,""),val); if((r = fmt.match(/^([0#]+)(\\?-([0#]+))+$/))) { @@ -8201,8 +8211,18 @@ function ods_to_csf_formula(f/*:string*/)/*:string*/ { f = f.substr(1); if(f.charCodeAt(0) == 61) f = f.substr(1); } + f = f.replace(/COM\.MICROSOFT\./g, ""); /* Part 3 Section 5.8 References */ - return f.replace(/\[((?:\.[A-Z]+[0-9]+)(?::\.[A-Z]+[0-9]+)?)\]/g, "$1").replace(/\./g, ""); + f = f.replace(/\[((?:\.[A-Z]+[0-9]+)(?::\.[A-Z]+[0-9]+)?)\]/g, function($$, $1) { return $1.replace(/\./g,""); }); + /* TODO: something other than this */ + f = f.replace(/\[.(#[A-Z]*[?!])\]/g, "$1"); + return f.replace(/[;~]/g,",").replace(/\|/g,";"); +} + +function csf_to_ods_formula(f/*:string*/)/*:string*/ { + var o = "of:=" + f.replace(crefregex, "$1[.$2$3$4$5]").replace(/\]:\[/g,":"); + /* TODO: something other than this */ + return o.replace(/;/g, "|").replace(/,/g,";"); } var strs = {}; // shared strings var _ssfopts = {}; // spreadsheet formatting options @@ -8390,7 +8410,7 @@ function write_ws_xml_cols(ws, cols)/*:string*/ { } function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { - if(cell.v === undefined || cell.t === 'z') return ""; + if(cell.v === undefined && cell.f === undefined || cell.t === 'z') return ""; var vv = ""; var oldt = cell.t, oldv = cell.v; switch(cell.t) { @@ -8416,7 +8436,7 @@ function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { case 'd': o.t = "d"; break; case 'b': o.t = "b"; break; case 'e': o.t = "e"; break; - default: + default: if(cell.v == null) { delete cell.t; break; } if(opts.bookSST) { v = writetag('v', ''+get_sst_id(opts.Strings, cell.v)); o.t = "s"; break; @@ -8424,6 +8444,10 @@ function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { o.t = "str"; break; } if(cell.t != oldt) { cell.t = oldt; cell.v = oldv; } + if(cell.f) { + var ff = cell.F && cell.F.substr(0, ref.length) == ref ? {t:"array", ref:cell.F} : null; + v = writextag('f', escapexml(cell.f), ff) + (cell.v != null ? v : ""); + } return writextag('c', v, o); } @@ -8479,7 +8503,8 @@ return function parse_ws_xml_data(sdata, s, opts, guess) { if((cref=d.match(match_v))!= null && /*::cref != null && */cref[1] !== '') p.v=unescapexml(cref[1]); if(opts.cellFormula) { if((cref=d.match(match_f))!= null && /*::cref != null && */cref[1] !== '') { - p.f=unescapexml(utf8read(cref[1])); + /* TODO: match against XLSXFutureFunctions */ + p.f=unescapexml(utf8read(cref[1])).replace(/_xlfn\./,""); if(/*::cref != null && cref[0] != null && */cref[0].indexOf('t="array"') > -1) { p.F = (d.match(refregex)||[])[1]; if(p.F.indexOf(":") > -1) arrayf.push([safe_decode_range(p.F), p.F]); @@ -9842,7 +9867,7 @@ function parse_xlml_data(xml, ss, data, cell/*:any*/, base, styles, csty, row, a case 'DateTime': cell.v = (Date.parse(xml) - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000); if(cell.v !== cell.v) cell.v = unescapexml(xml); - else if(cell.v >= 1 && cell.v<60) cell.v = cell.v -1; + else if(cell.v<60) cell.v = cell.v -1; if(!nf || nf == "General") nf = "yyyy-mm-dd"; /* falls through */ case 'Number': @@ -9984,6 +10009,7 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { sheetname = unescapexml(tmp.Name); cursheet = {}; mergecells = []; + arrayf = []; } break; case 'Table': @@ -10493,10 +10519,14 @@ function write_sty_xlml(wb, opts)/*:string*/ { } /* TODO */ function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr)/*:string*/{ - if(!cell || cell.v === undefined) return ""; + if(!cell || cell.v == undefined && cell.f == undefined) return ""; var attr = {}; if(cell.f) attr["ss:Formula"] = "=" + escapexml(a1_to_rc(cell.f, addr)); + if(cell.F && cell.F.substr(0, ref.length) == ref) { + var end = decode_cell(cell.F.substr(ref.length + 1)); + attr["ss:ArrayRange"] = "RC:R" + (end.r == addr.r ? "" : "[" + (end.r - addr.r) + "]") + "C" + (end.c == addr.c ? "" : "[" + (end.c - addr.c) + "]"); + } if(ws['!merges']) { var marr = ws['!merges']; @@ -10514,9 +10544,9 @@ function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr)/*:string*/{ case 'b': t = 'Boolean'; p = (cell.v ? "1" : "0"); break; case 'e': t = 'Error'; p = BErr[cell.v]; break; case 'd': t = 'DateTime'; p = new Date(cell.v).toISOString(); break; - case 's': t = 'String'; p = escapexml(cell.v||""); break; + case 's': t = 'String'; p = escapexml(cell.v||""); break; } - var m = '' + p + ''; + var m = '' + (cell.v != null ? p : "") + ''; return writextag("Cell", m, attr); } @@ -10699,6 +10729,19 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { if(cell.r + 1 > range.e.r) range.e.r = cell.r + 1; if(cell.c + 1 > range.e.c) range.e.c = cell.c + 1; } + if(options.cellFormula && line.f) { + for(var afi = 0; afi < array_formulae.length; ++afi) { + if(array_formulae[afi][0].s.c > cell.c) continue; + if(array_formulae[afi][0].s.r > cell.r) continue; + if(array_formulae[afi][0].e.c < cell.c) continue; + if(array_formulae[afi][0].e.r < cell.r) continue; + line.F = encode_range(array_formulae[afi][0]); + if(array_formulae[afi][0].s.c != cell.c) delete line.f; + if(array_formulae[afi][0].s.r != cell.r) delete line.f; + if(line.f) line.f = "" + stringify_formula(array_formulae[afi][1], range, cell, supbooks, opts); + break; + } + } if(options.sheetRows && lastcell.r >= options.sheetRows) cell_valid = false; else out[last_cell] = line; }; @@ -10851,7 +10894,9 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { else cur_sheet = (Directory[s] || {name:""}).name; mergecells = []; objects = []; + array_formulae = []; opts.arrayf = array_formulae; } break; + case 'Number': case 'BIFF2NUM': case 'BIFF2INT': { temp_val = {ixfe: val.ixfe, XF: XFs[val.ixfe], v:val.val, t:'n'}; safe_format_xf(temp_val, options, wb.opts.Date1904); @@ -10876,44 +10921,43 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { } } break; case 'Formula': { - switch(val.val) { - case 'String': last_formula = val; break; - case 'Array Formula': throw "Array Formula unsupported"; - default: - temp_val = ({v:val.val, ixfe:val.cell.ixfe, t:val.tt}/*:any*/); - temp_val.XF = XFs[temp_val.ixfe]; - if(options.cellFormula) { - var _f = val.formula; - if(_f && _f[0] && _f[0][0] && _f[0][0][0] == 'PtgExp') { - var _fr = _f[0][0][1][0], _fc = _f[0][0][1][1]; - var _fe = encode_cell({r:_fr, c:_fc}); - if(shared_formulae[_fe]) temp_val.f = ""+stringify_formula(val.formula,range,val.cell,supbooks, opts); - else temp_val.F = (out[_fe] || {}).F; - } else temp_val.f = ""+stringify_formula(val.formula,range,val.cell,supbooks, opts); - } - safe_format_xf(temp_val, options, wb.opts.Date1904); - addcell(val.cell, temp_val, options); - last_formula = val; + if(val.val == 'String') { last_formula = val; break; } + temp_val = ({v:val.val, ixfe:val.cell.ixfe, t:val.tt}/*:any*/); + temp_val.XF = XFs[temp_val.ixfe]; + if(options.cellFormula) { + var _f = val.formula; + if(_f && _f[0] && _f[0][0] && _f[0][0][0] == 'PtgExp') { + var _fr = _f[0][0][1][0], _fc = _f[0][0][1][1]; + var _fe = encode_cell({r:_fr, c:_fc}); + if(shared_formulae[_fe]) temp_val.f = ""+stringify_formula(val.formula,range,val.cell,supbooks, opts); + else temp_val.F = (out[_fe] || {}).F; + } else temp_val.f = ""+stringify_formula(val.formula,range,val.cell,supbooks, opts); } + safe_format_xf(temp_val, options, wb.opts.Date1904); + addcell(val.cell, temp_val, options); + last_formula = val; } break; case 'String': { - if(last_formula) { + if(last_formula) { /* technically always true */ last_formula.val = val; - temp_val = ({v:last_formula.val, ixfe:last_formula.cell.ixfe, t:'s'}/*:any*/); + temp_val = ({v:val, ixfe:last_formula.cell.ixfe, t:'s'}/*:any*/); temp_val.XF = XFs[temp_val.ixfe]; - if(options.cellFormula) temp_val.f = ""+stringify_formula(last_formula.formula, range, last_formula.cell, supbooks, opts); + if(options.cellFormula) { + temp_val.f = ""+stringify_formula(last_formula.formula, range, last_formula.cell, supbooks, opts); + } safe_format_xf(temp_val, options, wb.opts.Date1904); addcell(last_formula.cell, temp_val, options); last_formula = null; - } + } else throw new Error("String record expects Formula"); } break; case 'Array': { array_formulae.push(val); - if(options.cellFormula && out[last_cell]) { + var _arraystart = encode_cell(val[0].s); + if(options.cellFormula && out[_arraystart]) { if(!last_formula) break; /* technically unreachable */ - if(!last_cell || !out[last_cell]) break; /* technically unreachable */ - out[last_cell].f = ""+stringify_formula(last_formula.formula, range, last_formula.cell, supbooks, opts); - out[last_cell].F = encode_range(val[0]); + if(!_arraystart || !out[_arraystart]) break; + out[_arraystart].f = ""+stringify_formula(val[1], range, val[0], supbooks, opts); + out[_arraystart].F = encode_range(val[0]); } } break; case 'ShrFmla': { @@ -10952,6 +10996,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { safe_format_xf(temp_val, options, wb.opts.Date1904); addcell({c:val.c, r:val.r}, temp_val, options); break; + case 'Dimensions': { if(file_depth === 1) range = val; /* TODO: stack */ } break; @@ -12602,20 +12647,20 @@ function write_BIFF2LABEL(r, c, val) { } function write_ws_biff_cell(ba/*:BufArray*/, cell/*:Cell*/, R/*:number*/, C/*:number*/, opts) { - switch(cell.t) { + if(cell.v != null) switch(cell.t) { case 'n': if((cell.v == (cell.v|0)) && (cell.v >= 0) && (cell.v < 65536)) write_biff_rec(ba, 0x0002, write_BIFF2INT(R, C, cell.v)); else write_biff_rec(ba, 0x0003, write_BIFF2NUMBER(R,C, cell.v)); - break; - case 'b': case 'e': write_biff_rec(ba, 0x0005, write_BIFF2BERR(R, C, cell.v, cell.t)); break; + return; + case 'b': case 'e': write_biff_rec(ba, 0x0005, write_BIFF2BERR(R, C, cell.v, cell.t)); return; /* TODO: codepage, sst */ case 's': case 'str': write_biff_rec(ba, 0x0004, write_BIFF2LABEL(R, C, cell.v)); - break; - default: write_biff_rec(ba, 0x0001, write_BIFF2Cell(null, R, C)); + return; } + write_biff_rec(ba, 0x0001, write_BIFF2Cell(null, R, C)); } function write_biff_ws(ba/*:BufArray*/, ws/*:Worksheet*/, idx/*:number*/, opts, wb/*:Workbook*/) { @@ -12761,6 +12806,7 @@ var parse_content_xml = (function() { ctag = parsexmltag(Rn[0], false); q = ({t:ctag['数据类型'] || ctag['value-type'], v:null/*:: , z:null, w:""*/}/*:any*/); if(opts.cellFormula) { + if(ctag.formula) ctag.formula = unescapexml(ctag.formula); if(ctag['number-matrix-columns-spanned'] && ctag['number-matrix-rows-spanned']) { mR = parseInt(ctag['number-matrix-rows-spanned'],10) || 0; mC = parseInt(ctag['number-matrix-columns-spanned'],10) || 0; @@ -13108,11 +13154,22 @@ var write_content_xml/*:{(wb:any, opts:any):string}*/ = (function() { } if(skip) { o.push(covered_cell_xml); continue; } var ref = encode_cell({r:R, c:C}), cell = ws[ref]; + var fmla = ""; + if(cell && cell.f) { + fmla = ' table:formula="' + escapexml(csf_to_ods_formula(cell.f)) + '"'; + if(cell.F) { + if(cell.F.substr(0, ref.length) == ref) { + var _Fref = decode_range(cell.F); + fmla += ' table:number-matrix-columns-spanned="' + (_Fref.e.c - _Fref.s.c + 1)+ '"'; + fmla += ' table:number-matrix-rows-spanned="' + (_Fref.e.r - _Fref.s.r + 1) + '"'; + } else fmla = ""; + } + } if(cell) switch(cell.t) { - case 'b': o.push(cell_begin + mxml + vt + '"boolean" office:boolean-value="' + (cell.v ? 'true' : 'false') + '">' + p_begin + (cell.v ? 'TRUE' : 'FALSE') + p_end + cell_end); break; - case 'n': o.push(cell_begin + mxml + vt + '"float" office:value="' + cell.v + '">' + p_begin + (cell.w||cell.v) + p_end + cell_end); break; - case 's': case 'str': o.push(cell_begin + mxml + vt + '"string">' + p_begin + escapexml(cell.v) + p_end + cell_end); break; - case 'd': o.push(cell_begin + mxml + vt + '"date" office:date-value="' + (new Date(cell.v).toISOString()) + '">' + p_begin + (cell.w||(new Date(cell.v).toISOString())) + p_end + cell_end); break; + case 'b': o.push(cell_begin + mxml + vt + '"boolean" office:boolean-value="' + (cell.v ? 'true' : 'false') + '"' + fmla + '>' + p_begin + (cell.v ? 'TRUE' : 'FALSE') + p_end + cell_end); break; + case 'n': o.push(cell_begin + mxml + vt + '"float" office:value="' + cell.v + '"' + fmla + '>' + p_begin + (cell.w||cell.v) + p_end + cell_end); break; + case 's': case 'str': o.push(cell_begin + mxml + vt + '"string"' + fmla + '>' + p_begin + escapexml(cell.v) + p_end + cell_end); break; + case 'd': o.push(cell_begin + mxml + vt + '"date" office:date-value="' + (new Date(cell.v).toISOString()) + '"' + fmla + '>' + p_begin + (cell.w||(new Date(cell.v).toISOString())) + p_end + cell_end); break; //case 'e': default: o.push(null_cell_xml); } else o.push(null_cell_xml); @@ -13127,7 +13184,7 @@ var write_content_xml/*:{(wb:any, opts:any):string}*/ = (function() { var o = [XML_HEADER]; /* 3.1.3.2 */ if(opts.bookType == "fods") o.push(''); - else o.push('\n'); // TODO + else o.push('\n'); // TODO o.push(' \n'); o.push(' \n'); for(var i = 0; i != wb.SheetNames.length; ++i) o.push(write_ws(wb.Sheets[wb.SheetNames[i]], wb, i, opts)); @@ -13749,9 +13806,15 @@ function sheet_to_csv(sheet/*:Worksheet*/, opts/*:?Sheet2CSVOpts*/) { rr = encode_row(R); for(C = r.s.c; C <= r.e.c; ++C) { val = sheet[cols[C] + rr]; - txt = val !== undefined ? ''+format_cell(val) : ""; - for(i = 0, cc = 0; i !== txt.length; ++i) if((cc = txt.charCodeAt(i)) === fs || cc === rs || cc === 34) { - txt = "\"" + txt.replace(qreg, '""') + "\""; break; } + if(val == null) txt = ""; + else if(val.v != null) { + txt = ''+format_cell(val); + for(i = 0, cc = 0; i !== txt.length; ++i) if((cc = txt.charCodeAt(i)) === fs || cc === rs || cc === 34) { + txt = "\"" + txt.replace(qreg, '""') + "\""; break; } + } else if(val.f != null && !val.F) { + txt = '=' + val.f; if(txt.indexOf(",") >= 0) txt = '"' + txt.replace(qreg, '""') + '"'; + } else txt = ""; + /* NOTE: Excel CSV does not support array formulae */ row += (C === r.s.c ? "" : FS) + txt; } out += row + RS; diff --git a/xlsx.js b/xlsx.js index 3d88210..1684831 100644 --- a/xlsx.js +++ b/xlsx.js @@ -403,7 +403,18 @@ function hashq(str) { return o; } function rnd(val, d) { var dd = Math.pow(10,d); return ""+(Math.round(val * dd)/dd); } -function dec(val, d) { return Math.round((val-Math.floor(val))*Math.pow(10,d)); } +function dec(val, d) { + if (d < ('' + Math.round((val-Math.floor(val))*Math.pow(10,d))).length) { + return 0; + } + return Math.round((val-Math.floor(val))*Math.pow(10,d)); +} +function carry(val, d) { + if (d < ('' + Math.round((val-Math.floor(val))*Math.pow(10,d))).length) { + return 1; + } + return 0; +} function flr(val) { if(val < 2147483647 && val > -2147483648) return ""+(val >= 0 ? (val|0) : (val-1|0)); return ""+Math.floor(val); } function write_num_flt(type, fmt, val) { if(type.charCodeAt(0) === 40 && !fmt.match(closeparen)) { @@ -425,7 +436,6 @@ function write_num_flt(type, fmt, val) { if((r = fmt.match(frac1))) return write_num_f1(r, aval, sign); if(fmt.match(/^#+0+$/)) return sign + pad0r(aval,fmt.length - fmt.indexOf("0")); if((r = fmt.match(dec1))) { - // $FlowIgnore o = rnd(val, r[1].length).replace(/^([^\.]+)$/,"$1."+r[1]).replace(/\.$/,"."+r[1]).replace(/\.(\d*)$/,function($$, $1) { return "." + $1 + fill("0", r[1].length-$1.length); }); return fmt.indexOf("0.") !== -1 ? o : o.replace(/^0\./,"."); } @@ -435,7 +445,7 @@ function write_num_flt(type, fmt, val) { } if((r = fmt.match(/^#,##0(\.?)$/))) return sign + commaify(pad0r(aval,0)); if((r = fmt.match(/^#,##0\.([#0]*0)$/))) { - return val < 0 ? "-" + write_num_flt(type, fmt, -val) : commaify(""+(Math.floor(val))) + "." + pad0(dec(val, r[1].length),r[1].length); + return val < 0 ? "-" + write_num_flt(type, fmt, -val) : commaify(""+(Math.floor(val) + carry(val, r[1].length))) + "." + pad0(dec(val, r[1].length),r[1].length); } if((r = fmt.match(/^#,#*,#0/))) return write_num_flt(type,fmt.replace(/^#,#*,/,""),val); if((r = fmt.match(/^([0#]+)(\\?-([0#]+))+$/))) { @@ -8148,8 +8158,18 @@ function ods_to_csf_formula(f) { f = f.substr(1); if(f.charCodeAt(0) == 61) f = f.substr(1); } + f = f.replace(/COM\.MICROSOFT\./g, ""); /* Part 3 Section 5.8 References */ - return f.replace(/\[((?:\.[A-Z]+[0-9]+)(?::\.[A-Z]+[0-9]+)?)\]/g, "$1").replace(/\./g, ""); + f = f.replace(/\[((?:\.[A-Z]+[0-9]+)(?::\.[A-Z]+[0-9]+)?)\]/g, function($$, $1) { return $1.replace(/\./g,""); }); + /* TODO: something other than this */ + f = f.replace(/\[.(#[A-Z]*[?!])\]/g, "$1"); + return f.replace(/[;~]/g,",").replace(/\|/g,";"); +} + +function csf_to_ods_formula(f) { + var o = "of:=" + f.replace(crefregex, "$1[.$2$3$4$5]").replace(/\]:\[/g,":"); + /* TODO: something other than this */ + return o.replace(/;/g, "|").replace(/,/g,";"); } var strs = {}; // shared strings var _ssfopts = {}; // spreadsheet formatting options @@ -8337,7 +8357,7 @@ function write_ws_xml_cols(ws, cols) { } function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { - if(cell.v === undefined || cell.t === 'z') return ""; + if(cell.v === undefined && cell.f === undefined || cell.t === 'z') return ""; var vv = ""; var oldt = cell.t, oldv = cell.v; switch(cell.t) { @@ -8363,7 +8383,7 @@ function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { case 'd': o.t = "d"; break; case 'b': o.t = "b"; break; case 'e': o.t = "e"; break; - default: + default: if(cell.v == null) { delete cell.t; break; } if(opts.bookSST) { v = writetag('v', ''+get_sst_id(opts.Strings, cell.v)); o.t = "s"; break; @@ -8371,6 +8391,10 @@ function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { o.t = "str"; break; } if(cell.t != oldt) { cell.t = oldt; cell.v = oldv; } + if(cell.f) { + var ff = cell.F && cell.F.substr(0, ref.length) == ref ? {t:"array", ref:cell.F} : null; + v = writextag('f', escapexml(cell.f), ff) + (cell.v != null ? v : ""); + } return writextag('c', v, o); } @@ -8426,7 +8450,8 @@ return function parse_ws_xml_data(sdata, s, opts, guess) { if((cref=d.match(match_v))!= null && cref[1] !== '') p.v=unescapexml(cref[1]); if(opts.cellFormula) { if((cref=d.match(match_f))!= null && cref[1] !== '') { - p.f=unescapexml(utf8read(cref[1])); + /* TODO: match against XLSXFutureFunctions */ + p.f=unescapexml(utf8read(cref[1])).replace(/_xlfn\./,""); if(cref[0].indexOf('t="array"') > -1) { p.F = (d.match(refregex)||[])[1]; if(p.F.indexOf(":") > -1) arrayf.push([safe_decode_range(p.F), p.F]); @@ -9787,7 +9812,7 @@ function parse_xlml_data(xml, ss, data, cell, base, styles, csty, row, arrayf, o case 'DateTime': cell.v = (Date.parse(xml) - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000); if(cell.v !== cell.v) cell.v = unescapexml(xml); - else if(cell.v >= 1 && cell.v<60) cell.v = cell.v -1; + else if(cell.v<60) cell.v = cell.v -1; if(!nf || nf == "General") nf = "yyyy-mm-dd"; /* falls through */ case 'Number': @@ -9928,6 +9953,7 @@ for(var cma = c; cma <= cc; ++cma) { sheetname = unescapexml(tmp.Name); cursheet = {}; mergecells = []; + arrayf = []; } break; case 'Table': @@ -10436,10 +10462,14 @@ function write_sty_xlml(wb, opts) { } /* TODO */ function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr){ - if(!cell || cell.v === undefined) return ""; + if(!cell || cell.v == undefined && cell.f == undefined) return ""; var attr = {}; if(cell.f) attr["ss:Formula"] = "=" + escapexml(a1_to_rc(cell.f, addr)); + if(cell.F && cell.F.substr(0, ref.length) == ref) { + var end = decode_cell(cell.F.substr(ref.length + 1)); + attr["ss:ArrayRange"] = "RC:R" + (end.r == addr.r ? "" : "[" + (end.r - addr.r) + "]") + "C" + (end.c == addr.c ? "" : "[" + (end.c - addr.c) + "]"); + } if(ws['!merges']) { var marr = ws['!merges']; @@ -10457,9 +10487,9 @@ function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr){ case 'b': t = 'Boolean'; p = (cell.v ? "1" : "0"); break; case 'e': t = 'Error'; p = BErr[cell.v]; break; case 'd': t = 'DateTime'; p = new Date(cell.v).toISOString(); break; - case 's': t = 'String'; p = escapexml(cell.v||""); break; + case 's': t = 'String'; p = escapexml(cell.v||""); break; } - var m = '' + p + ''; + var m = '' + (cell.v != null ? p : "") + ''; return writextag("Cell", m, attr); } @@ -10642,6 +10672,19 @@ function parse_workbook(blob, options) { if(cell.r + 1 > range.e.r) range.e.r = cell.r + 1; if(cell.c + 1 > range.e.c) range.e.c = cell.c + 1; } + if(options.cellFormula && line.f) { + for(var afi = 0; afi < array_formulae.length; ++afi) { + if(array_formulae[afi][0].s.c > cell.c) continue; + if(array_formulae[afi][0].s.r > cell.r) continue; + if(array_formulae[afi][0].e.c < cell.c) continue; + if(array_formulae[afi][0].e.r < cell.r) continue; + line.F = encode_range(array_formulae[afi][0]); + if(array_formulae[afi][0].s.c != cell.c) delete line.f; + if(array_formulae[afi][0].s.r != cell.r) delete line.f; + if(line.f) line.f = "" + stringify_formula(array_formulae[afi][1], range, cell, supbooks, opts); + break; + } + } if(options.sheetRows && lastcell.r >= options.sheetRows) cell_valid = false; else out[last_cell] = line; }; @@ -10794,7 +10837,9 @@ function parse_workbook(blob, options) { else cur_sheet = (Directory[s] || {name:""}).name; mergecells = []; objects = []; + array_formulae = []; opts.arrayf = array_formulae; } break; + case 'Number': case 'BIFF2NUM': case 'BIFF2INT': { temp_val = {ixfe: val.ixfe, XF: XFs[val.ixfe], v:val.val, t:'n'}; safe_format_xf(temp_val, options, wb.opts.Date1904); @@ -10819,44 +10864,43 @@ function parse_workbook(blob, options) { } } break; case 'Formula': { - switch(val.val) { - case 'String': last_formula = val; break; - case 'Array Formula': throw "Array Formula unsupported"; - default: - temp_val = ({v:val.val, ixfe:val.cell.ixfe, t:val.tt}); - temp_val.XF = XFs[temp_val.ixfe]; - if(options.cellFormula) { - var _f = val.formula; - if(_f && _f[0] && _f[0][0] && _f[0][0][0] == 'PtgExp') { - var _fr = _f[0][0][1][0], _fc = _f[0][0][1][1]; - var _fe = encode_cell({r:_fr, c:_fc}); - if(shared_formulae[_fe]) temp_val.f = ""+stringify_formula(val.formula,range,val.cell,supbooks, opts); - else temp_val.F = (out[_fe] || {}).F; - } else temp_val.f = ""+stringify_formula(val.formula,range,val.cell,supbooks, opts); - } - safe_format_xf(temp_val, options, wb.opts.Date1904); - addcell(val.cell, temp_val, options); - last_formula = val; + if(val.val == 'String') { last_formula = val; break; } + temp_val = ({v:val.val, ixfe:val.cell.ixfe, t:val.tt}); + temp_val.XF = XFs[temp_val.ixfe]; + if(options.cellFormula) { + var _f = val.formula; + if(_f && _f[0] && _f[0][0] && _f[0][0][0] == 'PtgExp') { + var _fr = _f[0][0][1][0], _fc = _f[0][0][1][1]; + var _fe = encode_cell({r:_fr, c:_fc}); + if(shared_formulae[_fe]) temp_val.f = ""+stringify_formula(val.formula,range,val.cell,supbooks, opts); + else temp_val.F = (out[_fe] || {}).F; + } else temp_val.f = ""+stringify_formula(val.formula,range,val.cell,supbooks, opts); } + safe_format_xf(temp_val, options, wb.opts.Date1904); + addcell(val.cell, temp_val, options); + last_formula = val; } break; case 'String': { - if(last_formula) { + if(last_formula) { /* technically always true */ last_formula.val = val; - temp_val = ({v:last_formula.val, ixfe:last_formula.cell.ixfe, t:'s'}); + temp_val = ({v:val, ixfe:last_formula.cell.ixfe, t:'s'}); temp_val.XF = XFs[temp_val.ixfe]; - if(options.cellFormula) temp_val.f = ""+stringify_formula(last_formula.formula, range, last_formula.cell, supbooks, opts); + if(options.cellFormula) { + temp_val.f = ""+stringify_formula(last_formula.formula, range, last_formula.cell, supbooks, opts); + } safe_format_xf(temp_val, options, wb.opts.Date1904); addcell(last_formula.cell, temp_val, options); last_formula = null; - } + } else throw new Error("String record expects Formula"); } break; case 'Array': { array_formulae.push(val); - if(options.cellFormula && out[last_cell]) { + var _arraystart = encode_cell(val[0].s); + if(options.cellFormula && out[_arraystart]) { if(!last_formula) break; /* technically unreachable */ - if(!last_cell || !out[last_cell]) break; /* technically unreachable */ - out[last_cell].f = ""+stringify_formula(last_formula.formula, range, last_formula.cell, supbooks, opts); - out[last_cell].F = encode_range(val[0]); + if(!_arraystart || !out[_arraystart]) break; + out[_arraystart].f = ""+stringify_formula(val[1], range, val[0], supbooks, opts); + out[_arraystart].F = encode_range(val[0]); } } break; case 'ShrFmla': { @@ -10895,6 +10939,7 @@ function parse_workbook(blob, options) { safe_format_xf(temp_val, options, wb.opts.Date1904); addcell({c:val.c, r:val.r}, temp_val, options); break; + case 'Dimensions': { if(file_depth === 1) range = val; /* TODO: stack */ } break; @@ -12545,20 +12590,20 @@ function write_BIFF2LABEL(r, c, val) { } function write_ws_biff_cell(ba, cell, R, C, opts) { - switch(cell.t) { + if(cell.v != null) switch(cell.t) { case 'n': if((cell.v == (cell.v|0)) && (cell.v >= 0) && (cell.v < 65536)) write_biff_rec(ba, 0x0002, write_BIFF2INT(R, C, cell.v)); else write_biff_rec(ba, 0x0003, write_BIFF2NUMBER(R,C, cell.v)); - break; - case 'b': case 'e': write_biff_rec(ba, 0x0005, write_BIFF2BERR(R, C, cell.v, cell.t)); break; + return; + case 'b': case 'e': write_biff_rec(ba, 0x0005, write_BIFF2BERR(R, C, cell.v, cell.t)); return; /* TODO: codepage, sst */ case 's': case 'str': write_biff_rec(ba, 0x0004, write_BIFF2LABEL(R, C, cell.v)); - break; - default: write_biff_rec(ba, 0x0001, write_BIFF2Cell(null, R, C)); + return; } + write_biff_rec(ba, 0x0001, write_BIFF2Cell(null, R, C)); } function write_biff_ws(ba, ws, idx, opts, wb) { @@ -12704,6 +12749,7 @@ var parse_content_xml = (function() { ctag = parsexmltag(Rn[0], false); q = ({t:ctag['数据类型'] || ctag['value-type'], v:null}); if(opts.cellFormula) { + if(ctag.formula) ctag.formula = unescapexml(ctag.formula); if(ctag['number-matrix-columns-spanned'] && ctag['number-matrix-rows-spanned']) { mR = parseInt(ctag['number-matrix-rows-spanned'],10) || 0; mC = parseInt(ctag['number-matrix-columns-spanned'],10) || 0; @@ -13051,11 +13097,22 @@ var write_content_xml = (function() { } if(skip) { o.push(covered_cell_xml); continue; } var ref = encode_cell({r:R, c:C}), cell = ws[ref]; + var fmla = ""; + if(cell && cell.f) { + fmla = ' table:formula="' + escapexml(csf_to_ods_formula(cell.f)) + '"'; + if(cell.F) { + if(cell.F.substr(0, ref.length) == ref) { + var _Fref = decode_range(cell.F); + fmla += ' table:number-matrix-columns-spanned="' + (_Fref.e.c - _Fref.s.c + 1)+ '"'; + fmla += ' table:number-matrix-rows-spanned="' + (_Fref.e.r - _Fref.s.r + 1) + '"'; + } else fmla = ""; + } + } if(cell) switch(cell.t) { - case 'b': o.push(cell_begin + mxml + vt + '"boolean" office:boolean-value="' + (cell.v ? 'true' : 'false') + '">' + p_begin + (cell.v ? 'TRUE' : 'FALSE') + p_end + cell_end); break; - case 'n': o.push(cell_begin + mxml + vt + '"float" office:value="' + cell.v + '">' + p_begin + (cell.w||cell.v) + p_end + cell_end); break; - case 's': case 'str': o.push(cell_begin + mxml + vt + '"string">' + p_begin + escapexml(cell.v) + p_end + cell_end); break; - case 'd': o.push(cell_begin + mxml + vt + '"date" office:date-value="' + (new Date(cell.v).toISOString()) + '">' + p_begin + (cell.w||(new Date(cell.v).toISOString())) + p_end + cell_end); break; + case 'b': o.push(cell_begin + mxml + vt + '"boolean" office:boolean-value="' + (cell.v ? 'true' : 'false') + '"' + fmla + '>' + p_begin + (cell.v ? 'TRUE' : 'FALSE') + p_end + cell_end); break; + case 'n': o.push(cell_begin + mxml + vt + '"float" office:value="' + cell.v + '"' + fmla + '>' + p_begin + (cell.w||cell.v) + p_end + cell_end); break; + case 's': case 'str': o.push(cell_begin + mxml + vt + '"string"' + fmla + '>' + p_begin + escapexml(cell.v) + p_end + cell_end); break; + case 'd': o.push(cell_begin + mxml + vt + '"date" office:date-value="' + (new Date(cell.v).toISOString()) + '"' + fmla + '>' + p_begin + (cell.w||(new Date(cell.v).toISOString())) + p_end + cell_end); break; //case 'e': default: o.push(null_cell_xml); } else o.push(null_cell_xml); @@ -13070,7 +13127,7 @@ var write_content_xml = (function() { var o = [XML_HEADER]; /* 3.1.3.2 */ if(opts.bookType == "fods") o.push(''); - else o.push('\n'); // TODO + else o.push('\n'); // TODO o.push(' \n'); o.push(' \n'); for(var i = 0; i != wb.SheetNames.length; ++i) o.push(write_ws(wb.Sheets[wb.SheetNames[i]], wb, i, opts)); @@ -13684,9 +13741,15 @@ function sheet_to_csv(sheet, opts) { rr = encode_row(R); for(C = r.s.c; C <= r.e.c; ++C) { val = sheet[cols[C] + rr]; - txt = val !== undefined ? ''+format_cell(val) : ""; - for(i = 0, cc = 0; i !== txt.length; ++i) if((cc = txt.charCodeAt(i)) === fs || cc === rs || cc === 34) { - txt = "\"" + txt.replace(qreg, '""') + "\""; break; } + if(val == null) txt = ""; + else if(val.v != null) { + txt = ''+format_cell(val); + for(i = 0, cc = 0; i !== txt.length; ++i) if((cc = txt.charCodeAt(i)) === fs || cc === rs || cc === 34) { + txt = "\"" + txt.replace(qreg, '""') + "\""; break; } + } else if(val.f != null && !val.F) { + txt = '=' + val.f; if(txt.indexOf(",") >= 0) txt = '"' + txt.replace(qreg, '""') + '"'; + } else txt = ""; + /* NOTE: Excel CSV does not support array formulae */ row += (C === r.s.c ? "" : FS) + txt; } out += row + RS;