forked from sheetjs/sheetjs
		
	version bump 0.5.2: cleanup
more formats: o fractional seconds o absolute time formats o generalized engineering notation o better resolution in splitting format strings o support for LO uppercase date+time formats other changes - no more prototype pollution - many many more tests - passes jshint - code coverage: blanket + coveralls + travis - npmignore test files
This commit is contained in:
		
							parent
							
								
									c299585bfb
								
							
						
					
					
						commit
						b0b3ffea84
					
				
							
								
								
									
										5
									
								
								.npmignore
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										5
									
								
								.npmignore
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| test/*.tsv | ||||
| node_modules/ | ||||
| tmp/ | ||||
| .gitignore | ||||
| .vocrc | ||||
| @ -4,3 +4,7 @@ node_js: | ||||
|   - "0.8" | ||||
| before_install: | ||||
|   - "npm install -g mocha" | ||||
|   - "npm install blanket" | ||||
|   - "npm install coveralls mocha-lcov-reporter" | ||||
| after_success:  | ||||
|   - "make coveralls" | ||||
|  | ||||
							
								
								
									
										14
									
								
								Makefile
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										14
									
								
								Makefile
									
									
									
									
									
								
							| @ -4,3 +4,17 @@ ssf: ssf.md | ||||
| 
 | ||||
| test: | ||||
| 	npm test | ||||
| 
 | ||||
| .PHONY: lint | ||||
| lint: | ||||
| 	jshint ssf.js test/ | ||||
| 
 | ||||
| .PHONY: cov | ||||
| cov: tmp/coverage.html | ||||
| 
 | ||||
| tmp/coverage.html: ssf.md | ||||
| 	mocha --require blanket -R html-cov > tmp/coverage.html | ||||
| 
 | ||||
| .PHONY: coveralls | ||||
| coveralls: | ||||
| 	mocha --require blanket --reporter mocha-lcov-reporter | ./node_modules/coveralls/bin/coveralls.js | ||||
|  | ||||
							
								
								
									
										12
									
								
								README.md
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										12
									
								
								README.md
									
									
									
									
									
								
							| @ -23,11 +23,11 @@ define `DO_NOT_EXPORT_SSF`: | ||||
| 
 | ||||
| ## Usage | ||||
| 
 | ||||
| `.load(fmt, idx)` sets custom formats (generally indices above `164`) | ||||
| `.load(fmt, idx)` sets custom formats (generally indices above `164`). | ||||
| 
 | ||||
| `.format(fmt, val)` formats `val` using the format `fmt`.  If `fmt` is of type | ||||
| `number`, the internal table (and custom formats) will be used.  If `fmt` is a | ||||
| literal format, then it will be parsed and evaluated. | ||||
| `.format(fmt, val, opts)` formats `val` using the format `fmt`.  If `fmt` is of  | ||||
| type `number`, the internal table (and custom formats) will be used.  If `fmt`  | ||||
| is a literal format, then it will be parsed and evaluated. | ||||
| 
 | ||||
| `.parse_date_code(val, opts)` parses `val` as date code and returns object: | ||||
| 
 | ||||
| @ -36,6 +36,10 @@ literal format, then it will be parsed and evaluated. | ||||
| - `H,M,S,u`: (0-23)Hour, Minute, Second, Sub-second | ||||
| - `q`: Day of Week (0=Sunday, 1=Monday, ..., 5=Friday, 6=Saturday) | ||||
| 
 | ||||
| `.get_table()` gets the internal format table (number to format mapping). | ||||
| 
 | ||||
| `.load_table(table)` sets the internal format table. | ||||
| 
 | ||||
| ## Notes | ||||
| 
 | ||||
| Format code 14 in the spec is broken; the correct format is 'mm/dd/yy' (dashes, | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "ssf", | ||||
|   "version": "0.5.1", | ||||
|   "version": "0.5.2", | ||||
|   "author": "SheetJS", | ||||
|   "description": "pure-JS library to format data using ECMA-376 spreadsheet Format Codes", | ||||
|   "keywords": [ "format", "sprintf", "spreadsheet" ], | ||||
| @ -20,6 +20,11 @@ | ||||
|   "bin": { | ||||
|     "ssf": "./bin/ssf.njs" | ||||
|   }, | ||||
|   "config": { | ||||
|     "blanket": { | ||||
|       "pattern": "ssf.js" | ||||
|     } | ||||
|   }, | ||||
|   "bugs": { "url": "https://github.com/SheetJS/ssf/issues" }, | ||||
|   "license": "Apache-2.0", | ||||
|   "engines": { "node": ">=0.8" } | ||||
|  | ||||
							
								
								
									
										87
									
								
								ssf.js
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										87
									
								
								ssf.js
									
									
									
									
									
								
							| @ -1,8 +1,7 @@ | ||||
| /* ssf.js (C) 2013-2014 SheetJS -- http://sheetjs.com */ | ||||
| var SSF = {}; | ||||
| var make_ssf = function(SSF){ | ||||
| String.prototype.reverse=function(){return this.split("").reverse().join("");}; | ||||
| var _strrev = function(x) { return String(x).reverse(); }; | ||||
| var _strrev = function(x) { return String(x).split("").reverse().join("");}; | ||||
| function fill(c,l) { return new Array(l+1).join(c); } | ||||
| function pad(v,d,c){var t=String(v);return t.length>=d?t:(fill(c||0,d-t.length)+t);} | ||||
| function rpad(v,d,c){var t=String(v);return t.length>=d?t:(t+fill(c||0,d-t.length));} | ||||
| @ -14,6 +13,7 @@ opts_fmt.date1904 = 0; | ||||
| opts_fmt.output = ""; | ||||
| opts_fmt.mode = ""; | ||||
| var table_fmt = { | ||||
|   0:  'General', | ||||
|   1:  '0', | ||||
|   2:  '0.00', | ||||
|   3:  '#,##0', | ||||
| @ -83,7 +83,7 @@ var frac = function frac(x, D, mixed) { | ||||
|   if(Q > D) { Q = Q_1; P = P_1; } | ||||
|   if(Q > D) { Q = Q_2; P = P_2; } | ||||
|   if(!mixed) return [0, sgn * P, Q]; | ||||
|   if(Q==0) throw "Unexpected state: "+P+" "+P_1+" "+P_2+" "+Q+" "+Q_1+" "+Q_2; | ||||
|   if(Q===0) throw "Unexpected state: "+P+" "+P_1+" "+P_2+" "+Q+" "+Q_1+" "+Q_2; | ||||
|   var q = Math.floor(sgn * P/Q); | ||||
|   return [q, sgn*P - q*Q, Q]; | ||||
| }; | ||||
| @ -113,7 +113,7 @@ var general_fmt = function(v) { | ||||
| }; | ||||
| SSF._general = general_fmt; | ||||
| var parse_date_code = function parse_date_code(v,opts) { | ||||
|   var date = Math.floor(v), time = Math.floor(86400 * (v - date)), dow=0; | ||||
|   var date = Math.floor(v), time = Math.floor(86400 * (v - date)+1e-6), dow=0; | ||||
|   var dout=[], out={D:date, T:time, u:86400*(v-date)-time}; fixopts(opts = (opts||{})); | ||||
|   if(opts.date1904) date += 1462; | ||||
|   if(date > 2958465) return null; | ||||
| @ -126,7 +126,7 @@ var parse_date_code = function parse_date_code(v,opts) { | ||||
|     d.setDate(d.getDate() + date - 1); | ||||
|     dout = [d.getFullYear(), d.getMonth()+1,d.getDate()]; | ||||
|     dow = d.getDay(); | ||||
|     if(opts.mode === 'excel' && date < 60) dow = (dow + 6) % 7; | ||||
|     if(/* opts.mode === 'excel' && */ date < 60) dow = (dow + 6) % 7; | ||||
|   } | ||||
|   out.y = dout[0]; out.m = dout[1]; out.d = dout[2]; | ||||
|   out.S = time % 60; time = Math.floor(time / 60); | ||||
| @ -136,13 +136,16 @@ var parse_date_code = function parse_date_code(v,opts) { | ||||
|   return out; | ||||
| }; | ||||
| SSF.parse_date_code = parse_date_code; | ||||
| /*jshint -W086 */ | ||||
| var write_date = function(type, fmt, val) { | ||||
|   if(val < 0) return ""; | ||||
|   var o; | ||||
|   switch(type) { | ||||
|     case 'y': switch(fmt) { /* year */ | ||||
|       case 'y': case 'yy': return pad(val.y % 100,2); | ||||
|       default: return val.y; | ||||
|     } break; | ||||
|       case 'yyy': case 'yyyy': return pad(val.y % 10000,4); | ||||
|       default: throw 'bad year format: ' + fmt; | ||||
|     } | ||||
|     case 'm': switch(fmt) { /* month */ | ||||
|       case 'm': return val.m; | ||||
|       case 'mm': return pad(val.m,2); | ||||
| @ -150,47 +153,51 @@ var write_date = function(type, fmt, val) { | ||||
|       case 'mmmm': return months[val.m-1][2]; | ||||
|       case 'mmmmm': return months[val.m-1][0]; | ||||
|       default: throw 'bad month format: ' + fmt; | ||||
|     } break; | ||||
|     } | ||||
|     case 'd': switch(fmt) { /* day */ | ||||
|       case 'd': return val.d; | ||||
|       case 'dd': return pad(val.d,2); | ||||
|       case 'ddd': return days[val.q][0]; | ||||
|       case 'dddd': return days[val.q][1]; | ||||
|       default: throw 'bad day format: ' + fmt; | ||||
|     } break; | ||||
|     } | ||||
|     case 'h': switch(fmt) { /* 12-hour */ | ||||
|       case 'h': return 1+(val.H+11)%12; | ||||
|       case 'hh': return pad(1+(val.H+11)%12, 2); | ||||
|       default: throw 'bad hour format: ' + fmt; | ||||
|     } break; | ||||
|     } | ||||
|     case 'H': switch(fmt) { /* 24-hour */ | ||||
|       case 'h': return val.H; | ||||
|       case 'hh': return pad(val.H, 2); | ||||
|       default: throw 'bad hour format: ' + fmt; | ||||
|     } break; | ||||
|     } | ||||
|     case 'M': switch(fmt) { /* minutes */ | ||||
|       case 'm': return val.M; | ||||
|       case 'mm': return pad(val.M, 2); | ||||
|       default: throw 'bad minute format: ' + fmt; | ||||
|     } break; | ||||
|     } | ||||
|     case 's': switch(fmt) { /* seconds */ | ||||
|       case 's': return val.S; | ||||
|       case 's': return Math.round(val.S+val.u); | ||||
|       case 'ss': return pad(Math.round(val.S+val.u), 2); | ||||
|       case 'ss.0': var o = pad(Math.round(10*(val.S+val.u)),3); return o.substr(0,2)+"." + o.substr(2); | ||||
|       case 'ss.0': o = pad(Math.round(10*(val.S+val.u)),3); return o.substr(0,2)+"." + o.substr(2); | ||||
|       case 'ss.00': o = pad(Math.round(100*(val.S+val.u)),4); return o.substr(0,2)+"." + o.substr(2); | ||||
|       case 'ss.000': o = pad(Math.round(1000*(val.S+val.u)),5); return o.substr(0,2)+"." + o.substr(2); | ||||
|       default: throw 'bad second format: ' + fmt; | ||||
|     } break; | ||||
|     } | ||||
|     case 'Z': switch(fmt) { | ||||
|       case '[h]': return val.D*24+val.H; | ||||
|       case '[h]': case '[hh]': o = val.D*24+val.H; break; | ||||
|       case '[m]': case '[mm]': o = (val.D*24+val.H)*60+val.M; break; | ||||
|       case '[s]': case '[ss]': o = ((val.D*24+val.H)*60+val.M)*60+Math.round(val.S+val.u); break; | ||||
|       default: throw 'bad abstime format: ' + fmt; | ||||
|     } break; | ||||
|     } return fmt.length === 3 ? o : pad(o, 2); | ||||
|     /* TODO: handle the ECMA spec format ee -> yy */ | ||||
|     case 'e': { return val.y; } break; | ||||
|     case 'A': return (val.h>=12 ? 'P' : 'A') + fmt.substr(1); | ||||
|     default: throw 'bad format type ' + type + ' in ' + fmt; | ||||
|   } | ||||
| }; | ||||
| String.prototype.reverse = function() { return this.split("").reverse().join(""); }; | ||||
| var commaify = function(s) { return s.reverse().replace(/.../g,"$&,").reverse().replace(/^,/,""); }; | ||||
| /*jshint +W086 */ | ||||
| var commaify = function(s) { return _strrev(_strrev(s).replace(/.../g,"$&,")).replace(/^,/,""); }; | ||||
| var write_num = function(type, fmt, val) { | ||||
|   if(type === '(') { | ||||
|     var ffmt = fmt.replace(/\( */,"").replace(/ \)/,"").replace(/\)/,""); | ||||
| @ -202,15 +209,17 @@ var write_num = function(type, fmt, val) { | ||||
|   if(mul !== 0) return write_num(type, fmt, val * Math.pow(10,2*mul)) + fill("%",mul); | ||||
|   if(fmt.indexOf("E") > -1) { | ||||
|     var idx = fmt.indexOf("E") - fmt.indexOf(".") - 1; | ||||
|     //if(fmt.match(/^#+0\.0E\+0$/))
 | ||||
|     if(fmt == '##0.0E+0') { | ||||
|       var ee = (Number(val.toExponential(0).substr(2+(val<0))))%3; | ||||
|       o = (val/Math.pow(10,ee)).toPrecision(idx+1+(3+ee)%3); | ||||
|       var period = fmt.length - 5; | ||||
|       var ee = (Number(val.toExponential(0).substr(2+(val<0))))%period; | ||||
|       o = (val/Math.pow(10,ee)).toPrecision(idx+1+(period+ee)%period); | ||||
|       if(!o.match(/[Ee]/)) { | ||||
|         var fakee = (Number(val.toExponential(0).substr(2+(val<0)))); | ||||
|         if(o.indexOf(".") === -1) o = o[0] + "." + o.substr(1) + "E+" + (fakee - o.length+ee); | ||||
|         else throw "missing E"; | ||||
|         else throw "missing E |" + o; | ||||
|       } | ||||
|       o = o.replace(/^([+-]?)([0-9]*)\.([0-9]*)[Ee]/,function($$,$1,$2,$3) { return $1 + $2 + $3.substr(0,(3+ee)%3) + "." + $3.substr(ee) + "E"; }); | ||||
|       o = o.replace(/^([+-]?)([0-9]*)\.([0-9]*)[Ee]/,function($$,$1,$2,$3) { return $1 + $2 + $3.substr(0,(period+ee)%period) + "." + $3.substr(ee) + "E"; }); | ||||
|     } else o = val.toExponential(idx); | ||||
|     if(fmt.match(/E\+00$/) && o.match(/e[+-][0-9]$/)) o = o.substr(0,o.length-1) + "0" + o[o.length-1]; | ||||
|     if(fmt.match(/E\-/) && o.match(/e\+/)) o = o.replace(/e\+/,"e"); | ||||
| @ -223,6 +232,8 @@ var write_num = function(type, fmt, val) { | ||||
|     var myn = (rnd - base*den), myd = den; | ||||
|     return sign + (base?base:"") + " " + (myn === 0 ? fill(" ", r[1].length + 1 + r[2].length) : pad(myn,r[1].length," ") + "/" + pad(myd,r[2].length)); | ||||
|   } | ||||
|   if(fmt.match(/^00*$/)) return (val<0?"-":"")+pad(Math.round(aval),fmt.length); | ||||
|   if(fmt.match(/^####*$/)) return Math.round(val); | ||||
|   switch(fmt) { | ||||
|     case "0": return Math.round(val); | ||||
|     case "0.0": o = Math.round(val*10); | ||||
| @ -231,6 +242,9 @@ var write_num = function(type, fmt, val) { | ||||
|       return String(o/100).replace(/^([^\.]+)$/,"$1.00").replace(/\.$/,".00").replace(/\.([0-9])$/,".$1"+"0"); | ||||
|     case "0.000": o = Math.round(val*1000); | ||||
|       return String(o/1000).replace(/^([^\.]+)$/,"$1.000").replace(/\.$/,".000").replace(/\.([0-9])$/,".$1"+"00").replace(/\.([0-9][0-9])$/,".$1"+"0"); | ||||
|     case "#.##": o = Math.round(val*100); | ||||
|       return String(o/100).replace(/^([^\.]+)$/,"$1.").replace(/^0\.$/,"."); | ||||
|     case "#,###": var x = commaify(String(Math.round(aval))); return x !== "0" ? sign + x : ""; | ||||
|     case "#,##0": return sign + commaify(String(Math.round(aval))); | ||||
|     case "#,##0.0": r = Math.round((val-Math.floor(val))*10); return val < 0 ? "-" + write_num(type, fmt, -val) : commaify(String(Math.floor(val))) + "." + r; | ||||
|     case "#,##0.00": r = Math.round((val-Math.floor(val))*100); return val < 0 ? "-" + write_num(type, fmt, -val) : commaify(String(Math.floor(val))) + "." + (r < 10 ? "0"+r:r); | ||||
| @ -264,6 +278,10 @@ function eval_fmt(fmt, v, opts, flen) { | ||||
|   /* Tokenize */ | ||||
|   while(i < fmt.length) { | ||||
|     switch((c = fmt[i])) { | ||||
|       case 'G': /* General */ | ||||
|         if(fmt.substr(i, i+6).toLowerCase() !== "general") | ||||
|           throw 'unrecognized character ' + fmt[i] + ' in ' + fmt; | ||||
|         out.push({t:'G',v:'General'}); i+=7; break; | ||||
|       case '"': /* Literal text */ | ||||
|         for(o="";fmt[++i] !== '"' && i < fmt.length;) o += fmt[i]; | ||||
|         out.push({t:'t', v:o}); ++i; break; | ||||
| @ -273,14 +291,18 @@ function eval_fmt(fmt, v, opts, flen) { | ||||
|       case '@': /* Text Placeholder */ | ||||
|         out.push({t:'T', v:v}); ++i; break; | ||||
|       /* Dates */ | ||||
|       case 'M': case 'D': case 'Y': case 'H': case 'S': case 'E': | ||||
|         c = c.toLowerCase(); | ||||
|         /* falls through */ | ||||
|       case 'm': case 'd': case 'y': case 'h': case 's': case 'e': | ||||
|         if(v < 0) return ""; | ||||
|         if(!dt) dt = parse_date_code(v, opts); | ||||
|         if(!dt) return ""; | ||||
|         o = fmt[i]; while(fmt[++i] === c) o+=c; | ||||
|         o = fmt[i]; while((fmt[++i]||"").toLowerCase() === c) o+=c; | ||||
|         if(c === 's' && fmt[i] === '.' && fmt[i+1] === '0') { o+='.'; while(fmt[++i] === '0') o+= '0'; } | ||||
|         if(c === 'm' && lst.toLowerCase() === 'h') c = 'M'; /* m = minute */ | ||||
|         if(c === 'h') c = hr; | ||||
|         o = o.toLowerCase(); | ||||
|         q={t:c, v:o}; out.push(q); lst = c; break; | ||||
|       case 'A': | ||||
|         if(!dt) dt = parse_date_code(v, opts); | ||||
| @ -288,12 +310,16 @@ function eval_fmt(fmt, v, opts, flen) { | ||||
|         q={t:c,v:"A"}; | ||||
|         if(fmt.substr(i, 3) === "A/P") {q.v = dt.H >= 12 ? "P" : "A"; q.t = 'T'; hr='h';i+=3;} | ||||
|         else if(fmt.substr(i,5) === "AM/PM") { q.v = dt.H >= 12 ? "PM" : "AM"; q.t = 'T'; i+=5; hr='h'; } | ||||
|         else q.t = "t"; | ||||
|         else { q.t = "t"; i++; } | ||||
|         out.push(q); lst = c; break; | ||||
|       case '[': /* TODO: Fix this -- ignore all conditionals and formatting */ | ||||
|         o = c; | ||||
|         while(fmt[i++] !== ']') o += fmt[i]; | ||||
|         if(o == "[h]") out.push({t:'Z', v:o}); | ||||
|         if(o.match(/\[[HhMmSs]*\]/)) { | ||||
|           if(!dt) dt = parse_date_code(v, opts); | ||||
|           if(!dt) return ""; | ||||
|           out.push({t:'Z', v:o.toLowerCase()}); | ||||
|         } else { o=""; } | ||||
|         break; | ||||
|       /* Numbers */ | ||||
|       case '0': case '#': | ||||
| @ -340,6 +366,7 @@ function eval_fmt(fmt, v, opts, flen) { | ||||
|         out[i].v = write_num(out[i].t, out[i].v, v); | ||||
|         out[i].t = 't'; | ||||
|         i = jj-1; break; | ||||
|       case 'G': out[i].t = 't'; out[i].v = general_fmt(v,opts); break; | ||||
|       default: throw "unrecognized type " + out[i].t; | ||||
|     } | ||||
|   } | ||||
| @ -351,8 +378,9 @@ function choose_fmt(fmt, v, o) { | ||||
|   if(typeof fmt === "string") fmt = split_fmt(fmt); | ||||
|   var l = fmt.length; | ||||
|   switch(fmt.length) { | ||||
|     case 1: fmt = [fmt[0], fmt[0], fmt[0], "@"]; break; | ||||
|     case 2: fmt = [fmt[0], fmt[fmt[1] === "@"?0:1], fmt[0], "@"]; break; | ||||
|     case 1: fmt = fmt[0].indexOf("@")>-1 ? ["General", "General", "General", fmt[0]] : [fmt[0], fmt[0], fmt[0], "@"]; break; | ||||
|     case 2: fmt = fmt[1].indexOf("@")>-1 ? [fmt[0], fmt[0], fmt[0], fmt[1]] : [fmt[0], fmt[1], fmt[0], "@"]; break; | ||||
|     case 3: fmt = fmt[2].indexOf("@")>-1 ? [fmt[0], fmt[1], fmt[0], fmt[2]] : [fmt[0], fmt[1], fmt[2], "@"]; break; | ||||
|     case 4: break; | ||||
|     default: throw "cannot find right format for |" + fmt + "|"; | ||||
|   } | ||||
| @ -361,10 +389,11 @@ function choose_fmt(fmt, v, o) { | ||||
| } | ||||
| var format = function format(fmt,v,o) { | ||||
|   fixopts(o = (o||{})); | ||||
|   if(fmt === 0 || (typeof fmt === "string" && fmt.toLowerCase() === "general")) return general_fmt(v, o); | ||||
|   if(typeof fmt === "string" && fmt.toLowerCase() === "general") return general_fmt(v, o); | ||||
|   if(typeof fmt === 'number') fmt = (o.table || table_fmt)[fmt]; | ||||
|   var f = choose_fmt(fmt, v, o); | ||||
|   if(f[1].toLowerCase() === "general") return general_fmt(v,o); | ||||
|   if(v === true) v = "TRUE"; if(v === false) v = "FALSE"; | ||||
|   return eval_fmt(f[1], v, o, f[0]); | ||||
| }; | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										258
									
								
								ssf.md
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										258
									
								
								ssf.md
									
									
									
									
									
								
							| @ -180,6 +180,7 @@ None of the international formats are included here. | ||||
| 
 | ||||
| ```js>tmp/20_consts.js | ||||
| var table_fmt = { | ||||
|   0:  'General', | ||||
|   1:  '0', | ||||
|   2:  '0.00', | ||||
|   3:  '#,##0', | ||||
| @ -262,7 +263,7 @@ portion of a 24 hour day). | ||||
| 
 | ||||
| ```js>tmp/50_date.js | ||||
| var parse_date_code = function parse_date_code(v,opts) { | ||||
|   var date = Math.floor(v), time = Math.floor(86400 * (v - date)), dow=0; | ||||
|   var date = Math.floor(v), time = Math.floor(86400 * (v - date)+1e-6), dow=0; | ||||
|   var dout=[], out={D:date, T:time, u:86400*(v-date)-time}; fixopts(opts = (opts||{})); | ||||
| ``` | ||||
| 
 | ||||
| @ -310,7 +311,7 @@ Saturday.  The "right" thing to do is to keep the DOW consistent and just break | ||||
| the fact that there are two Wednesdays in that "week". | ||||
| 
 | ||||
| ``` | ||||
|     if(opts.mode === 'excel' && date < 60) dow = (dow + 6) % 7; | ||||
|     if(/* opts.mode === 'excel' && */ date < 60) dow = (dow + 6) % 7; | ||||
|   } | ||||
| ``` | ||||
| 
 | ||||
| @ -330,8 +331,7 @@ SSF.parse_date_code = parse_date_code; | ||||
| ## Evaluating Number Formats | ||||
| 
 | ||||
| ```js>tmp/60_number.js | ||||
| String.prototype.reverse = function() { return this.split("").reverse().join(""); }; | ||||
| var commaify = function(s) { return s.reverse().replace(/.../g,"$&,").reverse().replace(/^,/,""); }; | ||||
| var commaify = function(s) { return _strrev(_strrev(s).replace(/.../g,"$&,")).replace(/^,/,""); }; | ||||
| var write_num = function(type, fmt, val) { | ||||
| ``` | ||||
| 
 | ||||
| @ -364,9 +364,11 @@ For exponents, get the exponent and mantissa and format them separately: | ||||
| For the special case of engineering notation, "shift" the decimal: | ||||
| 
 | ||||
| ``` | ||||
|     //if(fmt.match(/^#+0\.0E\+0$/)) | ||||
|     if(fmt == '##0.0E+0') { | ||||
|       var ee = (Number(val.toExponential(0).substr(2+(val<0))))%3; | ||||
|       o = (val/Math.pow(10,ee)).toPrecision(idx+1+(3+ee)%3); | ||||
|       var period = fmt.length - 5; | ||||
|       var ee = (Number(val.toExponential(0).substr(2+(val<0))))%period; | ||||
|       o = (val/Math.pow(10,ee)).toPrecision(idx+1+(period+ee)%period); | ||||
|       if(!o.match(/[Ee]/)) { | ||||
| ``` | ||||
| 
 | ||||
| @ -375,9 +377,9 @@ TODO: something reasonable | ||||
| ``` | ||||
|         var fakee = (Number(val.toExponential(0).substr(2+(val<0)))); | ||||
|         if(o.indexOf(".") === -1) o = o[0] + "." + o.substr(1) + "E+" + (fakee - o.length+ee); | ||||
|         else throw "missing E"; | ||||
|         else throw "missing E |" + o; | ||||
|       } | ||||
|       o = o.replace(/^([+-]?)([0-9]*)\.([0-9]*)[Ee]/,function($$,$1,$2,$3) { return $1 + $2 + $3.substr(0,(3+ee)%3) + "." + $3.substr(ee) + "E"; }); | ||||
|       o = o.replace(/^([+-]?)([0-9]*)\.([0-9]*)[Ee]/,function($$,$1,$2,$3) { return $1 + $2 + $3.substr(0,(period+ee)%period) + "." + $3.substr(ee) + "E"; }); | ||||
|     } else o = val.toExponential(idx); | ||||
|     if(fmt.match(/E\+00$/) && o.match(/e[+-][0-9]$/)) o = o.substr(0,o.length-1) + "0" + o[o.length-1]; | ||||
|     if(fmt.match(/E\-/) && o.match(/e\+/)) o = o.replace(/e\+/,"e"); | ||||
| @ -402,6 +404,13 @@ Fractions with known denominator are resolved by rounding: | ||||
|   } | ||||
| ``` | ||||
| 
 | ||||
| A few special general cases can be handled in a very dumb manner: | ||||
| 
 | ||||
| ``` | ||||
|   if(fmt.match(/^00*$/)) return (val<0?"-":"")+pad(Math.round(aval),fmt.length); | ||||
|   if(fmt.match(/^####*$/)) return Math.round(val); | ||||
| ``` | ||||
| 
 | ||||
| The default cases are hard-coded.  TODO: actually parse them | ||||
| 
 | ||||
| ```js>tmp/60_number.js | ||||
| @ -413,6 +422,9 @@ The default cases are hard-coded.  TODO: actually parse them | ||||
|       return String(o/100).replace(/^([^\.]+)$/,"$1.00").replace(/\.$/,".00").replace(/\.([0-9])$/,".$1"+"0"); | ||||
|     case "0.000": o = Math.round(val*1000); | ||||
|       return String(o/1000).replace(/^([^\.]+)$/,"$1.000").replace(/\.$/,".000").replace(/\.([0-9])$/,".$1"+"00").replace(/\.([0-9][0-9])$/,".$1"+"0"); | ||||
|     case "#.##": o = Math.round(val*100); | ||||
|       return String(o/100).replace(/^([^\.]+)$/,"$1.").replace(/^0\.$/,"."); | ||||
|     case "#,###": var x = commaify(String(Math.round(aval))); return x !== "0" ? sign + x : ""; | ||||
|     case "#,##0": return sign + commaify(String(Math.round(aval))); | ||||
|     case "#,##0.0": r = Math.round((val-Math.floor(val))*10); return val < 0 ? "-" + write_num(type, fmt, -val) : commaify(String(Math.floor(val))) + "." + r; | ||||
|     case "#,##0.00": r = Math.round((val-Math.floor(val))*100); return val < 0 ? "-" + write_num(type, fmt, -val) : commaify(String(Math.floor(val))) + "." + (r < 10 ? "0"+r:r); | ||||
| @ -442,6 +454,15 @@ function eval_fmt(fmt, v, opts, flen) { | ||||
|     switch((c = fmt[i])) { | ||||
| ``` | ||||
| 
 | ||||
| LO Formats sometimes leak "GENERAL" or "General" to stand for general format: | ||||
| 
 | ||||
| ``` | ||||
|       case 'G': /* General */ | ||||
|         if(fmt.substr(i, i+6).toLowerCase() !== "general") | ||||
|           throw 'unrecognized character ' + fmt[i] + ' in ' + fmt; | ||||
|         out.push({t:'G',v:'General'}); i+=7; break; | ||||
| ``` | ||||
| 
 | ||||
| Text between double-quotes are treated literally, and individual characters are | ||||
| literal if they are preceded by a slash. | ||||
| 
 | ||||
| @ -478,6 +499,9 @@ The date codes `m,d,y,h,s` are standard.  There are some special formats like | ||||
| 
 | ||||
| ``` | ||||
|       /* Dates */ | ||||
|       case 'M': case 'D': case 'Y': case 'H': case 'S': case 'E': | ||||
|         c = c.toLowerCase(); | ||||
|         /* falls through */ | ||||
|       case 'm': case 'd': case 'y': case 'h': case 's': case 'e': | ||||
| ``` | ||||
| 
 | ||||
| @ -492,7 +516,7 @@ Merge strings like "mmmmm" or "hh" into one block: | ||||
| ``` | ||||
|         if(!dt) dt = parse_date_code(v, opts); | ||||
|         if(!dt) return ""; | ||||
|         o = fmt[i]; while(fmt[++i] === c) o+=c; | ||||
|         o = fmt[i]; while((fmt[++i]||"").toLowerCase() === c) o+=c; | ||||
| ``` | ||||
| 
 | ||||
| For the special case of s.00, the suffix should be swallowed with the s: | ||||
| @ -506,6 +530,7 @@ Only the forward corrections are made here.  The reverse corrections are made la | ||||
| ``` | ||||
|         if(c === 'm' && lst.toLowerCase() === 'h') c = 'M'; /* m = minute */ | ||||
|         if(c === 'h') c = hr; | ||||
|         o = o.toLowerCase(); | ||||
|         q={t:c, v:o}; out.push(q); lst = c; break; | ||||
| ``` | ||||
| 
 | ||||
| @ -524,18 +549,22 @@ the HH/hh jazz.  TODO: investigate this further. | ||||
|         q={t:c,v:"A"}; | ||||
|         if(fmt.substr(i, 3) === "A/P") {q.v = dt.H >= 12 ? "P" : "A"; q.t = 'T'; hr='h';i+=3;} | ||||
|         else if(fmt.substr(i,5) === "AM/PM") { q.v = dt.H >= 12 ? "PM" : "AM"; q.t = 'T'; i+=5; hr='h'; } | ||||
|         else q.t = "t"; | ||||
|         else { q.t = "t"; i++; } | ||||
|         out.push(q); lst = c; break; | ||||
| ``` | ||||
| 
 | ||||
| Conditional and color blocks should be handled at one point (TODO).  For now, | ||||
| only the absolute time `[h]` is captured (using the pseudo-type `Z`): | ||||
| Conditional and color blocks should be handled at one point (TODO).  The  | ||||
| pseudo-type `Z` is used to capture absolute time blocks: | ||||
| 
 | ||||
| ``` | ||||
|       case '[': /* TODO: Fix this -- ignore all conditionals and formatting */ | ||||
|         o = c; | ||||
|         while(fmt[i++] !== ']') o += fmt[i]; | ||||
|         if(o == "[h]") out.push({t:'Z', v:o}); | ||||
|         if(o.match(/\[[HhMmSs]*\]/)) { | ||||
|           if(!dt) dt = parse_date_code(v, opts); | ||||
|           if(!dt) return ""; | ||||
|           out.push({t:'Z', v:o.toLowerCase()}); | ||||
|         } else { o=""; } | ||||
|         break; | ||||
| ``` | ||||
| 
 | ||||
| @ -617,6 +646,7 @@ The default magic characters are listed in subsubsections 18.8.30-31 of ECMA376: | ||||
|         out[i].v = write_num(out[i].t, out[i].v, v); | ||||
|         out[i].t = 't'; | ||||
|         i = jj-1; break; | ||||
|       case 'G': out[i].t = 't'; out[i].v = general_fmt(v,opts); break; | ||||
|       default: throw "unrecognized type " + out[i].t; | ||||
|     } | ||||
|   } | ||||
| @ -632,15 +662,17 @@ There is some overloading of the `m` character.  According to the spec: | ||||
| hours) or immediately before the "ss" code (for seconds), the application shall | ||||
| display minutes instead of the month. | ||||
| 
 | ||||
| 
 | ||||
| ```js>tmp/50_date.js | ||||
| /*jshint -W086 */ | ||||
| var write_date = function(type, fmt, val) { | ||||
|   if(val < 0) return ""; | ||||
|   var o; | ||||
|   switch(type) { | ||||
|     case 'y': switch(fmt) { /* year */ | ||||
|       case 'y': case 'yy': return pad(val.y % 100,2); | ||||
|       default: return val.y; | ||||
|     } break; | ||||
|       case 'yyy': case 'yyyy': return pad(val.y % 10000,4); | ||||
|       default: throw 'bad year format: ' + fmt; | ||||
|     } | ||||
|     case 'm': switch(fmt) { /* month */ | ||||
|       case 'm': return val.m; | ||||
|       case 'mm': return pad(val.m,2); | ||||
| @ -648,44 +680,48 @@ var write_date = function(type, fmt, val) { | ||||
|       case 'mmmm': return months[val.m-1][2]; | ||||
|       case 'mmmmm': return months[val.m-1][0]; | ||||
|       default: throw 'bad month format: ' + fmt; | ||||
|     } break; | ||||
|     } | ||||
|     case 'd': switch(fmt) { /* day */ | ||||
|       case 'd': return val.d; | ||||
|       case 'dd': return pad(val.d,2); | ||||
|       case 'ddd': return days[val.q][0]; | ||||
|       case 'dddd': return days[val.q][1]; | ||||
|       default: throw 'bad day format: ' + fmt; | ||||
|     } break; | ||||
|     } | ||||
|     case 'h': switch(fmt) { /* 12-hour */ | ||||
|       case 'h': return 1+(val.H+11)%12; | ||||
|       case 'hh': return pad(1+(val.H+11)%12, 2); | ||||
|       default: throw 'bad hour format: ' + fmt; | ||||
|     } break; | ||||
|     } | ||||
|     case 'H': switch(fmt) { /* 24-hour */ | ||||
|       case 'h': return val.H; | ||||
|       case 'hh': return pad(val.H, 2); | ||||
|       default: throw 'bad hour format: ' + fmt; | ||||
|     } break; | ||||
|     } | ||||
|     case 'M': switch(fmt) { /* minutes */ | ||||
|       case 'm': return val.M; | ||||
|       case 'mm': return pad(val.M, 2); | ||||
|       default: throw 'bad minute format: ' + fmt; | ||||
|     } break; | ||||
|     } | ||||
|     case 's': switch(fmt) { /* seconds */ | ||||
|       case 's': return val.S; | ||||
|       case 's': return Math.round(val.S+val.u); | ||||
|       case 'ss': return pad(Math.round(val.S+val.u), 2); | ||||
|       case 'ss.0': var o = pad(Math.round(10*(val.S+val.u)),3); return o.substr(0,2)+"." + o.substr(2); | ||||
|       case 'ss.0': o = pad(Math.round(10*(val.S+val.u)),3); return o.substr(0,2)+"." + o.substr(2); | ||||
|       case 'ss.00': o = pad(Math.round(100*(val.S+val.u)),4); return o.substr(0,2)+"." + o.substr(2); | ||||
|       case 'ss.000': o = pad(Math.round(1000*(val.S+val.u)),5); return o.substr(0,2)+"." + o.substr(2); | ||||
|       default: throw 'bad second format: ' + fmt; | ||||
|     } break; | ||||
|     } | ||||
| ``` | ||||
| 
 | ||||
| The `Z` type refers to absolute time measures: | ||||
| 
 | ||||
| ``` | ||||
|     case 'Z': switch(fmt) { | ||||
|       case '[h]': return val.D*24+val.H; | ||||
|       case '[h]': case '[hh]': o = val.D*24+val.H; break; | ||||
|       case '[m]': case '[mm]': o = (val.D*24+val.H)*60+val.M; break; | ||||
|       case '[s]': case '[ss]': o = ((val.D*24+val.H)*60+val.M)*60+Math.round(val.S+val.u); break; | ||||
|       default: throw 'bad abstime format: ' + fmt; | ||||
|     } break; | ||||
|     } return fmt.length === 3 ? o : pad(o, 2); | ||||
| ``` | ||||
| 
 | ||||
| The `e` format behavior in excel diverges from the spec.  It claims that `ee` | ||||
| @ -698,6 +734,7 @@ should be a two-digit year, but `ee` in excel is actually the four-digit year: | ||||
|     default: throw 'bad format type ' + type + ' in ' + fmt; | ||||
|   } | ||||
| }; | ||||
| /*jshint +W086 */ | ||||
| ``` | ||||
| 
 | ||||
| Based on the value, `choose_fmt` picks the right format string.  If formats have | ||||
| @ -709,8 +746,21 @@ function choose_fmt(fmt, v, o) { | ||||
|   if(typeof fmt === "string") fmt = split_fmt(fmt); | ||||
|   var l = fmt.length; | ||||
|   switch(fmt.length) { | ||||
|     case 1: fmt = [fmt[0], fmt[0], fmt[0], "@"]; break; | ||||
|     case 2: fmt = [fmt[0], fmt[fmt[1] === "@"?0:1], fmt[0], "@"]; break; | ||||
| ``` | ||||
| 
 | ||||
| In the case of one format, if it contains an "@" then it is a text format.   | ||||
| There is a big TODO here regarding how to best handle this case. | ||||
| 
 | ||||
| ``` | ||||
|     case 1: fmt = fmt[0].indexOf("@")>-1 ? ["General", "General", "General", fmt[0]] : [fmt[0], fmt[0], fmt[0], "@"]; break; | ||||
| ``` | ||||
| 
 | ||||
| In the case of 2 or 3 formats, if an `@` appears in the last field of the format | ||||
| it is treated as the text format | ||||
| 
 | ||||
| ``` | ||||
|     case 2: fmt = fmt[1].indexOf("@")>-1 ? [fmt[0], fmt[0], fmt[0], fmt[1]] : [fmt[0], fmt[1], fmt[0], "@"]; break; | ||||
|     case 3: fmt = fmt[2].indexOf("@")>-1 ? [fmt[0], fmt[1], fmt[0], fmt[2]] : [fmt[0], fmt[1], fmt[2], "@"]; break; | ||||
|     case 4: break; | ||||
|     default: throw "cannot find right format for |" + fmt + "|"; | ||||
|   } | ||||
| @ -729,10 +779,19 @@ var format = function format(fmt,v,o) { | ||||
| LibreOffice appears to emit the format "GENERAL" for general: | ||||
| 
 | ||||
| ``` | ||||
|   if(fmt === 0 || (typeof fmt === "string" && fmt.toLowerCase() === "general")) return general_fmt(v, o); | ||||
|   if(typeof fmt === "string" && fmt.toLowerCase() === "general") return general_fmt(v, o); | ||||
|   if(typeof fmt === 'number') fmt = (o.table || table_fmt)[fmt]; | ||||
|   var f = choose_fmt(fmt, v, o); | ||||
|   if(f[1].toLowerCase() === "general") return general_fmt(v,o); | ||||
| ``` | ||||
| 
 | ||||
| The boolean TRUE and FALSE are formatted as if they are the uppercase text: | ||||
| 
 | ||||
| ``` | ||||
|   if(v === true) v = "TRUE"; if(v === false) v = "FALSE"; | ||||
| ``` | ||||
| 
 | ||||
| ``` | ||||
|   return eval_fmt(f[1], v, o, f[0]); | ||||
| }; | ||||
| 
 | ||||
| @ -779,7 +838,7 @@ var frac = function frac(x, D, mixed) { | ||||
|   if(Q > D) { Q = Q_1; P = P_1; } | ||||
|   if(Q > D) { Q = Q_2; P = P_2; } | ||||
|   if(!mixed) return [0, sgn * P, Q]; | ||||
|   if(Q==0) throw "Unexpected state: "+P+" "+P_1+" "+P_2+" "+Q+" "+Q_1+" "+Q_2; | ||||
|   if(Q===0) throw "Unexpected state: "+P+" "+P_1+" "+P_2+" "+Q+" "+Q_1+" "+Q_2; | ||||
|   var q = Math.floor(sgn * P/Q); | ||||
|   return [q, sgn*P - q*Q, Q]; | ||||
| }; | ||||
| @ -791,8 +850,7 @@ var frac = function frac(x, D, mixed) { | ||||
| /* ssf.js (C) 2013-2014 SheetJS -- http://sheetjs.com */ | ||||
| var SSF = {}; | ||||
| var make_ssf = function(SSF){ | ||||
| String.prototype.reverse=function(){return this.split("").reverse().join("");}; | ||||
| var _strrev = function(x) { return String(x).reverse(); }; | ||||
| var _strrev = function(x) { return String(x).split("").reverse().join("");}; | ||||
| function fill(c,l) { return new Array(l+1).join(c); } | ||||
| function pad(v,d,c){var t=String(v);return t.length>=d?t:(fill(c||0,d-t.length)+t);} | ||||
| function rpad(v,d,c){var t=String(v);return t.length>=d?t:(t+fill(c||0,d-t.length));} | ||||
| @ -825,6 +883,14 @@ node_modules/ | ||||
| .vocrc | ||||
| ``` | ||||
| 
 | ||||
| ```>.npmignore | ||||
| test/*.tsv | ||||
| node_modules/ | ||||
| tmp/ | ||||
| .gitignore | ||||
| .vocrc | ||||
| ``` | ||||
| 
 | ||||
| ```make>Makefile | ||||
| .PHONY: test ssf | ||||
| ssf: ssf.md | ||||
| @ -832,12 +898,36 @@ ssf: ssf.md | ||||
| 
 | ||||
| test: | ||||
|         npm test | ||||
| 
 | ||||
| .PHONY: lint | ||||
| lint: | ||||
|         jshint ssf.js test/ | ||||
| ``` | ||||
| 
 | ||||
| Coverage tests use [blanket](http://npm.im/blanket): | ||||
| 
 | ||||
| ``` | ||||
| 
 | ||||
| .PHONY: cov | ||||
| cov: tmp/coverage.html | ||||
| 
 | ||||
| tmp/coverage.html: ssf.md | ||||
|         mocha --require blanket -R html-cov > tmp/coverage.html | ||||
| ``` | ||||
| 
 | ||||
| Coveralls.io support | ||||
| 
 | ||||
| ``` | ||||
| 
 | ||||
| .PHONY: coveralls | ||||
| coveralls: | ||||
|         mocha --require blanket --reporter mocha-lcov-reporter | ./node_modules/coveralls/bin/coveralls.js | ||||
| ``` | ||||
| 
 | ||||
| ```json>package.json | ||||
| { | ||||
|   "name": "ssf", | ||||
|   "version": "0.5.1", | ||||
|   "version": "0.5.2", | ||||
|   "author": "SheetJS", | ||||
|   "description": "pure-JS library to format data using ECMA-376 spreadsheet Format Codes", | ||||
|   "keywords": [ "format", "sprintf", "spreadsheet" ], | ||||
| @ -857,6 +947,11 @@ test: | ||||
|   "bin": { | ||||
|     "ssf": "./bin/ssf.njs" | ||||
|   }, | ||||
|   "config": { | ||||
|     "blanket": { | ||||
|       "pattern": "ssf.js" | ||||
|     } | ||||
|   }, | ||||
|   "bugs": { "url": "https://github.com/SheetJS/ssf/issues" }, | ||||
|   "license": "Apache-2.0", | ||||
|   "engines": { "node": ">=0.8" } | ||||
| @ -874,6 +969,10 @@ node_js: | ||||
|   - "0.8" | ||||
| before_install: | ||||
|   - "npm install -g mocha" | ||||
|   - "npm install blanket" | ||||
|   - "npm install coveralls mocha-lcov-reporter" | ||||
| after_success:  | ||||
|   - "make coveralls" | ||||
| ``` | ||||
| 
 | ||||
| The mocha test driver tests the implied formats: | ||||
| @ -910,6 +1009,12 @@ describe('General format', function() { | ||||
|       assert.equal(SSF.format(d[1], d[0], {}), d[2]); | ||||
|     }); | ||||
|   }); | ||||
|   it('should fail for undefined and null', function() { | ||||
|     assert.throws(function() { | ||||
|       SSF.format("General", undefined); | ||||
|       SSF.format("General", null); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| @ -924,7 +1029,7 @@ var skip = []; | ||||
| describe('fractional formats', function() { | ||||
|   data.forEach(function(d) { | ||||
|     it(d[1]+" for "+d[0], skip.indexOf(d[1]) > -1 ? null : function(){ | ||||
|       var expected = d[2], actual = SSF.format(d[1], d[0], {}) | ||||
|       var expected = d[2], actual = SSF.format(d[1], d[0], {}); | ||||
|       //var r = actual.match(/(-?)\d* *\d+\/\d+/); | ||||
|       assert.equal(actual, expected); | ||||
|     }); | ||||
| @ -932,6 +1037,91 @@ describe('fractional formats', function() { | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| The dates test driver tests the date and time formats: | ||||
| 
 | ||||
| ```js>test/date.js | ||||
| /* vim: set ts=2: */ | ||||
| /*jshint loopfunc:true */ | ||||
| var SSF = require('../'); | ||||
| var fs = require('fs'), assert = require('assert'); | ||||
| var dates = fs.readFileSync('./test/dates.tsv','utf8').split("\n"); | ||||
| var times = fs.readFileSync('./test/times.tsv','utf8').split("\n"); | ||||
| 
 | ||||
| function doit(data) { | ||||
|   var step = Math.ceil(data.length/100), i = 1; | ||||
|   var headers = data[0].split("\t"); | ||||
|   for(j=0;j<=100;++j) it(j, function() { | ||||
|     for(var k = 0; k <= step; ++k,++i) { | ||||
|       if(!data[i]) 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], Number(d[0]), {}); | ||||
|         if(actual != expected) throw [actual, expected, w, headers[w],d[0],d].join("|"); | ||||
|         actual = SSF.format(headers[w].toUpperCase(), Number(d[0]), {}); | ||||
|         if(actual != expected) throw [actual, expected, w, headers[w],d[0],d].join("|"); | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| describe('time formats', function() { doit(times.slice(0,1000)); }); | ||||
| describe('date formats', function() { | ||||
|   doit(dates); | ||||
|   it('should fail for bad formats', function() { | ||||
|     var bad = ['yyyyy', 'mmmmmm', 'ddddd']; | ||||
|     var chk = function(fmt){ return function(){ SSF.format(fmt,0); }; }; | ||||
|     bad.forEach(function(fmt){assert.throws(chk(fmt));}); | ||||
|   }); | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| The exponential test driver tests exponential formats (pipe denotes fails) | ||||
| 
 | ||||
| ```js>test/exp.js | ||||
| /* vim: set ts=2: */ | ||||
| /*jshint loopfunc:true */ | ||||
| var SSF = require('../'); | ||||
| var fs = require('fs'), assert = require('assert'); | ||||
| var data = fs.readFileSync('./test/exp.tsv','utf8').split("\n"); | ||||
| function doit(d, headers) { | ||||
|   it(d[0], function() { | ||||
|     for(var w = 2; w < 3 /*TODO: 1:headers.length */; ++w) { | ||||
|       var expected = d[w].replace("|", ""), actual; | ||||
|       try { actual = SSF.format(headers[w], Number(d[0]), {}); } catch(e) { } | ||||
|       if(actual != expected && d[w][0] !== "|") throw [actual, expected, w, headers[w],d[0],d].join("|"); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| describe('exponential formats', function() { | ||||
|   var headers = data[0].split("\t"); | ||||
|   for(var j=14/* TODO: start from 1 */;j<data.length;++j) { | ||||
|     if(!data[j]) return; | ||||
|     doit(data[j].replace(/#{255}/g,"").split("\t"), headers); | ||||
|   } | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| The oddities test driver tests random odd formats | ||||
| 
 | ||||
| ```js>test/oddities.js | ||||
| /* vim: set ts=2: */ | ||||
| /*jshint loopfunc:true */ | ||||
| var SSF = require('../'); | ||||
| var fs = require('fs'), assert = require('assert'); | ||||
| var data = JSON.parse(fs.readFileSync('./test/oddities.json','utf8')); | ||||
| describe('oddities', function() { | ||||
|   data.forEach(function(d) { | ||||
|     it(d[0], function(){ | ||||
|       for(j=1;j<d.length;++j) { | ||||
|         if(d[j].length == 2) { | ||||
|           var expected = d[j][1], actual = SSF.format(d[0], d[j][0], {}); | ||||
|           assert.equal(actual, expected); | ||||
|         } else assert.throws(function() { SSF.format(d[0], d[j][0]); }); | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| # LICENSE | ||||
| 
 | ||||
| ```>LICENSE | ||||
|  | ||||
							
								
								
									
										32
									
								
								test/date.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										32
									
								
								test/date.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| /* vim: set ts=2: */ | ||||
| /*jshint loopfunc:true */ | ||||
| var SSF = require('../'); | ||||
| var fs = require('fs'), assert = require('assert'); | ||||
| var dates = fs.readFileSync('./test/dates.tsv','utf8').split("\n"); | ||||
| var times = fs.readFileSync('./test/times.tsv','utf8').split("\n"); | ||||
| 
 | ||||
| function doit(data) { | ||||
|   var step = Math.ceil(data.length/100), i = 1; | ||||
|   var headers = data[0].split("\t"); | ||||
|   for(j=0;j<=100;++j) it(j, function() { | ||||
|     for(var k = 0; k <= step; ++k,++i) { | ||||
|       if(!data[i]) 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], Number(d[0]), {}); | ||||
|         if(actual != expected) throw [actual, expected, w, headers[w],d[0],d].join("|"); | ||||
|         actual = SSF.format(headers[w].toUpperCase(), Number(d[0]), {}); | ||||
|         if(actual != expected) throw [actual, expected, w, headers[w],d[0],d].join("|"); | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| describe('time formats', function() { doit(times.slice(0,1000)); }); | ||||
| describe('date formats', function() { | ||||
|   doit(dates); | ||||
|   it('should fail for bad formats', function() { | ||||
|     var bad = ['yyyyy', 'mmmmmm', 'ddddd']; | ||||
|     var chk = function(fmt){ return function(){ SSF.format(fmt,0); }; }; | ||||
|     bad.forEach(function(fmt){assert.throws(chk(fmt));}); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										295849
									
								
								test/dates.tsv
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										295849
									
								
								test/dates.tsv
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										21
									
								
								test/exp.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										21
									
								
								test/exp.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| /* vim: set ts=2: */ | ||||
| /*jshint loopfunc:true */ | ||||
| var SSF = require('../'); | ||||
| var fs = require('fs'), assert = require('assert'); | ||||
| var data = fs.readFileSync('./test/exp.tsv','utf8').split("\n"); | ||||
| function doit(d, headers) { | ||||
|   it(d[0], function() { | ||||
|     for(var w = 2; w < 3 /*TODO: 1:headers.length */; ++w) { | ||||
|       var expected = d[w].replace("|", ""), actual; | ||||
|       try { actual = SSF.format(headers[w], Number(d[0]), {}); } catch(e) { } | ||||
|       if(actual != expected && d[w][0] !== "|") throw [actual, expected, w, headers[w],d[0],d].join("|"); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| describe('exponential formats', function() { | ||||
|   var headers = data[0].split("\t"); | ||||
|   for(var j=14/* TODO: start from 1 */;j<data.length;++j) { | ||||
|     if(!data[j]) return; | ||||
|     doit(data[j].replace(/#{255}/g,"").split("\t"), headers); | ||||
|   } | ||||
| }); | ||||
							
								
								
									
										46
									
								
								test/exp.tsv
									
									
									
									
									
										Normal file
									
								
							
							
								
									
								
								
								
								
								
									
									
								
							
						
						
									
										46
									
								
								test/exp.tsv
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| value	#0.0E+0	##0.0E+0	###0.0E+0	####0.0E+0 | ||||
| 1.23457E-13	12.3E-14	123.5E-15	1234.6E-16	123.5E-15 | ||||
| 1.23457E-12	1.2E-12	1.2E-12	1.2E-12	1234.6E-15 | ||||
| 1.23457E-11	12.3E-12	12.3E-12	12.3E-12	12345.7E-15 | ||||
| 1.23457E-10	1.2E-10	123.5E-12	123.5E-12	1.2E-10 | ||||
| 1.23457E-09	12.3E-10	1.2E-9	1234.6E-12	12.3E-10 | ||||
| 1.23457E-08	1.2E-8	12.3E-9	1.2E-8	123.5E-10 | ||||
| 0.000000123457	12.3E-8	123.5E-9	12.3E-8	1234.6E-10 | ||||
| 0.00000123457	1.2E-6	1.2E-6	123.5E-8	12345.7E-10 | ||||
| 0.0000123457	12.3E-6	12.3E-6	1234.6E-8	1.2E-5 | ||||
| 0.000123457	1.2E-4	123.5E-6	1.2E-4	12.3E-5 | ||||
| 0.001234568	12.3E-4	1.2E-3	12.3E-4	123.5E-5 | ||||
| 0.012345679	1.2E-2	12.3E-3	123.5E-4	1234.6E-5 | ||||
| 0.123456789	12.3E-2	123.5E-3	1234.6E-4	12345.7E-5 | ||||
| 1.23456789	1.2E+0	|1.2E+0	1.2E+0	1.2E+0 | ||||
| 12.3456789	12.3E+0	|12.3E+0	12.3E+0	12.3E+0 | ||||
| 123.456789	1.2E+2	|123.5E+0	123.5E+0	123.5E+0 | ||||
| 1234.56789	12.3E+2	1.2E+3	1234.6E+0	1234.6E+0 | ||||
| 12345.6789	1.2E+4	12.3E+3	1.2E+4	12345.7E+0 | ||||
| 123456.789	12.3E+4	123.5E+3	12.3E+4	1.2E+5 | ||||
| 1234567.89	1.2E+6	1.2E+6	123.5E+4	12.3E+5 | ||||
| 12345678.9	12.3E+6	12.3E+6	1234.6E+4	123.5E+5 | ||||
| 123456789	1.2E+8	123.5E+6	1.2E+8	1234.6E+5 | ||||
| 1234567890	12.3E+8	1.2E+9	12.3E+8	12345.7E+5 | ||||
| 12345678900	1.2E+10	12.3E+9	123.5E+8	1.2E+10 | ||||
| 123456789000	12.3E+10	123.5E+9	1234.6E+8	12.3E+10 | ||||
| 1234567890000	1.2E+12	1.2E+12	1.2E+12	123.5E+10 | ||||
| 12345678900000	12.3E+12	12.3E+12	12.3E+12	1234.6E+10 | ||||
| 123456789000000	1.2E+14	123.5E+12	123.5E+12	12345.7E+10 | ||||
| 1234567890000000	12.3E+14	1.2E+15	1234.6E+12	1.2E+15 | ||||
| 12345678900000000	1.2E+16	12.3E+15	1.2E+16	12.3E+15 | ||||
| 123456789000000000	12.3E+16	123.5E+15	12.3E+16	123.5E+15 | ||||
| 1234567890000000000	1.2E+18	1.2E+18	123.5E+16	1234.6E+15 | ||||
| 12345678900000000000	12.3E+18	12.3E+18	1234.6E+16	12345.7E+15 | ||||
| 123456789000000000000	1.2E+20	123.5E+18	1.2E+20	1.2E+20 | ||||
| 1234567890000000000000	12.3E+20	1.2E+21	12.3E+20	12.3E+20 | ||||
| 12345678900000000000000	1.2E+22	12.3E+21	123.5E+20	123.5E+20 | ||||
| 123456789000000000000000	12.3E+22	123.5E+21	1234.6E+20	1234.6E+20 | ||||
| 1234567890000000000000000	1.2E+24	1.2E+24	1.2E+24	12345.7E+20 | ||||
| 12345678900000000000000000	12.3E+24	12.3E+24	12.3E+24	1.2E+25 | ||||
| 123456789000000000000000000	1.2E+26	123.5E+24	123.5E+24	12.3E+25 | ||||
| 1234567890000000000000000000	12.3E+26	1.2E+27	1234.6E+24	123.5E+25 | ||||
| 12345678900000000000000000000	1.2E+28	12.3E+27	1.2E+28	|1234.6E+25 | ||||
| 123456789000000000000000000000	|12.3E+28	123.5E+27	|12.3E+28	|12345.7E+25 | ||||
| 1234567890000000000000000000000	1.2E+30	1.2E+30	|123.5E+28	1.2E+30 | ||||
| 12345678900000000000000000000000	|12.3E+30	12.3E+30	|1234.6E+28	|12.3E+30 | ||||
| 
 | 
| @ -6,7 +6,7 @@ var skip = []; | ||||
| describe('fractional formats', function() { | ||||
|   data.forEach(function(d) { | ||||
|     it(d[1]+" for "+d[0], skip.indexOf(d[1]) > -1 ? null : function(){ | ||||
|       var expected = d[2], actual = SSF.format(d[1], d[0], {}) | ||||
|       var expected = d[2], actual = SSF.format(d[1], d[0], {}); | ||||
|       //var r = actual.match(/(-?)\d* *\d+\/\d+/);
 | ||||
|       assert.equal(actual, expected); | ||||
|     }); | ||||
|  | ||||
| @ -9,4 +9,10 @@ describe('General format', function() { | ||||
|       assert.equal(SSF.format(d[1], d[0], {}), d[2]); | ||||
|     }); | ||||
|   }); | ||||
|   it('should fail for undefined and null', function() { | ||||
|     assert.throws(function() { | ||||
|       SSF.format("General", undefined); | ||||
|       SSF.format("General", null); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
							
								
								
									
										17
									
								
								test/oddities.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										17
									
								
								test/oddities.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| /* vim: set ts=2: */ | ||||
| /*jshint loopfunc:true */ | ||||
| var SSF = require('../'); | ||||
| var fs = require('fs'), assert = require('assert'); | ||||
| var data = JSON.parse(fs.readFileSync('./test/oddities.json','utf8')); | ||||
| describe('oddities', function() { | ||||
|   data.forEach(function(d) { | ||||
|     it(d[0], function(){ | ||||
|       for(j=1;j<d.length;++j) { | ||||
|         if(d[j].length == 2) { | ||||
|           var expected = d[j][1], actual = SSF.format(d[0], d[j][0], {}); | ||||
|           assert.equal(actual, expected); | ||||
|         } else assert.throws(function() { SSF.format(d[0], d[j][0]); }); | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										38
									
								
								test/oddities.json
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										38
									
								
								test/oddities.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| [ | ||||
|   ["\"foo\";\"bar\";\"baz\";\"qux\"", | ||||
|     [1, "foo"], [-1, "bar"], [0, "baz"], ["sheetjs", "qux"] | ||||
|   ], | ||||
|   ["\"foo\";\"bar\";\"baz\"", | ||||
|     [1, "foo"], [-1, "bar"], [0, "baz"], ["sheetjs", "sheetjs"] | ||||
|   ], | ||||
|   ["\"foo\";\"bar\";@", | ||||
|     [1, "foo"], [-1, "bar"], [0, "foo"], ["sheetjs", "sheetjs"] | ||||
|   ], | ||||
|   ["\"foo\";\"bar\"", | ||||
|     [1, "foo"], [-1, "bar"], [0, "foo"], ["sheetjs", "sheetjs"] | ||||
|   ], | ||||
|   ["@@", [1, "1"], [-1, "-1"], [0, "0"], ["sheetjs", "sheetjssheetjs"]], | ||||
|   ["[Blue]General", [1, "1"], [-1, "-1"], [0, "0"], ["sheetjs", "sheetjs"]], | ||||
|   ["[Blue]G3neral", [1], [-1], [0], ["TODO","TODO"]], | ||||
|   ["A\"TODO\"", [1, "ATODO"], [-1, "ATODO"], [0, "ATODO"], ["TODO","TODO"]], | ||||
|   ["r", [1], [-1], [0], ["TODO","TODO"]], | ||||
|   ["((;@", [1], [-1], [0], ["TODO","TODO"]], | ||||
|   ["\\r", [1, "r"], [-1, "r"], [0, "r"], ["TODO","TODO"]], | ||||
|   ["_($* #,##0_);_($* (#,##0);_($* \"-\"_);_(@_)", [1, " $1 "], [-1, " $(1)"], [0," $- "], ["TODO", " TODO "]], | ||||
|   ["#,##0.0", [1, "1.0"], [-1, "-1.0"], [0,"0.0"], ["TODO", "TODO"]], | ||||
|   ["#,###", [1, "1"], [-1, "-1"], [0,""], ["TODO", "TODO"]], | ||||
|   ["#.##", [1, "1."], [-1, "-1."], [0,"."], ["sheetjs", "sheetjs"]], | ||||
|   ["0;0", [1.1, "1"], [-1.1, "-1"], [0,"0"], ["sheetjs", "sheetjs"]], | ||||
|   ["0.0", [1, "1.0"], [-1, "-1.0"], [0,"0.0"], ["sheetjs", "sheetjs"]], | ||||
|   ["0.00", [1, "1.00"], [-1, "-1.00"], [0,"0.00"], ["sheetjs", "sheetjs"]], | ||||
|   ["0.000", [1, "1.000"], [-1, "-1.000"], [0,"0.000"], ["sheetjs", "sheetjs"]], | ||||
|   ["hh:mm AM/PM", [0.7, "04:48 PM"]], | ||||
|   ["hhh:mm AM/PM", [0.7]], | ||||
|   ["hhh:mmm:sss", [0.7]], | ||||
|   ["hh:mmm:sss", [0.7]], | ||||
|   ["hh:mm:sss", [0.7]], | ||||
|   ["[hhh]", [0.7]], | ||||
|   ["A/P", [0.7, "P"]], | ||||
|   ["e", [0.7, "1900"]], | ||||
|   ["\"foo\";\"bar\";\"baz\";\"qux\";\"foobar\"", [1], [0], [-1], ["sheetjs"]] | ||||
| ] | ||||
							
								
								
									
										1048576
									
								
								test/times.tsv
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										1048576
									
								
								test/times.tsv
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
		Reference in New Issue
	
	Block a user