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