Skip to content

Commit 4cc0412

Browse files
committed
roundtrip 1904 date setting
1 parent 83ddb4c commit 4cc0412

22 files changed

+3556
-554
lines changed

.github/workflows/deno.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
- run: sudo chmod a+x /usr/bin/rooster
2121
- run: make init
2222
- run: 'cd test_files; make all; cd -'
23-
- run: deno test --allow-env --allow-read --allow-write test.ts
23+
- run: deno test --allow-env --allow-read --allow-write --config misc/test.deno.jsonc test.ts
2424
# full test
2525
full:
2626
runs-on: ubuntu-latest
@@ -36,4 +36,4 @@ jobs:
3636
- run: sudo chmod a+x /usr/bin/rooster
3737
- run: make init
3838
- run: 'cd test_files; make all; cd -'
39-
- run: deno test --allow-env --allow-read --allow-write test.ts
39+
- run: deno test --allow-env --allow-read --allow-write --config misc/test.deno.jsonc test.ts

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,12 @@ test mocha: test.js ## Run test suite
141141
test-esm: test.mjs ## Run Node ESM test suite
142142
npx -y mocha@9 -R spec -t 30000 $<
143143

144+
test.ts: test.mts
145+
node -pe 'var data = fs.readFileSync("'$<'", "utf8"); data.split("\n").map(function(l) { return l.replace(/^describe\((.*?)function\(\)/, "Deno.test($$1async function(t)").replace(/\b(?:it|describe)\((.*?)function\(\)/g, "await t.step($$1async function(t)").replace("assert.ok", "assert.assert"); }).join("\n")' > $@
146+
144147
.PHONY: test-deno
145148
test-deno: test.ts ## Run Deno test suite
146-
deno test --allow-env --allow-read --allow-write $<
149+
deno test --allow-env --allow-read --allow-write --config misc/test.deno.jsonc $<
147150

