Skip to content

Commit

Permalink
Date/Time Minutiae
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
SheetJSDev committed Jun 1, 2017
1 parent 118e9ad commit aff7b95
Show file tree
Hide file tree
Showing 23 changed files with 548 additions and 241 deletions.
17 changes: 17 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
language: node_js
node_js:
- "8"
- "7"
- "6"
# note: travis has been acting up on old versions of node
Expand All @@ -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"
Expand Down
48 changes: 41 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -719,6 +720,9 @@ will not be generated; the parser `sheetStubs` option must be set to `true`.

#### Dates

<details>
<summary><b>Excel Date Code details</b> (click to show)</summary>

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
Expand All @@ -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.

</details>

<details>
<summary><b>Time Zones and Dates</b> (click to show)</summary>

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.

</details>

<details>
<summary><b>Epochs: 1900 and 1904</b> (click to show)</summary>

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)
```

</details>

### Sheet Objects

Each key that does not start with `!` maps to a cell (using `A-1` notation)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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) |

</details>

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
Expand Down
27 changes: 27 additions & 0 deletions bits/11_ssfutils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>*/)/*: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;
}

29 changes: 15 additions & 14 deletions bits/20_jsutils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -77,17 +72,23 @@ 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();
if(str.indexOf("" + s) > -1) return d;
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<number>*/)/*:string*/ {
Expand Down
2 changes: 1 addition & 1 deletion bits/22_xmlutils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
9 changes: 6 additions & 3 deletions bits/40_harb.js
Original file line number Diff line number Diff line change
Expand Up @@ -523,17 +523,20 @@ 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*/);
if(s.charCodeAt(0) == 0x3D) { cell.t = 'n'; cell.f = s.substr(1); }
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';
Expand Down
9 changes: 4 additions & 5 deletions bits/67_wsxml.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down Expand Up @@ -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':
Expand All @@ -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] = [];
Expand Down
2 changes: 1 addition & 1 deletion bits/68_wsbin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
70 changes: 42 additions & 28 deletions bits/71_wbcommon.js
Original file line number Diff line number Diff line change
@@ -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
];

Expand Down Expand Up @@ -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;
}
}
}

Expand All @@ -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"); }
Expand Down
Loading

0 comments on commit aff7b95

Please sign in to comment.