From aff7b952720a466bb739ffbbe1cb5d1f9635dcb5 Mon Sep 17 00:00:00 2001 From: SheetJS Date: Thu, 1 Jun 2017 17:22:11 -0400 Subject: [PATCH] Date/Time Minutiae - TZ consistency for DBF and others (closes #663 h/t @peeyushsrivastava) - Date1904 XLSX/XLSB/XLS/XLML consistency (fixes #175 h/t @SheetJSDev) - dateNF corrects for plaintext date parsing (fixes #658 h/t @mmancosu) - new travis tests override local time zones --- .travis.yml | 17 ++++ README.md | 48 ++++++++-- bits/11_ssfutils.js | 27 ++++++ bits/20_jsutils.js | 29 +++--- bits/22_xmlutils.js | 2 +- bits/40_harb.js | 9 +- bits/67_wsxml.js | 9 +- bits/68_wsbin.js | 2 +- bits/71_wbcommon.js | 70 ++++++++------ bits/72_wbxml.js | 21 ++-- bits/73_wbbin.js | 27 +++++- bits/75_xlml.js | 9 +- bits/76_xls.js | 8 +- docbits/52_datatype.md | 29 ++++++ docbits/55_wbobject.md | 7 +- docbits/57_wbbook.md | 11 ++- misc/docs/SUMMARY.md | 1 + misc/flow.js | 7 +- multiformat.lst | 8 -- test.js | 16 +++- types/index.d.ts | 9 ++ xlsx.flow.js | 213 +++++++++++++++++++++++++++-------------- xlsx.js | 210 ++++++++++++++++++++++++++-------------- 23 files changed, 548 insertions(+), 241 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9144608..749b489 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: node_js node_js: + - "8" - "7" - "6" # note: travis has been acting up on old versions of node @@ -9,6 +10,22 @@ node_js: # - "0.10" # - "0.9" # - "0.8" +matrix: + include: + - node_js: "6" + env: TZ="America/New_York" + - node_js: "8" + env: TZ="America/Los_Angeles" + - node_js: "6" + env: TZ="Europe/London" + - node_js: "8" + env: TZ="Europe/Berlin" + - node_js: "6" + env: TZ="Asia/Kolkata" + - node_js: "7" + env: TZ="Asia/Shanghai" + - node_js: "8" + env: TZ="Asia/Seoul" before_install: - "npm install -g npm@4.3.0" - "npm install -g mocha@2.x voc" diff --git a/README.md b/README.md index 01af5e0..b0b82c2 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ enhancements, additional features by request, and dedicated support. + [Workbook File Properties](#workbook-file-properties) * [Workbook-Level Attributes](#workbook-level-attributes) + [Defined Names](#defined-names) + + [Miscellaneous Workbook Properties](#miscellaneous-workbook-properties) * [Document Features](#document-features) + [Formulae](#formulae) + [Column Properties](#column-properties) @@ -719,6 +720,9 @@ will not be generated; the parser `sheetStubs` option must be set to `true`. #### Dates +
+ Excel Date Code details (click to show) + By default, Excel stores dates as numbers with a format code that specifies date processing. For example, the date `19-Feb-17` is stored as the number `42785` with a number format of `d-mmm-yy`. The `SSF` module understands number formats @@ -730,6 +734,32 @@ string. The formatter converts the date back to a number. The default behavior for all parsers is to generate number cells. Setting `cellDates` to true will force the generators to store dates. +
+ +
+ Time Zones and Dates (click to show) + +Excel has no native concept of universal time. All times are specified in the +local time zone. Excel limitations prevent specifying true absolute dates. + +Following Excel, this library treats all dates as relative to local time zone. + +
+ +
+ Epochs: 1900 and 1904 (click to show) + +Excel supports two epochs (January 1 1900 and January 1 1904), see +["1900 vs. 1904 Date System" article](http://support2.microsoft.com/kb/180162). +The workbook's epoch can be determined by examining the workbook's +`wb.Workbook.WBProps.date1904` property: + +```js +!!(((wb.Workbook||{}).WBProps||{}).date1904) +``` + +
+ ### Sheet Objects Each key that does not start with `!` maps to a cell (using `A-1` notation) @@ -850,12 +880,7 @@ first row of the chartsheet is the underlying header. custom properties. Since the XLS standard properties deviate from the XLSX standard, XLS parsing stores core properties in both places. -`wb.WBProps` includes more workbook-level properties: - -- Excel supports two epochs (January 1 1900 and January 1 1904), see - [1900 vs. 1904 Date System](http://support2.microsoft.com/kb/180162). - The workbook's epoch can be determined by examining the workbook's - `wb.WBProps.date1904` property. +`wb.Workbook` stores [workbook-level attributes](#workbook-level-attributes). #### Workbook File Properties @@ -902,7 +927,7 @@ XLSX.write(wb, {Props:{Author:"SheetJS"}}); ### Workbook-Level Attributes -`wb.Workbook` stores workbook level attributes. +`wb.Workbook` stores workbook-level attributes. #### Defined Names @@ -917,12 +942,21 @@ XLSX.write(wb, {Props:{Author:"SheetJS"}}); | `Name` | Case-sensitive name. Standard rules apply ** | | `Ref` | A1-style Reference (e.g. `"Sheet1!$A$1:$D$20"`) | | `Comment` | Comment (only applicable for XLS/XLSX/XLSB) | + Excel allows two sheet-scoped defined names to share the same name. However, a sheet-scoped name cannot collide with a workbook-scope name. Workbook writers may not enforce this constraint. +#### Miscellaneous Workbook Properties + +`wb.Workbook.WBProps` holds other workbook properties: + +| Key | Description | +|:-----------|:----------------------------------------------------| +| `date1904` | epoch: 0/false for 1900 system, 1/true for 1904 | + ### Document Features Even for basic features like date storage, the official Excel formats store the diff --git a/bits/11_ssfutils.js b/bits/11_ssfutils.js index f2d0c41..a700c68 100644 --- a/bits/11_ssfutils.js +++ b/bits/11_ssfutils.js @@ -18,3 +18,30 @@ var XLMLFormatMap/*{[string]:string}*/ = ({ "On/Off": '"Yes";"Yes";"No";@' }/*:any*/); +/* dateNF parse TODO: move to SSF */ +var dateNFregex = /[dD]+|[mM]+|[yYeE]+|[Hh]+|[Ss]+/g; +function dateNF_regex(dateNF/*:string|number*/)/*:RegExp*/ { + var fmt = typeof dateNF == "number" ? SSF._table[dateNF] : dateNF; + fmt = fmt.replace(dateNFregex, "(\\d+)"); + return new RegExp("^" + fmt + "$"); +} +function dateNF_fix(str/*:string*/, dateNF/*:string*/, match/*:Array*/)/*:string*/ { + var Y = -1, m = -1, d = -1, H = -1, M = -1, S = -1; + (dateNF.match(dateNFregex)||[]).forEach(function(n, i) { + var v = parseInt(match[i+1], 10); + switch(n.toLowerCase().charAt(0)) { + case 'y': Y = v; break; case 'd': d = v; break; + case 'h': H = v; break; case 's': S = v; break; + case 'm': if(H >= 0) M = v; else m = v; break; + } + }); + if(S >= 0 && M == -1 && m >= 0) { M = m; m = -1; } + var datestr = (("" + (Y>=0?Y: new Date().getFullYear())).slice(-4) + "-" + ("00" + (m>=1?m:1)).slice(-2) + "-" + ("00" + (d>=1?d:1)).slice(-2)); + if(datestr.length == 7) datestr = "0" + datestr; + if(datestr.length == 8) datestr = "20" + datestr; + var timestr = (("00" + (H>=0?H:0)).slice(-2) + ":" + ("00" + (M>=0?M:0)).slice(-2) + ":" + ("00" + (S>=0?S:0)).slice(-2)); + if(H == -1 && M == -1 && S == -1) return datestr; + if(Y == -1 && m == -1 && d == -1) return timestr; + return datestr + "T" + timestr; +} + diff --git a/bits/20_jsutils.js b/bits/20_jsutils.js index 1b217a8..92c8156 100644 --- a/bits/20_jsutils.js +++ b/bits/20_jsutils.js @@ -29,22 +29,17 @@ function evert_arr(obj/*:any*/)/*:EvertArrType*/ { return o; } +var basedate = new Date(1899, 11, 30, 0, 0, 0); // 2209161600000 +var dnthresh = basedate.getTime() + (new Date().getTimezoneOffset() - basedate.getTimezoneOffset()) * 60000; function datenum(v/*:Date*/, date1904/*:?boolean*/)/*:number*/ { var epoch = v.getTime(); if(date1904) epoch += 1462*24*60*60*1000; - return (epoch + 2209161600000) / (24 * 60 * 60 * 1000); + return (epoch - dnthresh) / (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; + var out = new Date(); + out.setTime(v * 24 * 60 * 60 * 1000 + dnthresh); + return out; } /* ISO 8601 Duration */ @@ -77,9 +72,15 @@ function parse_isodur(s) { var good_pd_date = new Date('2017-02-19T19:06:09.000Z'); if(isNaN(good_pd_date.getFullYear())) good_pd_date = new Date('2/19/17'); var good_pd = good_pd_date.getFullYear() == 2017; -function parseDate(str/*:string|Date*/)/*:Date*/ { +/* parses aa date as a local date */ +function parseDate(str/*:string|Date*/, fixdate/*:?number*/)/*:Date*/ { var d = new Date(str); - if(good_pd) return d; + if(good_pd) { + /*:: if(fixdate == null) fixdate = 0; */ + if(fixdate > 0) d.setTime(d.getTime() + d.getTimezoneOffset() * 60 * 1000); + else if(fixdate < 0) d.setTime(d.getTime() - d.getTimezoneOffset() * 60 * 1000); + return d; + } if(str instanceof Date) return str; if(good_pd_date.getFullYear() == 1917 && !isNaN(d.getFullYear())) { var s = d.getFullYear(); @@ -87,7 +88,7 @@ function parseDate(str/*:string|Date*/)/*:Date*/ { d.setFullYear(d.getFullYear() + 100); return d; } var n = str.match(/\d+/g)||["2017","2","19","0","0","0"]; - return new Date(Date.UTC(+n[0], +n[1] - 1, +n[2], (+n[3]||0), (+n[4]||0), (+n[5]||0))); + return new Date(+n[0], +n[1] - 1, +n[2], (+n[3]||0), (+n[4]||0), (+n[5]||0)); } function cc2str(arr/*:Array*/)/*:string*/ { diff --git a/bits/22_xmlutils.js b/bits/22_xmlutils.js index d5e8080..9d462f8 100644 --- a/bits/22_xmlutils.js +++ b/bits/22_xmlutils.js @@ -74,7 +74,7 @@ var xlml_unfixstr/*:StringConv*/ = (function() { function parsexmlbool(value/*:any*/, tag/*:?string*/)/*:boolean*/ { switch(value) { - case '1': case 'true': case 'TRUE': return true; + case 1: case true: case '1': case 'true': case 'TRUE': return true; /* case '0': case 'false': case 'FALSE':*/ default: return false; } diff --git a/bits/40_harb.js b/bits/40_harb.js index 03c0b34..31e121f 100644 --- a/bits/40_harb.js +++ b/bits/40_harb.js @@ -523,6 +523,7 @@ var PRN = (function() { var R = 0, C = 0, v = 0; var start = 0, end = 0, sepcc = sep.charCodeAt(0), instr = false, cc=0; str = str.replace(/\r\n/mg, "\n"); + var _re/*:?RegExp*/ = o.dateNF != null ? dateNF_regex(o.dateNF) : null; function finish_cell() { var s = str.slice(start, end); var cell = ({}/*:any*/); @@ -530,10 +531,12 @@ var PRN = (function() { else if(s == "TRUE") { cell.t = 'b'; cell.v = true; } else if(s == "FALSE") { cell.t = 'b'; cell.v = false; } else if(!isNaN(v = +s)) { cell.t = 'n'; cell.w = s; cell.v = v; } - else if(!isNaN(fuzzydate(s).getDate())) { + else if(!isNaN(fuzzydate(s).getDate()) || _re && s.match(_re)) { cell.z = o.dateNF || SSF._table[14]; - if(o.cellDates) { cell.t = 'd'; cell.v = parseDate(s); } - else { cell.t = 'n'; cell.v = datenum(parseDate(s)); } + var k = 0; + if(_re && s.match(_re)){ s=dateNF_fix(s, o.dateNF, (s.match(_re)||[])); k=1; } + if(o.cellDates) { cell.t = 'd'; cell.v = parseDate(s, k); } + else { cell.t = 'n'; cell.v = datenum(parseDate(s, k)); } cell.w = SSF.format(cell.z, cell.v instanceof Date ? datenum(cell.v):cell.v); } else { cell.t = 's'; diff --git a/bits/67_wsxml.js b/bits/67_wsxml.js index 2da4576..d79c28c 100644 --- a/bits/67_wsxml.js +++ b/bits/67_wsxml.js @@ -194,7 +194,7 @@ function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { case 'n': vv = ''+cell.v; break; case 'e': vv = BErr[cell.v]; break; case 'd': - if(opts.cellDates) vv = parseDate(cell.v).toISOString(); + if(opts.cellDates) vv = parseDate(cell.v, -1).toISOString(); else { cell.t = 'n'; vv = ''+(cell.v = datenum(parseDate(cell.v))); @@ -347,7 +347,8 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { break; case 'b': p.v = parsexmlbool(p.v); break; case 'd': - if(!opts.cellDates) { p.v = datenum(parseDate(p.v)); p.t = 'n'; } + if(opts.cellDates) p.v = parseDate(p.v, 1); + else { p.v = datenum(parseDate(p.v, 1)); p.t = 'n'; } break; /* error string in .w, number in .v */ case 'e': @@ -364,9 +365,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { } } safe_format(p, fmtid, fillid, opts, themes, styles); - if(opts.cellDates && do_format && p.t == 'n' && SSF.is_date(SSF._table[fmtid])) { - var _d = SSF.parse_date_code(p.v); if(_d) { p.t = 'd'; p.v = new Date(Date.UTC(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u)); } - } + if(opts.cellDates && do_format && p.t == 'n' && SSF.is_date(SSF._table[fmtid])) { p.t = 'd'; p.v = numdate(p.v); } if(dense) { var _r = decode_cell(tag.r); if(!s[_r.r]) s[_r.r] = []; diff --git a/bits/68_wsbin.js b/bits/68_wsbin.js index fbd2f4e..d95e3be 100644 --- a/bits/68_wsbin.js +++ b/bits/68_wsbin.js @@ -470,7 +470,7 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ { if(refguess.e.r < row.r) refguess.e.r = row.r; if(refguess.e.c < C) refguess.e.c = C; if(opts.cellDates && cf && p.t == 'n' && SSF.is_date(SSF._table[cf.ifmt])) { - var _d = SSF.parse_date_code(p.v); if(_d) { p.t = 'd'; p.v = new Date(Date.UTC(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u)); } + var _d = SSF.parse_date_code(p.v); if(_d) { p.t = 'd'; p.v = new Date(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u); } } break; diff --git a/bits/71_wbcommon.js b/bits/71_wbcommon.js index 627d5ad..ed815b8 100644 --- a/bits/71_wbcommon.js +++ b/bits/71_wbcommon.js @@ -1,37 +1,36 @@ /* 18.2.28 (CT_WorkbookProtection) Defaults */ var WBPropsDef = [ - ['allowRefreshQuery', '0'], - ['autoCompressPictures', '1'], - ['backupFile', '0'], - ['checkCompatibility', '0'], - ['codeName', ''], - ['date1904', '0'], - ['dateCompatibility', '1'], - //['defaultThemeVersion', '0'], - ['filterPrivacy', '0'], - ['hidePivotFieldList', '0'], - ['promptedSolutions', '0'], - ['publishItems', '0'], - ['refreshAllConnections', false], - ['saveExternalLinkValues', '1'], - ['showBorderUnselectedTables', '1'], - ['showInkAnnotation', '1'], - ['showObjects', 'all'], - ['showPivotChartFilter', '0'] - //['updateLinks', 'userSet'] + ['allowRefreshQuery', false, "bool"], + ['autoCompressPictures', true, "bool"], + ['backupFile', false, "bool"], + ['checkCompatibility', false, "bool"], + ['codeName', ''], + ['date1904', false, "bool"], + ['defaultThemeVersion', 0, "int"], + ['filterPrivacy', false, "bool"], + ['hidePivotFieldList', false, "bool"], + ['promptedSolutions', false, "bool"], + ['publishItems', false, "bool"], + ['refreshAllConnections', false, "bool"], + ['saveExternalLinkValues', true, "bool"], + ['showBorderUnselectedTables', true, "bool"], + ['showInkAnnotation', true, "bool"], + ['showObjects', 'all'], + ['showPivotChartFilter', false, "bool"], + ['updateLinks', 'userSet'] ]; /* 18.2.30 (CT_BookView) Defaults */ var WBViewDef = [ - ['activeTab', '0'], - ['autoFilterDateGrouping', '1'], - ['firstSheet', '0'], - ['minimized', '0'], - ['showHorizontalScroll', '1'], - ['showSheetTabs', '1'], - ['showVerticalScroll', '1'], - ['tabRatio', '600'], - ['visibility', 'visible'] + ['activeTab', 0, "int"], + ['autoFilterDateGrouping', true, "bool"], + ['firstSheet', 0, "int"], + ['minimized', false, "bool"], + ['showHorizontalScroll', true, "bool"], + ['showSheetTabs', true, "bool"], + ['showVerticalScroll', true, "bool"], + ['tabRatio', 600, "int"], + ['visibility', 'visible'] //window{Height,Width}, {x,y}Window ]; @@ -80,12 +79,20 @@ function push_defaults_array(target, defaults) { for(var j = 0; j != target.length; ++j) { var w = target[j]; for(var i=0; i != defaults.length; ++i) { var z = defaults[i]; if(w[z[0]] == null) w[z[0]] = z[1]; + else switch(z[2]) { + case "bool": if(typeof w[z[0]] == "string") w[z[0]] = parsexmlbool(w[z[0]], z[0]); break; + case "int": if(typeof w[z[0]] == "string") w[z[0]] = parseInt(w[z[0]], 10); break; + } } } } function push_defaults(target, defaults) { for(var i = 0; i != defaults.length; ++i) { var z = defaults[i]; if(target[z[0]] == null) target[z[0]] = z[1]; + else switch(z[2]) { + case "bool": if(typeof target[z[0]] == "string") target[z[0]] = parsexmlbool(target[z[0]], z[0]); break; + case "int": if(typeof target[z[0]] == "string") target[z[0]] = parseInt(target[z[0]], 10); break; + } } } @@ -99,6 +106,13 @@ function parse_wb_defaults(wb) { _ssfopts.date1904 = parsexmlbool(wb.WBProps.date1904, 'date1904'); } +function safe1904(wb/*:Workbook*/)/*:string*/ { + /* TODO: store date1904 somewhere else */ + if(!wb.Workbook) return "false"; + if(!wb.Workbook.WBProps) return "false"; + return parsexmlbool(wb.Workbook.WBProps.date1904) ? "true" : "false"; +} + var badchars = "][*?\/\\".split(""); function check_ws_name(n/*:string*/, safe/*:?boolean*/)/*:boolean*/ { if(n.length > 31) { if(safe) return false; throw new Error("Sheet names cannot exceed 31 chars"); } diff --git a/bits/72_wbxml.js b/bits/72_wbxml.js index fac8cf5..3fcf219 100644 --- a/bits/72_wbxml.js +++ b/bits/72_wbxml.js @@ -26,8 +26,17 @@ function parse_wb_xml(data, opts)/*:WorkbookFile*/ { case '': break; /* 18.2.28 workbookPr CT_WorkbookPr ? */ - case '': delete y[0]; wb.WBProps = y; break; + case '': + WBPropsDef.forEach(function(w) { + if(y[w[0]] == null) return; + switch(w[2]) { + case "bool": wb.WBProps[w[0]] = parsexmlbool(y[w[0]], w[0]); break; + case "int": wb.WBProps[w[0]] = parseInt(y[w[0]], 10); break; + default: wb.WBProps[w[0]] = y[w[0]]; + } + }); + break; case '': break; /* 18.2.29 workbookProtection CT_WorkbookProtection ? */ @@ -148,14 +157,6 @@ var WB_XML_ROOT = writextag('workbook', null, { 'xmlns:r': XMLNS.r }); -function safe1904(wb/*:Workbook*/)/*:string*/ { - /* TODO: store date1904 somewhere else */ - if(!wb.Workbook) return "false"; - if(!wb.Workbook.WBProps) return "false"; - // $FlowIgnore - return parsexmlbool(wb.Workbook.WBProps.date1904) ? "true" : "false"; -} - function write_wb_xml(wb/*:Workbook*/, opts/*:?WriteOpts*/)/*:string*/ { var o = [XML_HEADER]; o[o.length] = WB_XML_ROOT; diff --git a/bits/73_wbbin.js b/bits/73_wbbin.js index ad426dd..2028d9e 100644 --- a/bits/73_wbbin.js +++ b/bits/73_wbbin.js @@ -18,10 +18,27 @@ function write_BrtBundleSh(data, o) { /* [MS-XLSB] 2.4.807 BrtWbProp */ function parse_BrtWbProp(data, length) { - data.read_shift(4); - var dwThemeVersion = data.read_shift(4); + var o = {}; + var flags = data.read_shift(4); + o.defaultThemeVersion = data.read_shift(4); var strName = (length > 8) ? parse_XLWideString(data) : ""; - return [dwThemeVersion, strName]; + if(strName.length > 0) o.codeName = strName; + o.autoCompressPictures = !!(flags & 0x10000); + o.backupFile = !!(flags & 0x40); + o.checkCompatibility = !!(flags & 0x1000); + o.date1904 = !!(flags & 0x01); + o.filterPrivacy = !!(flags & 0x08); + o.hidePivotFieldList = !!(flags & 0x400); + o.promptedSolutions = !!(flags & 0x10); + o.publishItems = !!(flags & 0x800); + o.refreshAllConnections = !!(flags & 0x40000); + o.saveExternalLinkValues = !!(flags & 0x80); + o.showBorderUnselectedTables = !!(flags & 0x04); + o.showInkAnnotation = !!(flags & 0x20); + o.showObjects = ["all", "placeholders", "none"][(flags >> 13) & 0x03]; + o.showPivotChartFilter = !!(flags & 0x8000); + o.updateLinks = ["userSet", "never", "always"][(flags >> 8) & 0x03]; + return o; } function write_BrtWbProp(data, o) { if(!o) o = new_buf(72); @@ -85,6 +102,9 @@ function parse_wb_bin(data, opts)/*:WorkbookFile*/ { break; case 0x040C: /* 'BrtNameExt' */ break; + case 0x0099: /* 'BrtWbProp' */ + wb.WBProps = val; break; + /* case 'BrtModelTimeGroupingCalcCol' */ /* case 'BrtRevisionPtr' */ /* case 'BrtUid' */ @@ -116,7 +136,6 @@ function parse_wb_bin(data, opts)/*:WorkbookFile*/ { case 0x0822: /* 'BrtTimelineCachePivotCacheID' */ case 0x018D: /* 'BrtUserBookView' */ case 0x009A: /* 'BrtWbFactoid' */ - case 0x0099: /* 'BrtWbProp' */ case 0x045D: /* 'BrtWbProp14' */ case 0x0229: /* 'BrtWebOpt' */ case 0x082B: /* 'BrtWorkBookPr15' */ diff --git a/bits/75_xlml.js b/bits/75_xlml.js index 40fc5b0..c80af9a 100644 --- a/bits/75_xlml.js +++ b/bits/75_xlml.js @@ -74,7 +74,7 @@ function safe_format_xlml(cell/*:Cell*/, nf, o) { var z = XLMLFormatMap[nf]||nf||"General"; if(o.cellNF) cell.z = z; if(o.cellDates && cell.t == 'n' && SSF.is_date(z)) { - var _d = SSF.parse_date_code(cell.v); if(_d) { cell.t = 'd'; cell.v = new Date(Date.UTC(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u)); } + var _d = SSF.parse_date_code(cell.v); if(_d) { cell.t = 'd'; cell.v = new Date(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u); } } } catch(e) { if(o.WTF) throw e; } } @@ -191,7 +191,7 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { var cstys = [], csty, seencol = false; var arrayf = []; var rowinfo = [], rowobj = {}; - var Workbook/*:WBWBProps*/ = { Sheets:[] }, wsprops = {}; + var Workbook/*:WBWBProps*/ = ({ Sheets:[], WBProps:{date1904:false} }/*:any*/), wsprops = {}; xlmlregex.lastIndex = 0; str = str.replace(//mg,""); while((Rn = xlmlregex.exec(str))) switch(Rn[3]) { @@ -472,6 +472,10 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { /* ExcelWorkbook */ case 'ExcelWorkbook': switch(Rn[3]) { + case 'Date1904': + /*:: if(!Workbook.WBProps) Workbook.WBProps = {}; */ + Workbook.WBProps.date1904 = true; + break; case 'WindowHeight': break; case 'WindowWidth': break; case 'WindowTopX': break; @@ -490,7 +494,6 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { case 'Dll': break; case 'AcceptLabelsInFormulas': break; case 'DoNotSaveLinkValues': break; - case 'Date1904': break; case 'Iteration': break; case 'MaxIterations': break; case 'MaxChange': break; diff --git a/bits/76_xls.js b/bits/76_xls.js index 1651b81..e229b12 100644 --- a/bits/76_xls.js +++ b/bits/76_xls.js @@ -71,7 +71,7 @@ function safe_format_xf(p/*:any*/, opts/*:ParseOpts*/, date1904/*:?boolean*/) { } else p.w = SSF.format(fmtid,p.v, {date1904:!!date1904}); if(opts.cellDates && fmtid && p.t == 'n' && SSF.is_date(SSF._table[fmtid])) { - var _d = SSF.parse_date_code(p.v); if(_d) { p.t = 'd'; p.v = new Date(Date.UTC(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u)); } + var _d = SSF.parse_date_code(p.v); if(_d) { p.t = 'd'; p.v = new Date(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u); } } } catch(e) { if(opts.WTF) throw e; } } @@ -101,7 +101,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { var cell_valid = true; var XFs = []; /* XF records */ var palette = []; - var Workbook/*:WBWBProps*/ = ({ Sheets:[] }/*:any*/), wsprops = {}; + var Workbook/*:WBWBProps*/ = ({ Sheets:[], WBProps:{date1904:false} }/*:any*/), wsprops = {}; var get_rgb = function getrgb(icv) { if(icv < 8) return XLSIcv[icv]; if(icv < 64) return palette[icv-8] || XLSIcv[icv]; @@ -213,7 +213,9 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { /* nested switch statements to workaround V8 128 limit */ switch(Rn) { /* Workbook Options */ - case 'Date1904': wb.opts.Date1904 = val; break; + case 'Date1904': + /*:: if(!Workbook.WBProps) Workbook.WBProps = {}; */ + wb.opts.Date1904 = Workbook.WBProps.date1904 = val; break; case 'WriteProtect': wb.opts.WriteProtect = true; break; case 'FilePass': if(!opts.enc) blob.l = 0; diff --git a/docbits/52_datatype.md b/docbits/52_datatype.md index 8a3c0fa..45c69d8 100644 --- a/docbits/52_datatype.md +++ b/docbits/52_datatype.md @@ -44,6 +44,9 @@ will not be generated; the parser `sheetStubs` option must be set to `true`. #### Dates +
+ Excel Date Code details (click to show) + By default, Excel stores dates as numbers with a format code that specifies date processing. For example, the date `19-Feb-17` is stored as the number `42785` with a number format of `d-mmm-yy`. The `SSF` module understands number formats @@ -55,3 +58,29 @@ string. The formatter converts the date back to a number. The default behavior for all parsers is to generate number cells. Setting `cellDates` to true will force the generators to store dates. +
+ +
+ Time Zones and Dates (click to show) + +Excel has no native concept of universal time. All times are specified in the +local time zone. Excel limitations prevent specifying true absolute dates. + +Following Excel, this library treats all dates as relative to local time zone. + +
+ +
+ Epochs: 1900 and 1904 (click to show) + +Excel supports two epochs (January 1 1900 and January 1 1904), see +["1900 vs. 1904 Date System" article](http://support2.microsoft.com/kb/180162). +The workbook's epoch can be determined by examining the workbook's +`wb.Workbook.WBProps.date1904` property: + +```js +!!(((wb.Workbook||{}).WBProps||{}).date1904) +``` + +
+ diff --git a/docbits/55_wbobject.md b/docbits/55_wbobject.md index ec73af2..d143c4a 100644 --- a/docbits/55_wbobject.md +++ b/docbits/55_wbobject.md @@ -8,10 +8,5 @@ custom properties. Since the XLS standard properties deviate from the XLSX standard, XLS parsing stores core properties in both places. -`wb.WBProps` includes more workbook-level properties: - -- Excel supports two epochs (January 1 1900 and January 1 1904), see - [1900 vs. 1904 Date System](http://support2.microsoft.com/kb/180162). - The workbook's epoch can be determined by examining the workbook's - `wb.WBProps.date1904` property. +`wb.Workbook` stores [workbook-level attributes](#workbook-level-attributes). diff --git a/docbits/57_wbbook.md b/docbits/57_wbbook.md index a88cfd7..0eaaabe 100644 --- a/docbits/57_wbbook.md +++ b/docbits/57_wbbook.md @@ -1,6 +1,6 @@ ### Workbook-Level Attributes -`wb.Workbook` stores workbook level attributes. +`wb.Workbook` stores workbook-level attributes. #### Defined Names @@ -15,9 +15,18 @@ | `Name` | Case-sensitive name. Standard rules apply ** | | `Ref` | A1-style Reference (e.g. `"Sheet1!$A$1:$D$20"`) | | `Comment` | Comment (only applicable for XLS/XLSX/XLSB) | + Excel allows two sheet-scoped defined names to share the same name. However, a sheet-scoped name cannot collide with a workbook-scope name. Workbook writers may not enforce this constraint. +#### Miscellaneous Workbook Properties + +`wb.Workbook.WBProps` holds other workbook properties: + +| Key | Description | +|:-----------|:----------------------------------------------------| +| `date1904` | epoch: 0/false for 1900 system, 1/true for 1904 | + diff --git a/misc/docs/SUMMARY.md b/misc/docs/SUMMARY.md index ebefceb..8a467ec 100644 --- a/misc/docs/SUMMARY.md +++ b/misc/docs/SUMMARY.md @@ -30,6 +30,7 @@ + [Workbook File Properties](README.md#workbook-file-properties) * [Workbook-Level Attributes](README.md#workbook-level-attributes) + [Defined Names](README.md#defined-names) + + [Miscellaneous Workbook Properties](README.md#miscellaneous-workbook-properties) * [Document Features](README.md#document-features) + [Formulae](README.md#formulae) + [Column Properties](README.md#column-properties) diff --git a/misc/flow.js b/misc/flow.js index 8c73aed..201a4e1 100644 --- a/misc/flow.js +++ b/misc/flow.js @@ -26,12 +26,17 @@ type Workbook = { type WBWBProps = { Sheets: Array; Names?: Array; + WBProps?: WBProps; +}; + +type WBProps = { + date1904?: boolean; }; type WBWSProp = { Hidden?: number; name?: string; -} +}; interface CellAddress { r:number; diff --git a/multiformat.lst b/multiformat.lst index 02ec001..2dcec1c 100644 --- a/multiformat.lst +++ b/multiformat.lst @@ -6,15 +6,12 @@ AutoFilter .xls .xlsb .xlsx .xml # note: XLML only supports sheets, ods does not support dialog BlankSheetTypes .xls .xlsb .xlsm NumberFormatCondition .xls .xlsb .xlsm .xml -#RkNumber .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml RkNumber .xls .xlsb .xlsx .xls.xml #calendar_stress_test .xls .xlsb .xlsx .xml cell_style_simple .xls .xlsb .xlsx .xml # no-csv (newline character \r vs \n) -#comments_stress_test .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml comments_stress_test .xls .xlsb .xlsx .xls.xml # yes-csv -#custom_properties .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml custom_properties .xls .xlsb .xlsx .xls.xml defined_names_simple .xls .xlsb .xlsx .xml # no-csv (randbetween) note: ODS does not support many XLSX functions @@ -23,27 +20,22 @@ formula_stress_test .xls .xlsb .xlsx .xls.xml 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 merge_cells .xls .xlsb .xlsx .xls.xml # no-formula (filename-references in XLSX encoding as [0]) -#named_ranges_2011 .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml named_ranges_2011 .xls .xlsb .xlsx .xls.xml # yes-formula # no-csv (macro serialization in xml) # no-formula (rounding issue in IE8) -#number_format .xls .xlsb .xlsm .xls.xml .xlsb.xml .xlsm.xml number_format .xls .xlsb .xlsm .xls.xml # yes-formula # yes-csv number_format_entities .xls .xlsb .xlsx .xml pivot_table_named_range .xls .xlsb .xlsx .xml pivot_table_test .xls .xlsb .xlsm -#rich_text_stress .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml rich_text_stress .xls .xlsb .xlsx .xls.xml smart_tags_2007 .xls .xlsb .xlsx .xml sushi .xls .xlsb .xlsx .xml text_and_numbers .xls .xlsb .xlsx .xml #time_stress_test_1 .xls .xlsb .xlsx .xml -#xlsx-stream-d-date-cell .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml # no-formula (rounding issue in IE8) xlsx-stream-d-date-cell .xls .xlsb .xlsx .xls.xml diff --git a/test.js b/test.js index 2754e8a..813dd8b 100644 --- a/test.js +++ b/test.js @@ -1502,10 +1502,12 @@ describe('invalid files', function() { }); }); +var basedate = new Date(1899, 11, 30, 0, 0, 0); // 2209161600000 +var dnthresh = basedate.getTime() + (new Date().getTimezoneOffset() - basedate.getTimezoneOffset()) * 60000; function datenum(v/*:Date*/, date1904/*:?boolean*/)/*:number*/ { var epoch = v.getTime(); if(date1904) epoch += 1462*24*60*60*1000; - return (epoch + 2209161600000) / (24 * 60 * 60 * 1000); + return (epoch - dnthresh) / (24 * 60 * 60 * 1000); } describe('json output', function() { @@ -1685,7 +1687,17 @@ describe('csv', function() { var cell = get_cell(X.read(b, opts).Sheets.Sheet1, "C3"); assert.equal(cell.w, '14-02-19'); }); - + it('should interpret dateNF', function() { + var bb = "1,2,3,\nTRUE,FALSE,,sheetjs\nfoo,bar,2/3/14,0.3\n,,,\nbaz,,qux,\n"; + var opts = {type:"binary", cellDates:true, dateNF:'m/d/yy'}; + var cell = get_cell(X.read(bb, opts).Sheets.Sheet1, "C3"); + assert.equal(cell.v.getMonth(), 1); + assert.equal(cell.w, "2/3/14"); + opts = {type:"binary", cellDates:true, dateNF:'d/m/yy'}; + cell = get_cell(X.read(bb, opts).Sheets.Sheet1, "C3"); + assert.equal(cell.v.getMonth(), 2); + assert.equal(cell.w, "2/3/14"); + }); }); describe('output', function(){ var data, ws; diff --git a/types/index.d.ts b/types/index.d.ts index 090a309..6ac060e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -237,6 +237,15 @@ export interface WBProps { /** Defined Names */ Names?: DefinedName[]; + + /** Other Workbook Properties */ + WBProps?: WorkbookProperties; +} + +/** Other Workbook Properties */ +export interface WorkbookProperties { + /** Worksheet Epoch (1904 if true, 1900 if false) */ + date1904?: boolean; } export interface ColInfo { diff --git a/xlsx.flow.js b/xlsx.flow.js index 32a5423..85c56bd 100644 --- a/xlsx.flow.js +++ b/xlsx.flow.js @@ -993,6 +993,33 @@ var XLMLFormatMap/*{[string]:string}*/ = ({ "On/Off": '"Yes";"Yes";"No";@' }/*:any*/); +/* dateNF parse TODO: move to SSF */ +var dateNFregex = /[dD]+|[mM]+|[yYeE]+|[Hh]+|[Ss]+/g; +function dateNF_regex(dateNF/*:string|number*/)/*:RegExp*/ { + var fmt = typeof dateNF == "number" ? SSF._table[dateNF] : dateNF; + fmt = fmt.replace(dateNFregex, "(\\d+)"); + return new RegExp("^" + fmt + "$"); +} +function dateNF_fix(str/*:string*/, dateNF/*:string*/, match/*:Array*/)/*:string*/ { + var Y = -1, m = -1, d = -1, H = -1, M = -1, S = -1; + (dateNF.match(dateNFregex)||[]).forEach(function(n, i) { + var v = parseInt(match[i+1], 10); + switch(n.toLowerCase().charAt(0)) { + case 'y': Y = v; break; case 'd': d = v; break; + case 'h': H = v; break; case 's': S = v; break; + case 'm': if(H >= 0) M = v; else m = v; break; + } + }); + if(S >= 0 && M == -1 && m >= 0) { M = m; m = -1; } + var datestr = (("" + (Y>=0?Y: new Date().getFullYear())).slice(-4) + "-" + ("00" + (m>=1?m:1)).slice(-2) + "-" + ("00" + (d>=1?d:1)).slice(-2)); + if(datestr.length == 7) datestr = "0" + datestr; + if(datestr.length == 8) datestr = "20" + datestr; + var timestr = (("00" + (H>=0?H:0)).slice(-2) + ":" + ("00" + (M>=0?M:0)).slice(-2) + ":" + ("00" + (S>=0?S:0)).slice(-2)); + if(H == -1 && M == -1 && S == -1) return datestr; + if(Y == -1 && m == -1 && d == -1) return timestr; + return datestr + "T" + timestr; +} + var DO_NOT_EXPORT_CFB = true; /*:: declare var Base64:any; @@ -1435,22 +1462,17 @@ function evert_arr(obj/*:any*/)/*:EvertArrType*/ { return o; } +var basedate = new Date(1899, 11, 30, 0, 0, 0); // 2209161600000 +var dnthresh = basedate.getTime() + (new Date().getTimezoneOffset() - basedate.getTimezoneOffset()) * 60000; function datenum(v/*:Date*/, date1904/*:?boolean*/)/*:number*/ { var epoch = v.getTime(); if(date1904) epoch += 1462*24*60*60*1000; - return (epoch + 2209161600000) / (24 * 60 * 60 * 1000); + return (epoch - dnthresh) / (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; + var out = new Date(); + out.setTime(v * 24 * 60 * 60 * 1000 + dnthresh); + return out; } /* ISO 8601 Duration */ @@ -1483,9 +1505,15 @@ function parse_isodur(s) { var good_pd_date = new Date('2017-02-19T19:06:09.000Z'); if(isNaN(good_pd_date.getFullYear())) good_pd_date = new Date('2/19/17'); var good_pd = good_pd_date.getFullYear() == 2017; -function parseDate(str/*:string|Date*/)/*:Date*/ { +/* parses aa date as a local date */ +function parseDate(str/*:string|Date*/, fixdate/*:?number*/)/*:Date*/ { var d = new Date(str); - if(good_pd) return d; + if(good_pd) { + /*:: if(fixdate == null) fixdate = 0; */ + if(fixdate > 0) d.setTime(d.getTime() + d.getTimezoneOffset() * 60 * 1000); + else if(fixdate < 0) d.setTime(d.getTime() - d.getTimezoneOffset() * 60 * 1000); + return d; + } if(str instanceof Date) return str; if(good_pd_date.getFullYear() == 1917 && !isNaN(d.getFullYear())) { var s = d.getFullYear(); @@ -1493,7 +1521,7 @@ function parseDate(str/*:string|Date*/)/*:Date*/ { d.setFullYear(d.getFullYear() + 100); return d; } var n = str.match(/\d+/g)||["2017","2","19","0","0","0"]; - return new Date(Date.UTC(+n[0], +n[1] - 1, +n[2], (+n[3]||0), (+n[4]||0), (+n[5]||0))); + return new Date(+n[0], +n[1] - 1, +n[2], (+n[3]||0), (+n[4]||0), (+n[5]||0)); } function cc2str(arr/*:Array*/)/*:string*/ { @@ -1681,7 +1709,7 @@ var xlml_unfixstr/*:StringConv*/ = (function() { function parsexmlbool(value/*:any*/, tag/*:?string*/)/*:boolean*/ { switch(value) { - case '1': case 'true': case 'TRUE': return true; + case 1: case true: case '1': case 'true': case 'TRUE': return true; /* case '0': case 'false': case 'FALSE':*/ default: return false; } @@ -5666,6 +5694,7 @@ var PRN = (function() { var R = 0, C = 0, v = 0; var start = 0, end = 0, sepcc = sep.charCodeAt(0), instr = false, cc=0; str = str.replace(/\r\n/mg, "\n"); + var _re/*:?RegExp*/ = o.dateNF != null ? dateNF_regex(o.dateNF) : null; function finish_cell() { var s = str.slice(start, end); var cell = ({}/*:any*/); @@ -5673,10 +5702,12 @@ var PRN = (function() { else if(s == "TRUE") { cell.t = 'b'; cell.v = true; } else if(s == "FALSE") { cell.t = 'b'; cell.v = false; } else if(!isNaN(v = +s)) { cell.t = 'n'; cell.w = s; cell.v = v; } - else if(!isNaN(fuzzydate(s).getDate())) { + else if(!isNaN(fuzzydate(s).getDate()) || _re && s.match(_re)) { cell.z = o.dateNF || SSF._table[14]; - if(o.cellDates) { cell.t = 'd'; cell.v = parseDate(s); } - else { cell.t = 'n'; cell.v = datenum(parseDate(s)); } + var k = 0; + if(_re && s.match(_re)){ s=dateNF_fix(s, o.dateNF, (s.match(_re)||[])); k=1; } + if(o.cellDates) { cell.t = 'd'; cell.v = parseDate(s, k); } + else { cell.t = 'n'; cell.v = datenum(parseDate(s, k)); } cell.w = SSF.format(cell.z, cell.v instanceof Date ? datenum(cell.v):cell.v); } else { cell.t = 's'; @@ -10849,7 +10880,7 @@ function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { case 'n': vv = ''+cell.v; break; case 'e': vv = BErr[cell.v]; break; case 'd': - if(opts.cellDates) vv = parseDate(cell.v).toISOString(); + if(opts.cellDates) vv = parseDate(cell.v, -1).toISOString(); else { cell.t = 'n'; vv = ''+(cell.v = datenum(parseDate(cell.v))); @@ -11002,7 +11033,8 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { break; case 'b': p.v = parsexmlbool(p.v); break; case 'd': - if(!opts.cellDates) { p.v = datenum(parseDate(p.v)); p.t = 'n'; } + if(opts.cellDates) p.v = parseDate(p.v, 1); + else { p.v = datenum(parseDate(p.v, 1)); p.t = 'n'; } break; /* error string in .w, number in .v */ case 'e': @@ -11019,9 +11051,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { } } safe_format(p, fmtid, fillid, opts, themes, styles); - if(opts.cellDates && do_format && p.t == 'n' && SSF.is_date(SSF._table[fmtid])) { - var _d = SSF.parse_date_code(p.v); if(_d) { p.t = 'd'; p.v = new Date(Date.UTC(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u)); } - } + if(opts.cellDates && do_format && p.t == 'n' && SSF.is_date(SSF._table[fmtid])) { p.t = 'd'; p.v = numdate(p.v); } if(dense) { var _r = decode_cell(tag.r); if(!s[_r.r]) s[_r.r] = []; @@ -11638,7 +11668,7 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ { if(refguess.e.r < row.r) refguess.e.r = row.r; if(refguess.e.c < C) refguess.e.c = C; if(opts.cellDates && cf && p.t == 'n' && SSF.is_date(SSF._table[cf.ifmt])) { - var _d = SSF.parse_date_code(p.v); if(_d) { p.t = 'd'; p.v = new Date(Date.UTC(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u)); } + var _d = SSF.parse_date_code(p.v); if(_d) { p.t = 'd'; p.v = new Date(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u); } } break; @@ -12120,38 +12150,37 @@ function write_cs_bin(idx/*:number*/, opts, wb/*:Workbook*/, rels) { } /* 18.2.28 (CT_WorkbookProtection) Defaults */ var WBPropsDef = [ - ['allowRefreshQuery', '0'], - ['autoCompressPictures', '1'], - ['backupFile', '0'], - ['checkCompatibility', '0'], - ['codeName', ''], - ['date1904', '0'], - ['dateCompatibility', '1'], - //['defaultThemeVersion', '0'], - ['filterPrivacy', '0'], - ['hidePivotFieldList', '0'], - ['promptedSolutions', '0'], - ['publishItems', '0'], - ['refreshAllConnections', false], - ['saveExternalLinkValues', '1'], - ['showBorderUnselectedTables', '1'], - ['showInkAnnotation', '1'], - ['showObjects', 'all'], - ['showPivotChartFilter', '0'] - //['updateLinks', 'userSet'] + ['allowRefreshQuery', false, "bool"], + ['autoCompressPictures', true, "bool"], + ['backupFile', false, "bool"], + ['checkCompatibility', false, "bool"], + ['codeName', ''], + ['date1904', false, "bool"], + ['defaultThemeVersion', 0, "int"], + ['filterPrivacy', false, "bool"], + ['hidePivotFieldList', false, "bool"], + ['promptedSolutions', false, "bool"], + ['publishItems', false, "bool"], + ['refreshAllConnections', false, "bool"], + ['saveExternalLinkValues', true, "bool"], + ['showBorderUnselectedTables', true, "bool"], + ['showInkAnnotation', true, "bool"], + ['showObjects', 'all'], + ['showPivotChartFilter', false, "bool"], + ['updateLinks', 'userSet'] ]; /* 18.2.30 (CT_BookView) Defaults */ var WBViewDef = [ - ['activeTab', '0'], - ['autoFilterDateGrouping', '1'], - ['firstSheet', '0'], - ['minimized', '0'], - ['showHorizontalScroll', '1'], - ['showSheetTabs', '1'], - ['showVerticalScroll', '1'], - ['tabRatio', '600'], - ['visibility', 'visible'] + ['activeTab', 0, "int"], + ['autoFilterDateGrouping', true, "bool"], + ['firstSheet', 0, "int"], + ['minimized', false, "bool"], + ['showHorizontalScroll', true, "bool"], + ['showSheetTabs', true, "bool"], + ['showVerticalScroll', true, "bool"], + ['tabRatio', 600, "int"], + ['visibility', 'visible'] //window{Height,Width}, {x,y}Window ]; @@ -12200,12 +12229,20 @@ function push_defaults_array(target, defaults) { for(var j = 0; j != target.length; ++j) { var w = target[j]; for(var i=0; i != defaults.length; ++i) { var z = defaults[i]; if(w[z[0]] == null) w[z[0]] = z[1]; + else switch(z[2]) { + case "bool": if(typeof w[z[0]] == "string") w[z[0]] = parsexmlbool(w[z[0]], z[0]); break; + case "int": if(typeof w[z[0]] == "string") w[z[0]] = parseInt(w[z[0]], 10); break; + } } } } function push_defaults(target, defaults) { for(var i = 0; i != defaults.length; ++i) { var z = defaults[i]; if(target[z[0]] == null) target[z[0]] = z[1]; + else switch(z[2]) { + case "bool": if(typeof target[z[0]] == "string") target[z[0]] = parsexmlbool(target[z[0]], z[0]); break; + case "int": if(typeof target[z[0]] == "string") target[z[0]] = parseInt(target[z[0]], 10); break; + } } } @@ -12219,6 +12256,13 @@ function parse_wb_defaults(wb) { _ssfopts.date1904 = parsexmlbool(wb.WBProps.date1904, 'date1904'); } +function safe1904(wb/*:Workbook*/)/*:string*/ { + /* TODO: store date1904 somewhere else */ + if(!wb.Workbook) return "false"; + if(!wb.Workbook.WBProps) return "false"; + return parsexmlbool(wb.Workbook.WBProps.date1904) ? "true" : "false"; +} + var badchars = "][*?\/\\".split(""); function check_ws_name(n/*:string*/, safe/*:?boolean*/)/*:boolean*/ { if(n.length > 31) { if(safe) return false; throw new Error("Sheet names cannot exceed 31 chars"); } @@ -12269,8 +12313,17 @@ function parse_wb_xml(data, opts)/*:WorkbookFile*/ { case '': break; /* 18.2.28 workbookPr CT_WorkbookPr ? */ - case '': delete y[0]; wb.WBProps = y; break; + case '': + WBPropsDef.forEach(function(w) { + if(y[w[0]] == null) return; + switch(w[2]) { + case "bool": wb.WBProps[w[0]] = parsexmlbool(y[w[0]], w[0]); break; + case "int": wb.WBProps[w[0]] = parseInt(y[w[0]], 10); break; + default: wb.WBProps[w[0]] = y[w[0]]; + } + }); + break; case '': break; /* 18.2.29 workbookProtection CT_WorkbookProtection ? */ @@ -12391,14 +12444,6 @@ var WB_XML_ROOT = writextag('workbook', null, { 'xmlns:r': XMLNS.r }); -function safe1904(wb/*:Workbook*/)/*:string*/ { - /* TODO: store date1904 somewhere else */ - if(!wb.Workbook) return "false"; - if(!wb.Workbook.WBProps) return "false"; - // $FlowIgnore - return parsexmlbool(wb.Workbook.WBProps.date1904) ? "true" : "false"; -} - function write_wb_xml(wb/*:Workbook*/, opts/*:?WriteOpts*/)/*:string*/ { var o = [XML_HEADER]; o[o.length] = WB_XML_ROOT; @@ -12476,10 +12521,27 @@ function write_BrtBundleSh(data, o) { /* [MS-XLSB] 2.4.807 BrtWbProp */ function parse_BrtWbProp(data, length) { - data.read_shift(4); - var dwThemeVersion = data.read_shift(4); + var o = {}; + var flags = data.read_shift(4); + o.defaultThemeVersion = data.read_shift(4); var strName = (length > 8) ? parse_XLWideString(data) : ""; - return [dwThemeVersion, strName]; + if(strName.length > 0) o.codeName = strName; + o.autoCompressPictures = !!(flags & 0x10000); + o.backupFile = !!(flags & 0x40); + o.checkCompatibility = !!(flags & 0x1000); + o.date1904 = !!(flags & 0x01); + o.filterPrivacy = !!(flags & 0x08); + o.hidePivotFieldList = !!(flags & 0x400); + o.promptedSolutions = !!(flags & 0x10); + o.publishItems = !!(flags & 0x800); + o.refreshAllConnections = !!(flags & 0x40000); + o.saveExternalLinkValues = !!(flags & 0x80); + o.showBorderUnselectedTables = !!(flags & 0x04); + o.showInkAnnotation = !!(flags & 0x20); + o.showObjects = ["all", "placeholders", "none"][(flags >> 13) & 0x03]; + o.showPivotChartFilter = !!(flags & 0x8000); + o.updateLinks = ["userSet", "never", "always"][(flags >> 8) & 0x03]; + return o; } function write_BrtWbProp(data, o) { if(!o) o = new_buf(72); @@ -12543,6 +12605,9 @@ function parse_wb_bin(data, opts)/*:WorkbookFile*/ { break; case 0x040C: /* 'BrtNameExt' */ break; + case 0x0099: /* 'BrtWbProp' */ + wb.WBProps = val; break; + /* case 'BrtModelTimeGroupingCalcCol' */ /* case 'BrtRevisionPtr' */ /* case 'BrtUid' */ @@ -12574,7 +12639,6 @@ function parse_wb_bin(data, opts)/*:WorkbookFile*/ { case 0x0822: /* 'BrtTimelineCachePivotCacheID' */ case 0x018D: /* 'BrtUserBookView' */ case 0x009A: /* 'BrtWbFactoid' */ - case 0x0099: /* 'BrtWbProp' */ case 0x045D: /* 'BrtWbProp14' */ case 0x0229: /* 'BrtWebOpt' */ case 0x082B: /* 'BrtWorkBookPr15' */ @@ -12861,7 +12925,7 @@ function safe_format_xlml(cell/*:Cell*/, nf, o) { var z = XLMLFormatMap[nf]||nf||"General"; if(o.cellNF) cell.z = z; if(o.cellDates && cell.t == 'n' && SSF.is_date(z)) { - var _d = SSF.parse_date_code(cell.v); if(_d) { cell.t = 'd'; cell.v = new Date(Date.UTC(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u)); } + var _d = SSF.parse_date_code(cell.v); if(_d) { cell.t = 'd'; cell.v = new Date(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u); } } } catch(e) { if(o.WTF) throw e; } } @@ -12978,7 +13042,7 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { var cstys = [], csty, seencol = false; var arrayf = []; var rowinfo = [], rowobj = {}; - var Workbook/*:WBWBProps*/ = { Sheets:[] }, wsprops = {}; + var Workbook/*:WBWBProps*/ = ({ Sheets:[], WBProps:{date1904:false} }/*:any*/), wsprops = {}; xlmlregex.lastIndex = 0; str = str.replace(//mg,""); while((Rn = xlmlregex.exec(str))) switch(Rn[3]) { @@ -13259,6 +13323,10 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { /* ExcelWorkbook */ case 'ExcelWorkbook': switch(Rn[3]) { + case 'Date1904': + /*:: if(!Workbook.WBProps) Workbook.WBProps = {}; */ + Workbook.WBProps.date1904 = true; + break; case 'WindowHeight': break; case 'WindowWidth': break; case 'WindowTopX': break; @@ -13277,7 +13345,6 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { case 'Dll': break; case 'AcceptLabelsInFormulas': break; case 'DoNotSaveLinkValues': break; - case 'Date1904': break; case 'Iteration': break; case 'MaxIterations': break; case 'MaxChange': break; @@ -13938,7 +14005,7 @@ function safe_format_xf(p/*:any*/, opts/*:ParseOpts*/, date1904/*:?boolean*/) { } else p.w = SSF.format(fmtid,p.v, {date1904:!!date1904}); if(opts.cellDates && fmtid && p.t == 'n' && SSF.is_date(SSF._table[fmtid])) { - var _d = SSF.parse_date_code(p.v); if(_d) { p.t = 'd'; p.v = new Date(Date.UTC(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u)); } + var _d = SSF.parse_date_code(p.v); if(_d) { p.t = 'd'; p.v = new Date(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u); } } } catch(e) { if(opts.WTF) throw e; } } @@ -13968,7 +14035,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { var cell_valid = true; var XFs = []; /* XF records */ var palette = []; - var Workbook/*:WBWBProps*/ = ({ Sheets:[] }/*:any*/), wsprops = {}; + var Workbook/*:WBWBProps*/ = ({ Sheets:[], WBProps:{date1904:false} }/*:any*/), wsprops = {}; var get_rgb = function getrgb(icv) { if(icv < 8) return XLSIcv[icv]; if(icv < 64) return palette[icv-8] || XLSIcv[icv]; @@ -14080,7 +14147,9 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { /* nested switch statements to workaround V8 128 limit */ switch(Rn) { /* Workbook Options */ - case 'Date1904': wb.opts.Date1904 = val; break; + case 'Date1904': + /*:: if(!Workbook.WBProps) Workbook.WBProps = {}; */ + wb.opts.Date1904 = Workbook.WBProps.date1904 = val; break; case 'WriteProtect': wb.opts.WriteProtect = true; break; case 'FilePass': if(!opts.enc) blob.l = 0; diff --git a/xlsx.js b/xlsx.js index 3193e9e..8e03d2f 100644 --- a/xlsx.js +++ b/xlsx.js @@ -967,6 +967,33 @@ var XLMLFormatMap/*{[string]:string}*/ = ({ "On/Off": '"Yes";"Yes";"No";@' }); +/* dateNF parse TODO: move to SSF */ +var dateNFregex = /[dD]+|[mM]+|[yYeE]+|[Hh]+|[Ss]+/g; +function dateNF_regex(dateNF) { + var fmt = typeof dateNF == "number" ? SSF._table[dateNF] : dateNF; + fmt = fmt.replace(dateNFregex, "(\\d+)"); + return new RegExp("^" + fmt + "$"); +} +function dateNF_fix(str, dateNF, match) { + var Y = -1, m = -1, d = -1, H = -1, M = -1, S = -1; + (dateNF.match(dateNFregex)||[]).forEach(function(n, i) { + var v = parseInt(match[i+1], 10); + switch(n.toLowerCase().charAt(0)) { + case 'y': Y = v; break; case 'd': d = v; break; + case 'h': H = v; break; case 's': S = v; break; + case 'm': if(H >= 0) M = v; else m = v; break; + } + }); + if(S >= 0 && M == -1 && m >= 0) { M = m; m = -1; } + var datestr = (("" + (Y>=0?Y: new Date().getFullYear())).slice(-4) + "-" + ("00" + (m>=1?m:1)).slice(-2) + "-" + ("00" + (d>=1?d:1)).slice(-2)); + if(datestr.length == 7) datestr = "0" + datestr; + if(datestr.length == 8) datestr = "20" + datestr; + var timestr = (("00" + (H>=0?H:0)).slice(-2) + ":" + ("00" + (M>=0?M:0)).slice(-2) + ":" + ("00" + (S>=0?S:0)).slice(-2)); + if(H == -1 && M == -1 && S == -1) return datestr; + if(Y == -1 && m == -1 && d == -1) return timestr; + return datestr + "T" + timestr; +} + var DO_NOT_EXPORT_CFB = true; /* cfb.js (C) 2013-present SheetJS -- http://sheetjs.com */ /* vim: set ts=2: */ @@ -1385,22 +1412,17 @@ function evert_arr(obj) { return o; } +var basedate = new Date(1899, 11, 30, 0, 0, 0); // 2209161600000 +var dnthresh = basedate.getTime() + (new Date().getTimezoneOffset() - basedate.getTimezoneOffset()) * 60000; function datenum(v, date1904) { var epoch = v.getTime(); if(date1904) epoch += 1462*24*60*60*1000; - return (epoch + 2209161600000) / (24 * 60 * 60 * 1000); + return (epoch - dnthresh) / (24 * 60 * 60 * 1000); } function numdate(v) { - 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; + var out = new Date(); + out.setTime(v * 24 * 60 * 60 * 1000 + dnthresh); + return out; } /* ISO 8601 Duration */ @@ -1433,9 +1455,14 @@ function parse_isodur(s) { var good_pd_date = new Date('2017-02-19T19:06:09.000Z'); if(isNaN(good_pd_date.getFullYear())) good_pd_date = new Date('2/19/17'); var good_pd = good_pd_date.getFullYear() == 2017; -function parseDate(str) { +/* parses aa date as a local date */ +function parseDate(str, fixdate) { var d = new Date(str); - if(good_pd) return d; + if(good_pd) { +if(fixdate > 0) d.setTime(d.getTime() + d.getTimezoneOffset() * 60 * 1000); + else if(fixdate < 0) d.setTime(d.getTime() - d.getTimezoneOffset() * 60 * 1000); + return d; + } if(str instanceof Date) return str; if(good_pd_date.getFullYear() == 1917 && !isNaN(d.getFullYear())) { var s = d.getFullYear(); @@ -1443,7 +1470,7 @@ function parseDate(str) { d.setFullYear(d.getFullYear() + 100); return d; } var n = str.match(/\d+/g)||["2017","2","19","0","0","0"]; - return new Date(Date.UTC(+n[0], +n[1] - 1, +n[2], (+n[3]||0), (+n[4]||0), (+n[5]||0))); + return new Date(+n[0], +n[1] - 1, +n[2], (+n[3]||0), (+n[4]||0), (+n[5]||0)); } function cc2str(arr) { @@ -1630,7 +1657,7 @@ var xlml_unfixstr = (function() { function parsexmlbool(value, tag) { switch(value) { - case '1': case 'true': case 'TRUE': return true; + case 1: case true: case '1': case 'true': case 'TRUE': return true; /* case '0': case 'false': case 'FALSE':*/ default: return false; } @@ -5605,6 +5632,7 @@ var PRN = (function() { var R = 0, C = 0, v = 0; var start = 0, end = 0, sepcc = sep.charCodeAt(0), instr = false, cc=0; str = str.replace(/\r\n/mg, "\n"); + var _re = o.dateNF != null ? dateNF_regex(o.dateNF) : null; function finish_cell() { var s = str.slice(start, end); var cell = ({}); @@ -5612,10 +5640,12 @@ var PRN = (function() { else if(s == "TRUE") { cell.t = 'b'; cell.v = true; } else if(s == "FALSE") { cell.t = 'b'; cell.v = false; } else if(!isNaN(v = +s)) { cell.t = 'n'; cell.w = s; cell.v = v; } - else if(!isNaN(fuzzydate(s).getDate())) { + else if(!isNaN(fuzzydate(s).getDate()) || _re && s.match(_re)) { cell.z = o.dateNF || SSF._table[14]; - if(o.cellDates) { cell.t = 'd'; cell.v = parseDate(s); } - else { cell.t = 'n'; cell.v = datenum(parseDate(s)); } + var k = 0; + if(_re && s.match(_re)){ s=dateNF_fix(s, o.dateNF, (s.match(_re)||[])); k=1; } + if(o.cellDates) { cell.t = 'd'; cell.v = parseDate(s, k); } + else { cell.t = 'n'; cell.v = datenum(parseDate(s, k)); } cell.w = SSF.format(cell.z, cell.v instanceof Date ? datenum(cell.v):cell.v); } else { cell.t = 's'; @@ -10784,7 +10814,7 @@ function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { case 'n': vv = ''+cell.v; break; case 'e': vv = BErr[cell.v]; break; case 'd': - if(opts.cellDates) vv = parseDate(cell.v).toISOString(); + if(opts.cellDates) vv = parseDate(cell.v, -1).toISOString(); else { cell.t = 'n'; vv = ''+(cell.v = datenum(parseDate(cell.v))); @@ -10937,7 +10967,8 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { break; case 'b': p.v = parsexmlbool(p.v); break; case 'd': - if(!opts.cellDates) { p.v = datenum(parseDate(p.v)); p.t = 'n'; } + if(opts.cellDates) p.v = parseDate(p.v, 1); + else { p.v = datenum(parseDate(p.v, 1)); p.t = 'n'; } break; /* error string in .w, number in .v */ case 'e': @@ -10954,9 +10985,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { } } safe_format(p, fmtid, fillid, opts, themes, styles); - if(opts.cellDates && do_format && p.t == 'n' && SSF.is_date(SSF._table[fmtid])) { - var _d = SSF.parse_date_code(p.v); if(_d) { p.t = 'd'; p.v = new Date(Date.UTC(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u)); } - } + if(opts.cellDates && do_format && p.t == 'n' && SSF.is_date(SSF._table[fmtid])) { p.t = 'd'; p.v = numdate(p.v); } if(dense) { var _r = decode_cell(tag.r); if(!s[_r.r]) s[_r.r] = []; @@ -11572,7 +11601,7 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles) { if(refguess.e.r < row.r) refguess.e.r = row.r; if(refguess.e.c < C) refguess.e.c = C; if(opts.cellDates && cf && p.t == 'n' && SSF.is_date(SSF._table[cf.ifmt])) { - var _d = SSF.parse_date_code(p.v); if(_d) { p.t = 'd'; p.v = new Date(Date.UTC(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u)); } + var _d = SSF.parse_date_code(p.v); if(_d) { p.t = 'd'; p.v = new Date(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u); } } break; @@ -12054,38 +12083,37 @@ function write_cs_bin(idx, opts, wb, rels) { } /* 18.2.28 (CT_WorkbookProtection) Defaults */ var WBPropsDef = [ - ['allowRefreshQuery', '0'], - ['autoCompressPictures', '1'], - ['backupFile', '0'], - ['checkCompatibility', '0'], - ['codeName', ''], - ['date1904', '0'], - ['dateCompatibility', '1'], - //['defaultThemeVersion', '0'], - ['filterPrivacy', '0'], - ['hidePivotFieldList', '0'], - ['promptedSolutions', '0'], - ['publishItems', '0'], - ['refreshAllConnections', false], - ['saveExternalLinkValues', '1'], - ['showBorderUnselectedTables', '1'], - ['showInkAnnotation', '1'], - ['showObjects', 'all'], - ['showPivotChartFilter', '0'] - //['updateLinks', 'userSet'] + ['allowRefreshQuery', false, "bool"], + ['autoCompressPictures', true, "bool"], + ['backupFile', false, "bool"], + ['checkCompatibility', false, "bool"], + ['codeName', ''], + ['date1904', false, "bool"], + ['defaultThemeVersion', 0, "int"], + ['filterPrivacy', false, "bool"], + ['hidePivotFieldList', false, "bool"], + ['promptedSolutions', false, "bool"], + ['publishItems', false, "bool"], + ['refreshAllConnections', false, "bool"], + ['saveExternalLinkValues', true, "bool"], + ['showBorderUnselectedTables', true, "bool"], + ['showInkAnnotation', true, "bool"], + ['showObjects', 'all'], + ['showPivotChartFilter', false, "bool"], + ['updateLinks', 'userSet'] ]; /* 18.2.30 (CT_BookView) Defaults */ var WBViewDef = [ - ['activeTab', '0'], - ['autoFilterDateGrouping', '1'], - ['firstSheet', '0'], - ['minimized', '0'], - ['showHorizontalScroll', '1'], - ['showSheetTabs', '1'], - ['showVerticalScroll', '1'], - ['tabRatio', '600'], - ['visibility', 'visible'] + ['activeTab', 0, "int"], + ['autoFilterDateGrouping', true, "bool"], + ['firstSheet', 0, "int"], + ['minimized', false, "bool"], + ['showHorizontalScroll', true, "bool"], + ['showSheetTabs', true, "bool"], + ['showVerticalScroll', true, "bool"], + ['tabRatio', 600, "int"], + ['visibility', 'visible'] //window{Height,Width}, {x,y}Window ]; @@ -12134,12 +12162,20 @@ function push_defaults_array(target, defaults) { for(var j = 0; j != target.length; ++j) { var w = target[j]; for(var i=0; i != defaults.length; ++i) { var z = defaults[i]; if(w[z[0]] == null) w[z[0]] = z[1]; + else switch(z[2]) { + case "bool": if(typeof w[z[0]] == "string") w[z[0]] = parsexmlbool(w[z[0]], z[0]); break; + case "int": if(typeof w[z[0]] == "string") w[z[0]] = parseInt(w[z[0]], 10); break; + } } } } function push_defaults(target, defaults) { for(var i = 0; i != defaults.length; ++i) { var z = defaults[i]; if(target[z[0]] == null) target[z[0]] = z[1]; + else switch(z[2]) { + case "bool": if(typeof target[z[0]] == "string") target[z[0]] = parsexmlbool(target[z[0]], z[0]); break; + case "int": if(typeof target[z[0]] == "string") target[z[0]] = parseInt(target[z[0]], 10); break; + } } } @@ -12153,6 +12189,13 @@ function parse_wb_defaults(wb) { _ssfopts.date1904 = parsexmlbool(wb.WBProps.date1904, 'date1904'); } +function safe1904(wb) { + /* TODO: store date1904 somewhere else */ + if(!wb.Workbook) return "false"; + if(!wb.Workbook.WBProps) return "false"; + return parsexmlbool(wb.Workbook.WBProps.date1904) ? "true" : "false"; +} + var badchars = "][*?\/\\".split(""); function check_ws_name(n, safe) { if(n.length > 31) { if(safe) return false; throw new Error("Sheet names cannot exceed 31 chars"); } @@ -12203,8 +12246,17 @@ function parse_wb_xml(data, opts) { case '': break; /* 18.2.28 workbookPr CT_WorkbookPr ? */ - case '': delete y[0]; wb.WBProps = y; break; + case '': + WBPropsDef.forEach(function(w) { + if(y[w[0]] == null) return; + switch(w[2]) { + case "bool": wb.WBProps[w[0]] = parsexmlbool(y[w[0]], w[0]); break; + case "int": wb.WBProps[w[0]] = parseInt(y[w[0]], 10); break; + default: wb.WBProps[w[0]] = y[w[0]]; + } + }); + break; case '': break; /* 18.2.29 workbookProtection CT_WorkbookProtection ? */ @@ -12325,14 +12377,6 @@ var WB_XML_ROOT = writextag('workbook', null, { 'xmlns:r': XMLNS.r }); -function safe1904(wb) { - /* TODO: store date1904 somewhere else */ - if(!wb.Workbook) return "false"; - if(!wb.Workbook.WBProps) return "false"; - // $FlowIgnore - return parsexmlbool(wb.Workbook.WBProps.date1904) ? "true" : "false"; -} - function write_wb_xml(wb, opts) { var o = [XML_HEADER]; o[o.length] = WB_XML_ROOT; @@ -12410,10 +12454,27 @@ function write_BrtBundleSh(data, o) { /* [MS-XLSB] 2.4.807 BrtWbProp */ function parse_BrtWbProp(data, length) { - data.read_shift(4); - var dwThemeVersion = data.read_shift(4); + var o = {}; + var flags = data.read_shift(4); + o.defaultThemeVersion = data.read_shift(4); var strName = (length > 8) ? parse_XLWideString(data) : ""; - return [dwThemeVersion, strName]; + if(strName.length > 0) o.codeName = strName; + o.autoCompressPictures = !!(flags & 0x10000); + o.backupFile = !!(flags & 0x40); + o.checkCompatibility = !!(flags & 0x1000); + o.date1904 = !!(flags & 0x01); + o.filterPrivacy = !!(flags & 0x08); + o.hidePivotFieldList = !!(flags & 0x400); + o.promptedSolutions = !!(flags & 0x10); + o.publishItems = !!(flags & 0x800); + o.refreshAllConnections = !!(flags & 0x40000); + o.saveExternalLinkValues = !!(flags & 0x80); + o.showBorderUnselectedTables = !!(flags & 0x04); + o.showInkAnnotation = !!(flags & 0x20); + o.showObjects = ["all", "placeholders", "none"][(flags >> 13) & 0x03]; + o.showPivotChartFilter = !!(flags & 0x8000); + o.updateLinks = ["userSet", "never", "always"][(flags >> 8) & 0x03]; + return o; } function write_BrtWbProp(data, o) { if(!o) o = new_buf(72); @@ -12477,6 +12538,9 @@ function parse_wb_bin(data, opts) { break; case 0x040C: /* 'BrtNameExt' */ break; + case 0x0099: /* 'BrtWbProp' */ + wb.WBProps = val; break; + /* case 'BrtModelTimeGroupingCalcCol' */ /* case 'BrtRevisionPtr' */ /* case 'BrtUid' */ @@ -12508,7 +12572,6 @@ function parse_wb_bin(data, opts) { case 0x0822: /* 'BrtTimelineCachePivotCacheID' */ case 0x018D: /* 'BrtUserBookView' */ case 0x009A: /* 'BrtWbFactoid' */ - case 0x0099: /* 'BrtWbProp' */ case 0x045D: /* 'BrtWbProp14' */ case 0x0229: /* 'BrtWebOpt' */ case 0x082B: /* 'BrtWorkBookPr15' */ @@ -12793,7 +12856,7 @@ function safe_format_xlml(cell, nf, o) { var z = XLMLFormatMap[nf]||nf||"General"; if(o.cellNF) cell.z = z; if(o.cellDates && cell.t == 'n' && SSF.is_date(z)) { - var _d = SSF.parse_date_code(cell.v); if(_d) { cell.t = 'd'; cell.v = new Date(Date.UTC(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u)); } + var _d = SSF.parse_date_code(cell.v); if(_d) { cell.t = 'd'; cell.v = new Date(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u); } } } catch(e) { if(o.WTF) throw e; } } @@ -12910,7 +12973,7 @@ function parse_xlml_xml(d, opts) { var cstys = [], csty, seencol = false; var arrayf = []; var rowinfo = [], rowobj = {}; - var Workbook = { Sheets:[] }, wsprops = {}; + var Workbook = ({ Sheets:[], WBProps:{date1904:false} }), wsprops = {}; xlmlregex.lastIndex = 0; str = str.replace(//mg,""); while((Rn = xlmlregex.exec(str))) switch(Rn[3]) { @@ -13190,6 +13253,9 @@ Workbook.Names.push(_DefinedName); /* ExcelWorkbook */ case 'ExcelWorkbook': switch(Rn[3]) { + case 'Date1904': +Workbook.WBProps.date1904 = true; + break; case 'WindowHeight': break; case 'WindowWidth': break; case 'WindowTopX': break; @@ -13208,7 +13274,6 @@ Workbook.Names.push(_DefinedName); case 'Dll': break; case 'AcceptLabelsInFormulas': break; case 'DoNotSaveLinkValues': break; - case 'Date1904': break; case 'Iteration': break; case 'MaxIterations': break; case 'MaxChange': break; @@ -13868,7 +13933,7 @@ function safe_format_xf(p, opts, date1904) { } else p.w = SSF.format(fmtid,p.v, {date1904:!!date1904}); if(opts.cellDates && fmtid && p.t == 'n' && SSF.is_date(SSF._table[fmtid])) { - var _d = SSF.parse_date_code(p.v); if(_d) { p.t = 'd'; p.v = new Date(Date.UTC(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u)); } + var _d = SSF.parse_date_code(p.v); if(_d) { p.t = 'd'; p.v = new Date(_d.y, _d.m-1,_d.d,_d.H,_d.M,_d.S,_d.u); } } } catch(e) { if(opts.WTF) throw e; } } @@ -13898,7 +13963,7 @@ function parse_workbook(blob, options) { var cell_valid = true; var XFs = []; /* XF records */ var palette = []; - var Workbook = ({ Sheets:[] }), wsprops = {}; + var Workbook = ({ Sheets:[], WBProps:{date1904:false} }), wsprops = {}; var get_rgb = function getrgb(icv) { if(icv < 8) return XLSIcv[icv]; if(icv < 64) return palette[icv-8] || XLSIcv[icv]; @@ -14010,7 +14075,8 @@ function parse_workbook(blob, options) { /* nested switch statements to workaround V8 128 limit */ switch(Rn) { /* Workbook Options */ - case 'Date1904': wb.opts.Date1904 = val; break; + case 'Date1904': +wb.opts.Date1904 = Workbook.WBProps.date1904 = val; break; case 'WriteProtect': wb.opts.WriteProtect = true; break; case 'FilePass': if(!opts.enc) blob.l = 0;