diff --git a/packages/ssf/bits/35_datecode.js b/packages/ssf/bits/35_datecode.js index e111e30..45ee400 100644 --- a/packages/ssf/bits/35_datecode.js +++ b/packages/ssf/bits/35_datecode.js @@ -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}; diff --git a/packages/ssf/bits/50_date.js b/packages/ssf/bits/50_date.js index 92167c8..3c66dba 100644 --- a/packages/ssf/bits/50_date.js +++ b/packages/ssf/bits/50_date.js @@ -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); diff --git a/packages/ssf/bits/82_eval.js b/packages/ssf/bits/82_eval.js index 90f0064..9d87f9f 100644 --- a/packages/ssf/bits/82_eval.js +++ b/packages/ssf/bits/82_eval.js @@ -115,31 +115,7 @@ function eval_fmt(fmt/*:string*/, v/*:any*/, opts/*:any*/, flen/*:number*/) { } /* 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 {nstr,out} = replace_fields(out, dt, ss0, v, opts); var vv = "", myv, ostr; if(nstr.length > 0) { if(nstr.charCodeAt(0) == 40) /* '(' */ { @@ -153,7 +129,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 +181,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; diff --git a/packages/ssf/ssf.flow.js b/packages/ssf/ssf.flow.js index 9fa401a..03d8809 100644 --- a/packages/ssf/ssf.flow.js +++ b/packages/ssf/ssf.flow.js @@ -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*/ } 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); @@ -810,31 +809,7 @@ function eval_fmt(fmt/*:string*/, v/*:any*/, opts/*:any*/, flen/*:number*/) { } /* 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 {nstr,out} = replace_fields(out, dt, ss0, v, opts); var vv = "", myv, ostr; if(nstr.length > 0) { if(nstr.charCodeAt(0) == 40) /* '(' */ { @@ -848,7 +823,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 +875,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 +1039,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; diff --git a/packages/ssf/ssf.js b/packages/ssf/ssf.js index d93de3c..5189668 100644 --- a/packages/ssf/ssf.js +++ b/packages/ssf/ssf.js @@ -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); @@ -801,30 +800,7 @@ if(dt.u >= 0.5) { dt.u = 0; ++dt.S; } } /* 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 {nstr,out} = replace_fields(out, dt, ss0, v, opts); var vv = "", myv, ostr; if(nstr.length > 0) { if(nstr.charCodeAt(0) == 40) /* '(' */ { @@ -838,7 +814,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 +866,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 +1025,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; diff --git a/packages/ssf/test/date.js b/packages/ssf/test/date.js index f854f5d..c12c274 100644 --- a/packages/ssf/test/date.js +++ b/packages/ssf/test/date.js @@ -12,21 +12,48 @@ 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('date formats', function() { doit(process.env.MINTEST ? dates.slice(0,4000) : dates); if(0) doit(process.env.MINTEST ? date2.slice(0,1000) : date2);