forked from sheetjs/sheetjs
		
	fix rounding issue
See: #2560 - Improved rounding to account for minutes, hours, etc. - Added tests for rounding dates/times
This commit is contained in:
		
							parent
							
								
									a373597294
								
							
						
					
					
						commit
						c580ffcbc2
					
				| @ -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); | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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); | ||||
| @ -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; | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user