148151
#* To run tests for one format, make test_<fmt>
149152
#* To run the core test suite, make test_misc

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ port calculations to web apps; automate common spreadsheet tasks, and much more!
1212
![License](https://img.shields.io/github/license/SheetJS/sheetjs)
1313
[![Build Status](https://img.shields.io/github/workflow/status/sheetjs/sheetjs/Tests:%20node.js)](https://github.com/SheetJS/sheetjs/actions)
1414
[![Snyk Vulnerabilities](https://img.shields.io/snyk/vulnerabilities/github/SheetJS/sheetjs)](https://snyk.io/test/github/SheetJS/sheetjs)
15-
[![npm Downloads](https://img.shields.io/npm/dm/xlsx.svg)](https://npmjs.org/package/xlsx)
15+
[![npm Downloads](https://img.shields.io/npm/dm/xlsx.svg)](https://cdn.sheetjs.com/)
16+
[![GitHub Repo stars](https://img.shields.io/github/stars/SheetJS/sheetjs?style=social)](https://github.com/SheetJS/sheetjs)
17+
1618
[![Analytics](https://ga-beacon.appspot.com/UA-36810333-1/SheetJS/sheetjs?pixel)](https://github.com/SheetJS/sheetjs)
1719

1820
[![Build Status](https://saucelabs.com/browser-matrix/sheetjs.svg)](https://saucelabs.com/u/sheetjs)
@@ -25,13 +27,13 @@ port calculations to web apps; automate common spreadsheet tasks, and much more!
2527

2628
## Related Projects
2729

28-
- <https://oss.sheetjs.com/notes/> file format notes
30+
- <https://oss.sheetjs.com/notes/>: File Format Notes
2931

3032
- [`ssf`](packages/ssf): Format data using ECMA-376 spreadsheet format codes
3133

3234
- [`xlsx-cli`](packages/xlsx-cli/): NodeJS command-line tool for processing files
3335

34-
- [`test_files`](https://github.com/SheetJS/test_files): test files repo
36+
- [`test_files`](https://github.com/SheetJS/test_files): Sample spreadsheets
3537

3638
- [`cfb`](https://github.com/SheetJS/js-cfb): Container (OLE/ZIP) format library
3739

bits/27_csfutils.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ function sheet_add_aoa(_ws/*:?Worksheet*/, data/*:AOA*/, opts/*:?any*/)/*:Worksh
157157
else if(typeof cell.v === 'boolean') cell.t = 'b';
158158
else if(cell.v instanceof Date) {
159159
cell.z = o.dateNF || table_fmt[14];
160-
if(o.cellDates) { cell.t = 'd'; cell.w = SSF_format(cell.z, datenum(cell.v)); }
161-
else { cell.t = 'n'; cell.v = datenum(cell.v); cell.w = SSF_format(cell.z, cell.v); }
160+
if(o.cellDates) { cell.t = 'd'; cell.w = SSF_format(cell.z, datenum(cell.v, o.date1904)); }
161+
else { cell.t = 'n'; cell.v = datenum(cell.v, o.date1904); cell.w = SSF_format(cell.z, cell.v); }
162162
}
163163
else cell.t = 's';
164164
}

bits/40_harb.js

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ var SYLK = /*#__PURE__*/(function() {
440440
wb.Workbook.Names.push(nn);
441441
} break;
442442
case 'C': /* cell */
443-
var C_seen_K = false, C_seen_X = false, C_seen_S = false, C_seen_E = false, _R = -1, _C = -1;
443+
var C_seen_K = false, C_seen_X = false, C_seen_S = false, C_seen_E = false, _R = -1, _C = -1, formula = "", cell_t = "z";
444444
for(rj=1; rj<record.length; ++rj) switch(record[rj].charAt(0)) {
445445
case 'A': break; // TODO: comment
446446
case 'X': C = parseInt(record[rj].slice(1), 10)-1; C_seen_X = true; break;
@@ -450,42 +450,46 @@ var SYLK = /*#__PURE__*/(function() {
450450
break;
451451
case 'K':
452452
val = record[rj].slice(1);
453-
if(val.charAt(0) === '"') val = val.slice(1,val.length - 1);
454-
else if(val === 'TRUE') val = true;
455-
else if(val === 'FALSE') val = false;
453+
if(val.charAt(0) === '"') { val = val.slice(1,val.length - 1); cell_t = "s"; }
454+
else if(val === 'TRUE' || val === 'FALSE') { val = val === 'TRUE'; cell_t = "b"; }
456455
else if(!isNaN(fuzzynum(val))) {
457-
val = fuzzynum(val);
458-
if(next_cell_format !== null && fmt_is_date(next_cell_format)) val = numdate(wb.Workbook.WBProps.date1904 ? val + 1462 : val);
456+
val = fuzzynum(val); cell_t = "n";
457+
if(next_cell_format !== null && fmt_is_date(next_cell_format) && opts.cellDates) { val = numdate(wb.Workbook.WBProps.date1904 ? val + 1462 : val); cell_t = "d"; }
459458
} else if(!isNaN(fuzzydate(val).getDate())) {
460-
val = parseDate(val);
459+
val = parseDate(val); cell_t = "d";
460+
if(!opts.cellDates) { cell_t = "n"; val = datenum(val, wb.Workbook.WBProps.date1904); }
461461
}
462462
if(typeof $cptable !== 'undefined' && typeof val == "string" && ((opts||{}).type != "string") && (opts||{}).codepage) val = $cptable.utils.decode(opts.codepage, val);
463463
C_seen_K = true;
464464
break;
465465
case 'E':
466466
C_seen_E = true;
467-
var formula = rc_to_a1(record[rj].slice(1), {r:R,c:C});
468-
arr[R][C] = [arr[R][C], formula];
467+
formula = rc_to_a1(record[rj].slice(1), {r:R,c:C});
469468
break;
470469
case 'S':
471470
C_seen_S = true;
472-
arr[R][C] = [arr[R][C], "S5S"];
473471
break;
474472
case 'G': break; // unknown
475473
case 'R': _R = parseInt(record[rj].slice(1), 10)-1; break;
476474
case 'C': _C = parseInt(record[rj].slice(1), 10)-1; break;
477475
default: if(opts && opts.WTF) throw new Error("SYLK bad record " + rstr);
478476
}
479477
if(C_seen_K) {
480-
if(arr[R][C] && arr[R][C].length == 2) arr[R][C][0] = val;
481-
else arr[R][C] = val;
478+
if(!arr[R][C]) arr[R][C] = { t: cell_t, v: val };
479+
else { arr[R][C].t = cell_t; arr[R][C].v = val; }
480+
if(next_cell_format) arr[R][C].z = next_cell_format;
481+
if(opts.cellText !== false && next_cell_format) arr[R][C].w = SSF_format(arr[R][C].z, arr[R][C].v, { date1904: wb.Workbook.WBProps.date1904 });
482482
next_cell_format = null;
483483
}
484484
if(C_seen_S) {
485485
if(C_seen_E) throw new Error("SYLK shared formula cannot have own formula");
486486
var shrbase = _R > -1 && arr[_R][_C];
487487
if(!shrbase || !shrbase[1]) throw new Error("SYLK shared formula cannot find base");
488-
arr[R][C][1] = shift_formula_str(shrbase[1], {r: R - _R, c: C - _C});
488+
formula = shift_formula_str(shrbase[1], {r: R - _R, c: C - _C});
489+
}
490+
if(formula) {
491+
if(!arr[R][C]) arr[R][C] = { t: 'n', f: formula };
492+
else arr[R][C].f = formula;
489493
}
490494
break;
491495
case 'F': /* Format */
@@ -537,7 +541,8 @@ var SYLK = /*#__PURE__*/(function() {
537541
function sylk_to_workbook(d/*:RawData*/, opts)/*:Workbook*/ {
538542
var aoasht = sylk_to_aoa(d, opts);
539543
var aoa = aoasht[0], ws = aoasht[1], wb = aoasht[2];
540-
var o = aoa_to_sheet(aoa, opts);
544+
var _opts = dup(opts); _opts.date1904 = (((wb||{}).Workbook || {}).WBProps || {}).date1904;
545+
var o = aoa_to_sheet(aoa, _opts);
541546
keys(ws).forEach(function(k) { o[k] = ws[k]; });
542547
var outwb = sheet_to_workbook(o, opts);
543548
keys(wb).forEach(function(k) { outwb[k] = wb[k]; });
@@ -581,24 +586,26 @@ var SYLK = /*#__PURE__*/(function() {
581586
});
582587
}
583588

584-
function sheet_to_sylk(ws/*:Worksheet*/, opts/*:?any*/)/*:string*/ {
589+
function sheet_to_sylk(ws/*:Worksheet*/, opts/*:?any*/, wb/*:?WorkBook*/)/*:string*/ {
585590
var preamble/*:Array<string>*/ = ["ID;PSheetJS;N;E"], o/*:Array<string>*/ = [];
586591
var r = safe_decode_range(ws['!ref']), cell/*:Cell*/;
587592
var dense = Array.isArray(ws);
588593
var RS = "\r\n";
594+
var d1904 = (((wb||{}).Workbook||{}).WBProps||{}).date1904;
589595

590596
preamble.push("P;PGeneral");
591597
preamble.push("F;P0;DG0G8;M255");
592598
if(ws['!cols']) write_ws_cols_sylk(preamble, ws['!cols']);
593599
if(ws['!rows']) write_ws_rows_sylk(preamble, ws['!rows']);
594600

595601
preamble.push("B;Y" + (r.e.r - r.s.r + 1) + ";X" + (r.e.c - r.s.c + 1) + ";D" + [r.s.c,r.s.r,r.e.c,r.e.r].join(" "));
602+
preamble.push("O;L;D;B" + (d1904 ? ";V4" : "") + ";K47;G100 0.001");
596603
for(var R = r.s.r; R <= r.e.r; ++R) {
597604
for(var C = r.s.c; C <= r.e.c; ++C) {
598605
var coord = encode_cell({r:R,c:C});
599606
cell = dense ? (ws[R]||[])[C]: ws[coord];
600607
if(!cell || (cell.v == null && (!cell.f || cell.F))) continue;
601-
o.push(write_ws_cell_sylk(cell, ws, R, C, opts));
608+
o.push(write_ws_cell_sylk(cell, ws, R, C, opts)); // TODO: pass date1904 info
602609
}
603610
}
604611
return preamble.join(RS) + RS + o.join(RS) + RS + "E" + RS;

bits/73_wbbin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ function write_BrtWbProp(data/*:?WBProps*/, o) {
4545
var flags = 0;
4646
if(data) {
4747
/* TODO: mirror parse_BrtWbProp fields */
48+
if(data.date1904) flags |= 0x01;
4849
if(data.filterPrivacy) flags |= 0x08;
4950
}
5051
o.write_shift(4, flags);

bits/75_xlml.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -942,9 +942,10 @@ function write_props_xlml(wb/*:Workbook*/, opts)/*:string*/ {
942942
return o.join("");
943943
}
944944
/* TODO */
945-
function write_wb_xlml(/*::wb, opts*/)/*:string*/ {
945+
function write_wb_xlml(wb/*::, opts*/)/*:string*/ {
946946
/* OfficeDocumentSettings */
947947
/* ExcelWorkbook */
948+
if((((wb||{}).Workbook||{}).WBProps||{}).date1904) return '<ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel"><Date1904/></ExcelWorkbook>';
948949
return "";
949950
}
950951
/* TODO */

bits/80_parseods.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,14 @@ function parse_content_xml(d/*:string*/, _opts)/*:Workbook*/ {
4949
var merges/*:Array<Range>*/ = [], mrange = {}, mR = 0, mC = 0;
5050
var rowinfo/*:Array<RowInfo>*/ = [], rowpeat = 1, colpeat = 1;
5151
var arrayf/*:Array<[Range, string]>*/ = [];
52-
var WB = {Names:[]};
52+
var WB = {Names:[], WBProps:{}};
5353
var atag = ({}/*:any*/);
5454
var _Ref/*:[string, string]*/ = ["", ""];
5555
var comments/*:Array<Comment>*/ = [], comment/*:Comment*/ = ({}/*:any*/);
5656
var creator = "", creatoridx = 0;
5757
var isstub = false, intable = false;
5858
var i = 0;
59+
var baddate = 1;
5960
xlmlregex.lastIndex = 0;
6061
str = str.replace(/<!--([\s\S]*?)-->/mg,"").replace(/<!DOCTYPE[^\[]*\[[^\]]*\]>/gm,"");
6162
while((Rn = xlmlregex.exec(str))) switch((Rn[3]=Rn[3].replace(/_.*$/,""))) {
@@ -167,7 +168,7 @@ function parse_content_xml(d/*:string*/, _opts)/*:Workbook*/ {
167168
case 'percentage': q.t = 'n'; q.v = parseFloat(ctag.value); break;
168169
case 'currency': q.t = 'n'; q.v = parseFloat(ctag.value); break;
169170
case 'date': q.t = 'd'; q.v = parseDate(ctag['date-value']);
170-
if(!opts.cellDates) { q.t = 'n'; q.v = datenum(q.v); }
171+
if(!opts.cellDates) { q.t = 'n'; q.v = datenum(q.v, WB.WBProps.date1904) - baddate; }
171172
q.z = 'm/d/yy'; break;
172173
case 'time': q.t = 'n'; q.v = parse_isodur(ctag['time-value'])/86400;
173174
if(opts.cellDates) { q.t = 'd'; q.v = numdate(q.v); }
@@ -368,7 +369,14 @@ function parse_content_xml(d/*:string*/, _opts)/*:Workbook*/ {
368369
case 'table-header-columns': break; // 9.1.11 <table:table-header-columns>
369370
case 'table-columns': break; // 9.1.12 <table:table-columns>
370371

371-
case 'null-date': break; // 9.4.2 <table:null-date> TODO: date1904
372+
case 'null-date': // 9.4.2 <table:null-date>
373+
tag = parsexmltag(Rn[0], false);
374+
switch(tag["date-value"]) {
375+
case "1904-01-01": WB.WBProps.date1904 = true;
376+
/* falls through */
377+
case "1900-01-01": baddate = 0;
378+
}
379+
break;
372380

373381
case 'graphic-properties': break; // 17.21 <style:graphic-properties>
374382
case 'calculation-settings': break; // 9.4.1 <table:calculation-settings>

bits/81_writeods.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ var write_content_ods/*:{(wb:any, opts:any):string}*/ = /* @__PURE__ */(function
255255
write_automatic_styles_ods(o, wb);
256256
o.push(' <office:body>\n');
257257
o.push(' <office:spreadsheet>\n');
258+
if(((wb.Workbook||{}).WBProps||{}).date1904) o.push(' <table:calculation-settings table:case-sensitive="false" table:search-criteria-must-apply-to-whole-cell="true" table:use-wildcards="true" table:use-regular-expressions="false" table:automatic-find-labels="false">\n <table:null-date table:date-value="1904-01-01"/>\n </table:calculation-settings>\n');
258259
for(var i = 0; i != wb.SheetNames.length; ++i) o.push(write_ws(wb.Sheets[wb.SheetNames[i]], wb, i, opts));
259260
if((wb.Workbook||{}).Names) o.push(write_names_ods(wb.Workbook.Names, wb.SheetNames, -1));
260261
o.push(' </office:spreadsheet>\n');

bits/88_write.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ function writeSync(wb/*:Workbook*/, opts/*:?WriteOpts*/) {
134134
case 'xml':
135135
case 'xlml': return write_string_type(write_xlml(wb, o), o);
136136
case 'slk':
137-
case 'sylk': return write_string_type(SYLK.from_sheet(wb.Sheets[wb.SheetNames[idx]], o), o);
137+
case 'sylk': return write_string_type(SYLK.from_sheet(wb.Sheets[wb.SheetNames[idx]], o, wb), o);
138138
case 'htm':
139139
case 'html': return write_string_type(sheet_to_html(wb.Sheets[wb.SheetNames[idx]], o), o);
140140
case 'txt': return write_stxt_type(sheet_to_txt(wb.Sheets[wb.SheetNames[idx]], o), o);

0 commit comments

Comments
 (0)