Compare commits

...

2 Commits

Author SHA1 Message Date
scottysseus
456c403c09 added test cases from #2560's thread; fixed more rounding edge cases 2022-06-21 20:58:17 +00:00
scottysseus
c580ffcbc2 fix rounding issue
See: #2560

- Improved rounding to account for minutes, hours, etc.
- Added tests for rounding dates/times
2022-06-15 18:40:16 +00:00
6 changed files with 333 additions and 140 deletions

@ -1,5 +1,6 @@
function parse_date_code(v/*:number*/,opts/*:?any*/,b2/*:?boolean*/) {
if(v > 2958465 || v < 0) return null;
opts.b2 = b2 || false;
var date = (v|0), time = Math.floor(86400 * (v - date)), dow=0;
var dout=[];
var out={D:date, T:time, u:86400*(v-date)-time,y:0,m:0,d:0,H:0,M:0,S:0,q:0};

@ -1,4 +1,5 @@
/*jshint -W086 */
var ROUNDING_FLAG = "rounding is necessary"
function write_date(type/*:number*/, fmt/*:string*/, val, ss0/*:?number*/)/*:string*/ {
var o="", ss=0, tt=0, y = val.y, out, outl = 0;
switch(type) {
@ -45,7 +46,7 @@ function write_date(type/*:number*/, fmt/*:string*/, val, ss0/*:?number*/)/*:str
if(ss0 >= 2) tt = ss0 === 3 ? 1000 : 100;
else tt = ss0 === 1 ? 10 : 1;
ss = Math.round((tt)*(val.S + val.u));
if(ss >= 60*tt) ss = 0;
if(ss >= 60*tt) throw ROUNDING_FLAG;
if(fmt === 's') return ss === 0 ? "0" : ""+ss/tt;
o = pad0(ss,2 + ss0);
if(fmt === 'ss') return o.substr(0,2);
@ -54,7 +55,7 @@ function write_date(type/*:number*/, fmt/*:string*/, val, ss0/*:?number*/)/*:str
switch(fmt) {
case '[h]': case '[hh]': out = val.D*24+val.H; break;
case '[m]': case '[mm]': out = (val.D*24+val.H)*60+val.M; break;
case '[s]': case '[ss]': out = ((val.D*24+val.H)*60+val.M)*60+Math.round(val.S+val.u); break;
case '[s]': case '[ss]': out = ((val.D*24+val.H)*60+val.M)*60+(ss0 < 1 ? Math.round(val.S+val.u) : val.S); break;
default: throw 'bad abstime format: ' + fmt;
} outl = fmt.length === 3 ? 1 : 2; break;
case 101: /* 'e' era */

@ -99,47 +99,14 @@ function eval_fmt(fmt/*:string*/, v/*:any*/, opts/*:any*/, flen/*:number*/) {
}
}
/* time rounding depends on presence of minute / second / usec fields */
switch(bt) {
case 0: break;
case 1:
/*::if(!dt) break;*/
if(dt.u >= 0.5) { dt.u = 0; ++dt.S; }
if(dt.S >= 60) { dt.S = 0; ++dt.M; }
if(dt.M >= 60) { dt.M = 0; ++dt.H; }
break;
case 2:
/*::if(!dt) break;*/
if(dt.u >= 0.5) { dt.u = 0; ++dt.S; }
if(dt.S >= 60) { dt.S = 0; ++dt.M; }
break;
if (bt > 0 && bt < 3 && dt.u >= 0.5) {
round_up_date(dt, opts);
}
/* replace fields */
var nstr = "", jj;
for(i=0; i < out.length; ++i) {
switch(out[i].t) {
case 't': case 'T': case ' ': case 'D': break;
case 'X': out[i].v = ""; out[i].t = ";"; break;
case 'd': case 'm': case 'y': case 'h': case 'H': case 'M': case 's': case 'e': case 'b': case 'Z':
/*::if(!dt) throw "unreachable"; */
out[i].v = write_date(out[i].t.charCodeAt(0), out[i].v, dt, ss0);
out[i].t = 't'; break;
case 'n': case '?':
jj = i+1;
while(out[jj] != null && (
(c=out[jj].t) === "?" || c === "D" ||
((c === " " || c === "t") && out[jj+1] != null && (out[jj+1].t === '?' || out[jj+1].t === "t" && out[jj+1].v === '/')) ||
(out[i].t === '(' && (c === ' ' || c === 'n' || c === ')')) ||
(c === 't' && (out[jj].v === '/' || out[jj].v === ' ' && out[jj+1] != null && out[jj+1].t == '?'))
)) {
out[i].v += out[jj].v;
out[jj] = {v:"", t:";"}; ++jj;
}
nstr += out[i].v;
i = jj-1; break;
case 'G': out[i].t = 't'; out[i].v = general_fmt(v,opts); break;
}
}
var replaced = replace_fields(out, dt, ss0, v, opts);
var nstr = replaced.nstr;
out = replaced.out;
var vv = "", myv, ostr;
if(nstr.length > 0) {
if(nstr.charCodeAt(0) == 40) /* '(' */ {
@ -153,7 +120,7 @@ function eval_fmt(fmt/*:string*/, v/*:any*/, opts/*:any*/, flen/*:number*/) {
out[0].v = "-" + out[0].v;
}
}
jj=ostr.length-1;
var jj=ostr.length-1;
var decpt = out.length;
for(i=0; i < out.length; ++i) if(out[i] != null && out[i].t != 't' && out[i].v.indexOf(".") > -1) { decpt = i; break; }
var lasti=out.length;
@ -205,4 +172,83 @@ function eval_fmt(fmt/*:string*/, v/*:any*/, opts/*:any*/, flen/*:number*/) {
for(i=0; i !== out.length; ++i) if(out[i] != null) retval += out[i].v;
return retval;
}
function replace_fields(fields, dt, ss0, v, opts) {
var out = [];
for (var i = 0; i < fields.length; i++) {out[i] = {t: fields[i].t, v: fields[i].v};}
var nstr = "", jj;
for(i=0; i < out.length; ++i) {
switch(out[i].t) {
case 't': case 'T': case ' ': case 'D': break;
case 'X': out[i].v = ""; out[i].t = ";"; break;
case 'd': case 'm': case 'y': case 'h': case 'H': case 'M': case 's': case 'e': case 'b': case 'Z':
/*::if(!dt) throw "unreachable"; */
try {
out[i].v = write_date(out[i].t.charCodeAt(0), out[i].v, dt, ss0);
} catch (e) {
if (e === ROUNDING_FLAG) {
round_up_date(dt, opts);
return replace_fields(fields, dt, ss0, v, opts);
}
throw e;
}
out[i].t = 't'; break;
case 'n': case '?':
jj = i+1;
while(out[jj] != null && (
(c=out[jj].t) === "?" || c === "D" ||
((c === " " || c === "t") && out[jj+1] != null && (out[jj+1].t === '?' || out[jj+1].t === "t" && out[jj+1].v === '/')) ||
(out[i].t === '(' && (c === ' ' || c === 'n' || c === ')')) ||
(c === 't' && (out[jj].v === '/' || out[jj].v === ' ' && out[jj+1] != null && out[jj+1].t == '?'))
)) {
out[i].v += out[jj].v;
out[jj] = {v:"", t:";"}; ++jj;
}
nstr += out[i].v;
i = jj-1; break;
case 'G': out[i].t = 't'; out[i].v = general_fmt(v,opts); break;
}
}
return {nstr: nstr, out: out};
}
function round_up_date(out, opts) {
if (!opts) opts = {};
var tmp = new Date(out.y, out.m - 1, out.d, out.H, out.M, out.S);
var oldDate = tmp.getDate();
tmp.setSeconds(out.S + 1);
var use1900 = !opts.date1904 && !opts.b2;
if (tmp.getDate() !== oldDate) {
if (out.D === 0 && use1900) {
// 0 corresponds with Jan 0th, 1900
out.y = 1900;
out.m = 1;
out.d = 1;
out.q = (tmp.getDay() + 6) % 7;
} else if (out.D === 60 && use1900) {
// Excel & SSF have an intentional bug where they treat 1900 as a leap year
// The 60th day (Feb 29) rounds up to Mar 1
out.y = 1900;
out.m = 3;
out.d = 1;
out.q = 4;
} else if (out.D == 59 && use1900) {
// Excel & SSF have an intentional bug where they treat 1900 as a leap year
// The 59th day (Feb 28) rounds up to Feb 29
out.y = 1900;
out.m = 2;
out.d = 29;
out.q = 3;
} else {
out.y = tmp.getFullYear();
out.m = tmp.getMonth() + 1;
out.d = tmp.getDate();
out.q = out.D < 60 && use1900 ? (tmp.getDay() + 6) % 7 : tmp.getDay();
}
out.D += 1;
}
out.H = tmp.getHours();
out.M = tmp.getMinutes();
out.S = tmp.getSeconds();
out.u = 0;
out.T += 1;
}
SSF._eval = eval_fmt;

@ -3,7 +3,7 @@
/*jshint -W041 */
/*:: declare var DO_NOT_EXPORT_SSF: any; */
var SSF/*:SSFModule*/ = ({}/*:any*/);
var make_ssf = function make_ssf(SSF/*:SSFModule*/){
function make_ssf(SSF/*:SSFModule*/){
SSF.version = '0.11.2';
function _strrev(x/*:string*/)/*:string*/ { var o = "", i = x.length-1; while(i>=0) o += x.charAt(i--); return o; }
function fill(c/*:string*/,l/*:number*/)/*:string*/ { var o = ""; while(o.length < l) o+=c; return o; }
@ -163,6 +163,7 @@ function frac(x/*:number*/, D/*:number*/, mixed/*:?boolean*/)/*:Array<number>*/
}
function parse_date_code(v/*:number*/,opts/*:?any*/,b2/*:?boolean*/) {
if(v > 2958465 || v < 0) return null;
opts.b2 = b2 || false;
var date = (v|0), time = Math.floor(86400 * (v - date)), dow=0;
var dout=[];
var out={D:date, T:time, u:86400*(v-date)-time,y:0,m:0,d:0,H:0,M:0,S:0,q:0};
@ -201,10 +202,6 @@ function datenum_local(v/*:Date*/, date1904/*:?boolean*/)/*:number*/ {
else if(v >= base1904) epoch += 24*60*60*1000;
return (epoch - (dnthresh + (v.getTimezoneOffset() - basedate.getTimezoneOffset()) * 60000)) / (24 * 60 * 60 * 1000);
}
/* The longest 32-bit integer text is "-4294967296", exactly 11 chars */
function general_fmt_int(v/*:number*/)/*:string*/ { return v.toString(10); }
SSF._general_int = general_fmt_int;
/* ECMA-376 18.8.30 numFmt*/
/* Note: `toPrecision` uses standard form when prec > E and E >= -6 */
var general_fmt_num = (function make_general_fmt_num() {
@ -257,6 +254,7 @@ SSF._general_num = general_fmt_num;
- "up to 11 characters" displayed for numbers
- Default date format (code 14) used for Dates
The longest 32-bit integer text is "-2147483648", exactly 11 chars
TODO: technically the display depends on the width of the cell
*/
function general_fmt(v/*:any*/, opts/*:any*/) {
@ -281,6 +279,7 @@ function fix_hijri(date/*:Date*/, o/*:[number, number, number]*/) {
}
var THAI_DIGITS = "\u0E50\u0E51\u0E52\u0E53\u0E54\u0E55\u0E56\u0E57\u0E58\u0E59".split("");
/*jshint -W086 */
var ROUNDING_FLAG = "rounding is necessary"
function write_date(type/*:number*/, fmt/*:string*/, val, ss0/*:?number*/)/*:string*/ {
var o="", ss=0, tt=0, y = val.y, out, outl = 0;
switch(type) {
@ -327,7 +326,7 @@ function write_date(type/*:number*/, fmt/*:string*/, val, ss0/*:?number*/)/*:str
if(ss0 >= 2) tt = ss0 === 3 ? 1000 : 100;
else tt = ss0 === 1 ? 10 : 1;
ss = Math.round((tt)*(val.S + val.u));
if(ss >= 60*tt) ss = 0;
if(ss >= 60*tt) throw ROUNDING_FLAG;
if(fmt === 's') return ss === 0 ? "0" : ""+ss/tt;
o = pad0(ss,2 + ss0);
if(fmt === 'ss') return o.substr(0,2);
@ -336,7 +335,7 @@ function write_date(type/*:number*/, fmt/*:string*/, val, ss0/*:?number*/)/*:str
switch(fmt) {
case '[h]': case '[hh]': out = val.D*24+val.H; break;
case '[m]': case '[mm]': out = (val.D*24+val.H)*60+val.M; break;
case '[s]': case '[ss]': out = ((val.D*24+val.H)*60+val.M)*60+Math.round(val.S+val.u); break;
case '[s]': case '[ss]': out = ((val.D*24+val.H)*60+val.M)*60+(ss0 < 1 ? Math.round(val.S+val.u) : val.S); break;
default: throw 'bad abstime format: ' + fmt;
} outl = fmt.length === 3 ? 1 : 2; break;
case 101: /* 'e' era */
@ -794,47 +793,14 @@ function eval_fmt(fmt/*:string*/, v/*:any*/, opts/*:any*/, flen/*:number*/) {
}
}
/* time rounding depends on presence of minute / second / usec fields */
switch(bt) {
case 0: break;
case 1:
/*::if(!dt) break;*/
if(dt.u >= 0.5) { dt.u = 0; ++dt.S; }
if(dt.S >= 60) { dt.S = 0; ++dt.M; }
if(dt.M >= 60) { dt.M = 0; ++dt.H; }
break;
case 2:
/*::if(!dt) break;*/
if(dt.u >= 0.5) { dt.u = 0; ++dt.S; }
if(dt.S >= 60) { dt.S = 0; ++dt.M; }
break;
if (bt > 0 && bt < 3 && dt.u >= 0.5) {
round_up_date(dt, opts);
}
/* replace fields */
var nstr = "", jj;
for(i=0; i < out.length; ++i) {
switch(out[i].t) {
case 't': case 'T': case ' ': case 'D': break;
case 'X': out[i].v = ""; out[i].t = ";"; break;
case 'd': case 'm': case 'y': case 'h': case 'H': case 'M': case 's': case 'e': case 'b': case 'Z':
/*::if(!dt) throw "unreachable"; */
out[i].v = write_date(out[i].t.charCodeAt(0), out[i].v, dt, ss0);
out[i].t = 't'; break;
case 'n': case '?':
jj = i+1;
while(out[jj] != null && (
(c=out[jj].t) === "?" || c === "D" ||
((c === " " || c === "t") && out[jj+1] != null && (out[jj+1].t === '?' || out[jj+1].t === "t" && out[jj+1].v === '/')) ||
(out[i].t === '(' && (c === ' ' || c === 'n' || c === ')')) ||
(c === 't' && (out[jj].v === '/' || out[jj].v === ' ' && out[jj+1] != null && out[jj+1].t == '?'))
)) {
out[i].v += out[jj].v;
out[jj] = {v:"", t:";"}; ++jj;
}
nstr += out[i].v;
i = jj-1; break;
case 'G': out[i].t = 't'; out[i].v = general_fmt(v,opts); break;
}
}
var replaced = replace_fields(out, dt, ss0, v, opts);
var nstr = replaced.nstr;
out = replaced.out;
var vv = "", myv, ostr;
if(nstr.length > 0) {
if(nstr.charCodeAt(0) == 40) /* '(' */ {
@ -848,7 +814,7 @@ function eval_fmt(fmt/*:string*/, v/*:any*/, opts/*:any*/, flen/*:number*/) {
out[0].v = "-" + out[0].v;
}
}
jj=ostr.length-1;
var jj=ostr.length-1;
var decpt = out.length;
for(i=0; i < out.length; ++i) if(out[i] != null && out[i].t != 't' && out[i].v.indexOf(".") > -1) { decpt = i; break; }
var lasti=out.length;
@ -900,6 +866,85 @@ function eval_fmt(fmt/*:string*/, v/*:any*/, opts/*:any*/, flen/*:number*/) {
for(i=0; i !== out.length; ++i) if(out[i] != null) retval += out[i].v;
return retval;
}
function replace_fields(fields, dt, ss0, v, opts) {
var out = [];
for (var i = 0; i < fields.length; i++) {out[i] = {t: fields[i].t, v: fields[i].v};}
var nstr = "", jj;
for(i=0; i < out.length; ++i) {
switch(out[i].t) {
case 't': case 'T': case ' ': case 'D': break;
case 'X': out[i].v = ""; out[i].t = ";"; break;
case 'd': case 'm': case 'y': case 'h': case 'H': case 'M': case 's': case 'e': case 'b': case 'Z':
/*::if(!dt) throw "unreachable"; */
try {
out[i].v = write_date(out[i].t.charCodeAt(0), out[i].v, dt, ss0);
} catch (e) {
if (e === ROUNDING_FLAG) {
round_up_date(dt, opts);
return replace_fields(fields, dt, ss0, v, opts);
}
throw e;
}
out[i].t = 't'; break;
case 'n': case '?':
jj = i+1;
while(out[jj] != null && (
(c=out[jj].t) === "?" || c === "D" ||
((c === " " || c === "t") && out[jj+1] != null && (out[jj+1].t === '?' || out[jj+1].t === "t" && out[jj+1].v === '/')) ||
(out[i].t === '(' && (c === ' ' || c === 'n' || c === ')')) ||
(c === 't' && (out[jj].v === '/' || out[jj].v === ' ' && out[jj+1] != null && out[jj+1].t == '?'))
)) {
out[i].v += out[jj].v;
out[jj] = {v:"", t:";"}; ++jj;
}
nstr += out[i].v;
i = jj-1; break;
case 'G': out[i].t = 't'; out[i].v = general_fmt(v,opts); break;
}
}
return {nstr: nstr, out: out};
}
function round_up_date(out, opts) {
if (!opts) opts = {};
var tmp = new Date(out.y, out.m - 1, out.d, out.H, out.M, out.S);
var oldDate = tmp.getDate();
tmp.setSeconds(out.S + 1);
var use1900 = !opts.date1904 && !opts.b2;
if (tmp.getDate() !== oldDate) {
if (out.D === 0 && use1900) {
// 0 corresponds with Jan 0th, 1900
out.y = 1900;
out.m = 1;
out.d = 1;
out.q = (tmp.getDay() + 6) % 7;
} else if (out.D === 60 && use1900) {
// Excel & SSF have an intentional bug where they treat 1900 as a leap year
// The 60th day (Feb 29) rounds up to Mar 1
out.y = 1900;
out.m = 3;
out.d = 1;
out.q = 4;
} else if (out.D == 59 && use1900) {
// Excel & SSF have an intentional bug where they treat 1900 as a leap year
// The 59th day (Feb 28) rounds up to Feb 29
out.y = 1900;
out.m = 2;
out.d = 29;
out.q = 3;
} else {
out.y = tmp.getFullYear();
out.m = tmp.getMonth() + 1;
out.d = tmp.getDate();
out.q = out.D < 60 && use1900 ? (tmp.getDay() + 6) % 7 : tmp.getDay();
}
out.D += 1;
}
out.H = tmp.getHours();
out.M = tmp.getMinutes();
out.S = tmp.getSeconds();
out.u = 0;
out.T += 1;
}
SSF._eval = eval_fmt;
var cfregex = /\[[=<>]/;
var cfregex2 = /\[(=|>[=]?|<[>=]?)(-?\d+(?:\.\d*)?)\]/;
@ -985,7 +1030,8 @@ SSF.load_table = function load_table(tbl/*:SSFTable*/)/*:void*/ {
};
SSF.init_table = init_table;
SSF.format = format;
};
SSF.choose_format = choose_fmt;
}
make_ssf(SSF);
/*global module */
if(typeof module !== 'undefined' && typeof DO_NOT_EXPORT_SSF === 'undefined') module.exports = SSF;

@ -2,7 +2,7 @@
/* vim: set ts=2: */
/*jshint -W041 */
var SSF = ({});
var make_ssf = function make_ssf(SSF){
function make_ssf(SSF){
SSF.version = '0.11.2';
function _strrev(x) { var o = "", i = x.length-1; while(i>=0) o += x.charAt(i--); return o; }
function fill(c,l) { var o = ""; while(o.length < l) o+=c; return o; }
@ -159,6 +159,7 @@ function frac(x, D, mixed) {
}
function parse_date_code(v,opts,b2) {
if(v > 2958465 || v < 0) return null;
opts.b2 = b2 || false;
var date = (v|0), time = Math.floor(86400 * (v - date)), dow=0;
var dout=[];
var out={D:date, T:time, u:86400*(v-date)-time,y:0,m:0,d:0,H:0,M:0,S:0,q:0};
@ -197,10 +198,6 @@ function datenum_local(v, date1904) {
else if(v >= base1904) epoch += 24*60*60*1000;
return (epoch - (dnthresh + (v.getTimezoneOffset() - basedate.getTimezoneOffset()) * 60000)) / (24 * 60 * 60 * 1000);
}
/* The longest 32-bit integer text is "-4294967296", exactly 11 chars */
function general_fmt_int(v) { return v.toString(10); }
SSF._general_int = general_fmt_int;
/* ECMA-376 18.8.30 numFmt*/
/* Note: `toPrecision` uses standard form when prec > E and E >= -6 */
var general_fmt_num = (function make_general_fmt_num() {
@ -253,6 +250,7 @@ SSF._general_num = general_fmt_num;
- "up to 11 characters" displayed for numbers
- Default date format (code 14) used for Dates
The longest 32-bit integer text is "-2147483648", exactly 11 chars
TODO: technically the display depends on the width of the cell
*/
function general_fmt(v, opts) {
@ -277,6 +275,7 @@ function fix_hijri(date, o) {
}
var THAI_DIGITS = "\u0E50\u0E51\u0E52\u0E53\u0E54\u0E55\u0E56\u0E57\u0E58\u0E59".split("");
/*jshint -W086 */
var ROUNDING_FLAG = "rounding is necessary"
function write_date(type, fmt, val, ss0) {
var o="", ss=0, tt=0, y = val.y, out, outl = 0;
switch(type) {
@ -322,7 +321,7 @@ function write_date(type, fmt, val, ss0) {
if(ss0 >= 2) tt = ss0 === 3 ? 1000 : 100;
else tt = ss0 === 1 ? 10 : 1;
ss = Math.round((tt)*(val.S + val.u));
if(ss >= 60*tt) ss = 0;
if(ss >= 60*tt) throw ROUNDING_FLAG;
if(fmt === 's') return ss === 0 ? "0" : ""+ss/tt;
o = pad0(ss,2 + ss0);
if(fmt === 'ss') return o.substr(0,2);
@ -331,7 +330,7 @@ if(ss0 >= 2) tt = ss0 === 3 ? 1000 : 100;
switch(fmt) {
case '[h]': case '[hh]': out = val.D*24+val.H; break;
case '[m]': case '[mm]': out = (val.D*24+val.H)*60+val.M; break;
case '[s]': case '[ss]': out = ((val.D*24+val.H)*60+val.M)*60+Math.round(val.S+val.u); break;
case '[s]': case '[ss]': out = ((val.D*24+val.H)*60+val.M)*60+(ss0 < 1 ? Math.round(val.S+val.u) : val.S); break;
default: throw 'bad abstime format: ' + fmt;
} outl = fmt.length === 3 ? 1 : 2; break;
case 101: /* 'e' era */
@ -787,44 +786,14 @@ function eval_fmt(fmt, v, opts, flen) {
}
}
/* time rounding depends on presence of minute / second / usec fields */
switch(bt) {
case 0: break;
case 1:
if(dt.u >= 0.5) { dt.u = 0; ++dt.S; }
if(dt.S >= 60) { dt.S = 0; ++dt.M; }
if(dt.M >= 60) { dt.M = 0; ++dt.H; }
break;
case 2:
if(dt.u >= 0.5) { dt.u = 0; ++dt.S; }
if(dt.S >= 60) { dt.S = 0; ++dt.M; }
break;
if (bt > 0 && bt < 3 && dt.u >= 0.5) {
round_up_date(dt, opts);
}
/* replace fields */
var nstr = "", jj;
for(i=0; i < out.length; ++i) {
switch(out[i].t) {
case 't': case 'T': case ' ': case 'D': break;
case 'X': out[i].v = ""; out[i].t = ";"; break;
case 'd': case 'm': case 'y': case 'h': case 'H': case 'M': case 's': case 'e': case 'b': case 'Z':
out[i].v = write_date(out[i].t.charCodeAt(0), out[i].v, dt, ss0);
out[i].t = 't'; break;
case 'n': case '?':
jj = i+1;
while(out[jj] != null && (
(c=out[jj].t) === "?" || c === "D" ||
((c === " " || c === "t") && out[jj+1] != null && (out[jj+1].t === '?' || out[jj+1].t === "t" && out[jj+1].v === '/')) ||
(out[i].t === '(' && (c === ' ' || c === 'n' || c === ')')) ||
(c === 't' && (out[jj].v === '/' || out[jj].v === ' ' && out[jj+1] != null && out[jj+1].t == '?'))
)) {
out[i].v += out[jj].v;
out[jj] = {v:"", t:";"}; ++jj;
}
nstr += out[i].v;
i = jj-1; break;
case 'G': out[i].t = 't'; out[i].v = general_fmt(v,opts); break;
}
}
var replaced = replace_fields(out, dt, ss0, v, opts);
var nstr = replaced.nstr;
out = replaced.out;
var vv = "", myv, ostr;
if(nstr.length > 0) {
if(nstr.charCodeAt(0) == 40) /* '(' */ {
@ -838,7 +807,7 @@ out[i].v = write_date(out[i].t.charCodeAt(0), out[i].v, dt, ss0);
out[0].v = "-" + out[0].v;
}
}
jj=ostr.length-1;
var jj=ostr.length-1;
var decpt = out.length;
for(i=0; i < out.length; ++i) if(out[i] != null && out[i].t != 't' && out[i].v.indexOf(".") > -1) { decpt = i; break; }
var lasti=out.length;
@ -890,6 +859,84 @@ out[i].v = write_date(out[i].t.charCodeAt(0), out[i].v, dt, ss0);
for(i=0; i !== out.length; ++i) if(out[i] != null) retval += out[i].v;
return retval;
}
function replace_fields(fields, dt, ss0, v, opts) {
var out = [];
for (var i = 0; i < fields.length; i++) {out[i] = {t: fields[i].t, v: fields[i].v};}
var nstr = "", jj;
for(i=0; i < out.length; ++i) {
switch(out[i].t) {
case 't': case 'T': case ' ': case 'D': break;
case 'X': out[i].v = ""; out[i].t = ";"; break;
case 'd': case 'm': case 'y': case 'h': case 'H': case 'M': case 's': case 'e': case 'b': case 'Z':
try {
out[i].v = write_date(out[i].t.charCodeAt(0), out[i].v, dt, ss0);
} catch (e) {
if (e === ROUNDING_FLAG) {
round_up_date(dt, opts);
return replace_fields(fields, dt, ss0, v, opts);
}
throw e;
}
out[i].t = 't'; break;
case 'n': case '?':
jj = i+1;
while(out[jj] != null && (
(c=out[jj].t) === "?" || c === "D" ||
((c === " " || c === "t") && out[jj+1] != null && (out[jj+1].t === '?' || out[jj+1].t === "t" && out[jj+1].v === '/')) ||
(out[i].t === '(' && (c === ' ' || c === 'n' || c === ')')) ||
(c === 't' && (out[jj].v === '/' || out[jj].v === ' ' && out[jj+1] != null && out[jj+1].t == '?'))
)) {
out[i].v += out[jj].v;
out[jj] = {v:"", t:";"}; ++jj;
}
nstr += out[i].v;
i = jj-1; break;
case 'G': out[i].t = 't'; out[i].v = general_fmt(v,opts); break;
}
}
return {nstr: nstr, out: out};
}
function round_up_date(out, opts) {
if (!opts) opts = {};
var tmp = new Date(out.y, out.m - 1, out.d, out.H, out.M, out.S);
var oldDate = tmp.getDate();
tmp.setSeconds(out.S + 1);
var use1900 = !opts.date1904 && !opts.b2;
if (tmp.getDate() !== oldDate) {
if (out.D === 0 && use1900) {
// 0 corresponds with Jan 0th, 1900
out.y = 1900;
out.m = 1;
out.d = 1;
out.q = (tmp.getDay() + 6) % 7;
} else if (out.D === 60 && use1900) {
// Excel & SSF have an intentional bug where they treat 1900 as a leap year
// The 60th day (Feb 29) rounds up to Mar 1
out.y = 1900;
out.m = 3;
out.d = 1;
out.q = 4;
} else if (out.D == 59 && use1900) {
// Excel & SSF have an intentional bug where they treat 1900 as a leap year
// The 59th day (Feb 28) rounds up to Feb 29
out.y = 1900;
out.m = 2;
out.d = 29;
out.q = 3;
} else {
out.y = tmp.getFullYear();
out.m = tmp.getMonth() + 1;
out.d = tmp.getDate();
out.q = out.D < 60 && use1900 ? (tmp.getDay() + 6) % 7 : tmp.getDay();
}
out.D += 1;
}
out.H = tmp.getHours();
out.M = tmp.getMinutes();
out.S = tmp.getSeconds();
out.u = 0;
out.T += 1;
}
SSF._eval = eval_fmt;
var cfregex = /\[[=<>]/;
var cfregex2 = /\[(=|>[=]?|<[>=]?)(-?\d+(?:\.\d*)?)\]/;
@ -971,7 +1018,8 @@ SSF.load_table = function load_table(tbl) {
};
SSF.init_table = init_table;
SSF.format = format;
};
SSF.choose_format = choose_fmt;
}
make_ssf(SSF);
/*global module */
if(typeof module !== 'undefined' && typeof DO_NOT_EXPORT_SSF === 'undefined') module.exports = SSF;

@ -12,21 +12,72 @@ function doit(data) {
for(var j = 0; j <= 100; ++j) it(String(j), function() {
for(var k = 0; k <= step; ++k,++i) {
if(data[i] == null || data[i].length < 3) return;
var d = data[i].replace(/#{255}/g,"").split("\t");
for(var w = 1; w < headers.length; ++w) {
var expected = d[w], actual = SSF.format(headers[w], parseFloat(d[0]), {});
if(actual != expected) throw new Error([actual, expected, w, headers[w],d[0],d,i].join("|"));
actual = SSF.format(headers[w].toUpperCase(), parseFloat(d[0]), {});
if(actual != expected) throw new Error([actual, expected, w, headers[w].toUpperCase(),d[0],d,i].join("|"));
}
var row = data[i].replace(/#{255}/g,"").split("\t");
testRow(row, headers, {})
}
});
}
function testRow(row, headers, opts) {
for(var w = 1; w < headers.length; ++w) {
var expected = row[w], actual = SSF.format(headers[w], parseFloat(row[0]), opts);
if(actual != expected) throw new Error([actual, expected, w, headers[w],row[0],row].join("|"));
actual = SSF.format(headers[w].toUpperCase(), parseFloat(row[0]), opts);
if(actual != expected) throw new Error([actual, expected, w, headers[w].toUpperCase(),row[0],row].join("|"));
}
}
describe('time formats', function() {
doit(process.env.MINTEST ? times.slice(0,4000) : times);
});
describe('time format rounding', function() {
var headers=['value', 'yyyy mmm ddd dd hh:mm:ss'];
var testCases = [
{desc: "rounds up to 1 minute", value: "0.00069", date1904: {"false": "1900 Jan Sat 00 00:01:00", "true": "1904 Jan Fri 01 00:01:00"}},
{desc: "rounds up to 2 munutes", value: "0.001388", date1904: {"false": "1900 Jan Sat 00 00:02:00", "true": "1904 Jan Fri 01 00:02:00"}},
{desc: "rounds up to 10 minutes", value: "0.00694", date1904: {"false": "1900 Jan Sat 00 00:10:00", "true": "1904 Jan Fri 01 00:10:00"}},
{desc: "rounds up to 2 hours", value: "0.08333", date1904: {"false": "1900 Jan Sat 00 02:00:00", "true": "1904 Jan Fri 01 02:00:00"}},
{desc: "rounds up day", value: "0.999999", date1904: {"false": "1900 Jan Sun 01 00:00:00", "true": "1904 Jan Sat 02 00:00:00"}},
{desc: "rounds up month", value: "31.999999", date1904: {"false": "1900 Feb Wed 01 00:00:00", "true": "1904 Feb Tue 02 00:00:00"}},
{desc: "rounds up to 1900 leap day", value: "59.999999", date1904: {"false": "1900 Feb Wed 29 00:00:00", "true": "1904 Mar Tue 01 00:00:00"}},
{desc: "rounds 1900 leap day up", value: "60.999999", date1904: {"false": "1900 Mar Thu 01 00:00:00", "true": "1904 Mar Wed 02 00:00:00"}},
{desc: "rounds up day in March", value: "77.999999", date1904: {"false": "1900 Mar Sun 18 00:00:00", "true": "1904 Mar Sat 19 00:00:00"}},
{desc: "rounds up leap year 1900", value: "366.999999", date1904: {"false": "1901 Jan Tue 01 00:00:00", "true": "1905 Jan Mon 02 00:00:00"}},
{desc: "rounds up leap year 1904", value: "365.999999", date1904: {"false": "1900 Dec Mon 31 00:00:00", "true": "1905 Jan Sun 01 00:00:00"}},
];
[{date1904: true}, {date1904: false}].forEach(opts => {
testCases.forEach(testCase => {
it(testCase.desc + ` (1904: ${opts.date1904})`,
() => testRow([testCase.value, testCase.date1904[`${opts.date1904}`]], headers, opts));
});
});
});
describe('time format precision rounding', function() {
var value = "4018.99999998843";
var testCases = [
{desc: "end-of-year thousandths rounding", format: "mm/dd/yyyy hh:mm:ss.000", expected: "12/31/1910 23:59:59.999"},
{desc: "end-of-year hundredths round up", format: "mm/dd/yyyy hh:mm:ss.00", expected: "01/01/1911 00:00:00.00"},
{desc: "end-of-year minutes round up", format: "mm/dd/yyyy hh:mm", expected: "01/01/1911 00:00"},
{desc: "hour duration thousandths rounding", format: "[hh]:mm:ss.000", expected: "96455:59:59.999"},
{desc: "hour duration hundredths round up", format: "[hh]:mm:ss.00", expected: "96456:00:00.00"},
{desc: "hour duration minute round up (w/ ss)", format: "[hh]:mm:ss", expected: "96456:00:00"},
{desc: "hour duration minute round up", format: "[hh]:mm", expected: "96456:00"},
{desc: "hour duration round up", format: "[hh]", expected: "96456"},
{desc: "minute duration thousandths rounding", format: "[mm]:ss.000", expected: "5787359:59.999"},
{desc: "minute duration hundredths round up", format: "[mm]:ss.00", expected: "5787360:00.00"},
{desc: "minute duration round up", format: "[mm]:ss", expected: "5787360:00"},
{desc: "second duration thousandths rounding", format: "[ss].000", expected: "347241599.999"},
{desc: "second duration hundredths round up", format: "[ss].00", expected: "347241600.00"},
{desc: "second duration round up", format: "[ss]", expected: "347241600"},
];
testCases.forEach(testCase => {
var headers = ["value", testCase.format];
it(testCase.desc, () => {testRow([value, testCase.expected], headers, {})});
});
});
describe('date formats', function() {
doit(process.env.MINTEST ? dates.slice(0,4000) : dates);
if(0) doit(process.env.MINTEST ? date2.slice(0,1000) : date2);