forked from sheetjs/sheetjs
		
	Initial commit
This commit is contained in:
		
						commit
						e176abd8de
					
				
							
								
								
									
										6
									
								
								.travis.yml
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										6
									
								
								.travis.yml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| language: node_js | ||||
| node_js: | ||||
|   - "0.10" | ||||
|   - "0.8" | ||||
| before_install: | ||||
|   - "npm install -g mocha" | ||||
							
								
								
									
										13
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										13
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| Copyright 2013   SheetJS | ||||
| 
 | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
| 
 | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
| 
 | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
							
								
								
									
										35
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										35
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| # SSF | ||||
| 
 | ||||
| SpreadSheet Format (SSF) is a pure-JS library to format data using ECMA-376  | ||||
| spreadsheet format codes (like those used in Microsoft Excel) | ||||
| 
 | ||||
| This is written in [voc](https://npmjs.org/package/voc) -- see ssf.md for code. | ||||
| 
 | ||||
| To build: `voc ssf.md` | ||||
| 
 | ||||
| ## Setup | ||||
| 
 | ||||
| In the browser: | ||||
| 
 | ||||
|     <script src="ssf.js"></script> | ||||
| 
 | ||||
| In node: | ||||
| 
 | ||||
|     var SSF = require('ssf'); | ||||
| 
 | ||||
| ## Usage | ||||
| 
 | ||||
| `.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. | ||||
| 
 | ||||
| ## Notes | ||||
| 
 | ||||
| Format code 14 in the spec is broken; the correct format is 'mm/dd/yy' (dashes, | ||||
| not spaces) | ||||
| 
 | ||||
| ## License | ||||
| 
 | ||||
| Apache 2.0 | ||||
							
								
								
									
										22
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										22
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| { | ||||
|   "name": "ssf", | ||||
|   "version": "0.2.0", | ||||
|   "author": "SheetJS", | ||||
|   "description": "pure-JS library to format data using ECMA-376 spreadsheet Format Codes", | ||||
|   "keywords": [ "format", "sprintf", "spreadsheet" ], | ||||
|   "main": "ssf_node.js", | ||||
|   "dependencies": { | ||||
|     "voc":"", | ||||
|     "colors":"" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "mocha":"" | ||||
|   }, | ||||
|   "repository": { "type":"git", "url":"git://github.com/SheetJS/ssf.git" }, | ||||
|   "scripts": { | ||||
|     "test": "mocha -R spec" | ||||
|   }, | ||||
|   "bugs": { "url": "https://github.com/SheetJS/ssf/issues" }, | ||||
|   "license": "Apache-2.0", | ||||
|   "engines": { "node": ">=0.8" } | ||||
| } | ||||
							
								
								
									
										231
									
								
								ssf.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										231
									
								
								ssf.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,231 @@ | ||||
| var SSF; | ||||
| (function(SSF){ | ||||
| String.prototype.reverse=function(){return this.split("").reverse().join("");}; | ||||
| var _strrev = function(x) { return String(x).reverse(); }; | ||||
| function fill(c,l) { return new Array(l+1).join(c); } | ||||
| function pad(v,d){var t=String(v);return t.length>=d?t:(fill(0,d-t.length)+t);} | ||||
| /* Options */ | ||||
| var opts_fmt = {}; | ||||
| function fixopts(o){for(var y in opts_fmt) if(o[y]===undefined) o[y]=opts_fmt[y];} | ||||
| SSF.opts = opts_fmt; | ||||
| opts_fmt.date1904 = 0; | ||||
| opts_fmt.output = ""; | ||||
| opts_fmt.mode = ""; | ||||
| var table_fmt = { | ||||
|   1:  '0', | ||||
|   2:  '0.00', | ||||
|   3:  '#,##0', | ||||
|   4:  '#,##0.00', | ||||
|   9:  '0%', | ||||
|   10: '0.00%', | ||||
|   11: '0.00E+00', | ||||
|   12: '# ?/?', | ||||
|   13: '# ??/??', | ||||
|   14: 'mm/dd/yy', | ||||
|   15: 'd-mmm-yy', | ||||
|   16: 'd-mmm', | ||||
|   17: 'mmm-yy', | ||||
|   18: 'h:mm AM/PM', | ||||
|   19: 'h:mm:ss AM/PM', | ||||
|   20: 'h:mm', | ||||
|   21: 'h:mm:ss', | ||||
|   22: 'm/d/yy h:mm', | ||||
|   37: '#,##0 ;(#,##0)', | ||||
|   38: '#,##0 ;[Red](#,##0)', | ||||
|   39: '#,##0.00;(#,##0.00)', | ||||
|   40: '#,##0.00;[Red](#,##0.00)', | ||||
|   45: 'mm:ss', | ||||
|   46: '[h]:mm:ss', | ||||
|   47: 'mmss.0', | ||||
|   48: '##0.0E+0', | ||||
|   49: '@' | ||||
| }; | ||||
| var days = [ | ||||
|   ['Sun', 'Sunday'], | ||||
|   ['Mon', 'Monday'], | ||||
|   ['Tue', 'Tuesday'], | ||||
|   ['Wed', 'Wednesday'], | ||||
|   ['Thu', 'Thursday'], | ||||
|   ['Fri', 'Friday'], | ||||
|   ['Sat', 'Saturday'] | ||||
| ]; | ||||
| var months = [ | ||||
|   ['J', 'Jan', 'January'], | ||||
|   ['F', 'Feb', 'February'], | ||||
|   ['M', 'Mar', 'March'], | ||||
|   ['A', 'Apr', 'April'], | ||||
|   ['M', 'May', 'May'], | ||||
|   ['J', 'Jun', 'June'], | ||||
|   ['J', 'Jul', 'July'], | ||||
|   ['A', 'Aug', 'August'], | ||||
|   ['S', 'Sep', 'September'], | ||||
|   ['O', 'Oct', 'October'], | ||||
|   ['N', 'Nov', 'November'], | ||||
|   ['D', 'Dec', 'December'] | ||||
| ]; | ||||
| var general_fmt = function(v) { | ||||
|   if(typeof v === 'boolean') return v ? "TRUE" : "FALSE"; | ||||
| }; | ||||
| SSF._general = general_fmt; | ||||
| var parse_date_code = function parse_date_code(v,opts) { | ||||
|   var date = Math.floor(v), time = Math.round(86400 * (v - date)), dow=0; | ||||
|   var dout=[], out={D:date, T:time}; fixopts(opts = (opts||{})); | ||||
|   if(opts.date1904) date += 1462; | ||||
|   if(date === 60) {dout = [1900,2,29]; dow=3;} | ||||
|   else { | ||||
|     if(date > 60) --date; | ||||
|     /* 1 = Jan 1 1900 */ | ||||
|     var d = new Date(1900,0,1); | ||||
|     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; | ||||
|   } | ||||
|   out.y = dout[0]; out.m = dout[1]; out.d = dout[2]; | ||||
|   out.S = time % 60; time = Math.floor(time / 60); | ||||
|   out.M = time % 60; time = Math.floor(time / 60); | ||||
|   out.H = time; | ||||
|   out.q = dow; | ||||
|   return out; | ||||
| }; | ||||
| SSF.parse_date_code = parse_date_code; | ||||
| var write_date = function(type, fmt, val) { | ||||
|   switch(type) { | ||||
|     case 'y': switch(fmt) { /* year */ | ||||
|       case 'y': case 'yy': return pad(val.y % 100,2); | ||||
|       default: return val.y; | ||||
|     } break; | ||||
|     case 'm': switch(fmt) { /* month */ | ||||
|       case 'm': return val.m; | ||||
|       case 'mm': return pad(val.m,2); | ||||
|       case 'mmm': return months[val.m-1][1]; | ||||
|       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 'ss': return pad(val.S, 2); | ||||
|       default: throw 'bad second format: ' + fmt; | ||||
|     } break; | ||||
|     /* 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; | ||||
|   } | ||||
| }; | ||||
| function split_fmt(fmt) { | ||||
|   var out = []; | ||||
|   var in_str = -1; | ||||
|   for(var i = 0, j = 0; i < fmt.length; ++i) { | ||||
|     if(in_str != -1) { if(fmt[i] == '"') in_str = -1; continue; } | ||||
|     if(fmt[i] == "_" || fmt[i] == "*" || fmt[i] == "\\") { ++i; continue; } | ||||
|     if(fmt[i] == '"') { in_str = i; continue; } | ||||
|     if(fmt[i] != ";") continue; | ||||
|     out.push(fmt.slice(j,i)); | ||||
|     j = i+1; | ||||
|   } | ||||
|   out.push(fmt.slice(j)); | ||||
|   if(in_str !=-1) throw "Format |" + fmt + "| unterminated string at " + in_str; | ||||
|   return out; | ||||
| } | ||||
| SSF._split = split_fmt; | ||||
| function eval_fmt(fmt, v, opts) { | ||||
|   var out = [], o = "", i = 0, c = "", lst='t', q = {}, dt; | ||||
|   fixopts(opts = (opts || {})); | ||||
|   var hr='H'; | ||||
|   /* Tokenize */ | ||||
|   while(i < fmt.length) { | ||||
|     switch((c = fmt[i])) { | ||||
|       case '"': /* Literal text */ | ||||
|         for(o="";fmt[++i] !== '"';) o += fmt[(fmt[i] === '\\' ? ++i : i)]; | ||||
|         out.push({t:'t', v:o}); break; | ||||
|       case '\\': out.push({t:'t', v:fmt[++i]}); ++i; break; | ||||
|       case '@': /* Text Placeholder */ | ||||
|         out.push({t:'T', v:v}); ++i; break; | ||||
|       /* Dates */ | ||||
|       case 'm': case 'd': case 'y': case 'h': case 's': case 'e': | ||||
|         if(!dt) dt = parse_date_code(v, opts); | ||||
|         o = fmt[i]; while(fmt[++i] === c) o+=c; | ||||
|         if(c === 'm' && lst.toLowerCase() === 'h') c = 'M'; /* m = minute */ | ||||
|         if(c === 'h') c = hr; | ||||
|         q={t:c, v:o}; out.push(q); lst = c; break; | ||||
|       case 'A': | ||||
|         if(!dt) dt = parse_date_code(v, opts); | ||||
|         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"; | ||||
|         out.push(q); lst = c; break; | ||||
|       case '[': /* TODO: Fix this -- ignore all conditionals and formatting */ | ||||
|         while(fmt[i++] !== ']'); break; | ||||
|       default: | ||||
|         if("$-+/():!^&'~{}<>= ".indexOf(c) === -1) | ||||
|           throw 'unrecognized character ' + fmt[i] + ' in ' + fmt; | ||||
|         out.push({t:'t', v:c}); ++i; break; | ||||
|     } | ||||
|   } | ||||
|   /* walk backwards */ | ||||
|   for(i=out.length-1, lst='t'; i >= 0; --i) { | ||||
|     switch(out[i].t) { | ||||
|       case 'h': case 'H': out[i].t = hr; lst='h'; break; | ||||
|       case 'd': case 'y': case 's': case 'M': case 'e': lst=out[i].t; break; | ||||
|       case 'm': if(lst === 's') out[i].t = 'M'; break; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /* replace fields */ | ||||
|   for(i=0; i < out.length; ++i) { | ||||
|     switch(out[i].t) { | ||||
|       case 't': case 'T': break; | ||||
|       case 'd': case 'm': case 'y': case 'h': case 'H': case 'M': case 's': case 'A': case 'e': | ||||
|         out[i].v = write_date(out[i].t, out[i].v, dt); | ||||
|         out[i].t = 't'; break; | ||||
|       default: throw "unrecognized type " + out[i].t; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return out.map(function(x){return x.v;}).join(""); | ||||
| } | ||||
| SSF._eval = eval_fmt; | ||||
| function choose_fmt(fmt, v) { | ||||
|   if(typeof fmt === "string") fmt = split_fmt(fmt); | ||||
|   if(typeof v !== "number") return fmt[3]; | ||||
|   return v > 0 ? fmt[0] : v < 0 ? fmt[1] : fmt[2]; | ||||
| } | ||||
| 
 | ||||
| var format = function format(fmt,v,o) { | ||||
|   fixopts(o = (o||{})); | ||||
|   if(fmt === 0) return general_fmt(v, o); | ||||
|   if(typeof fmt === 'number') fmt = table_fmt[fmt]; | ||||
|   var f = choose_fmt(fmt, v, o); | ||||
|   return eval_fmt(f, v, o); | ||||
| }; | ||||
| 
 | ||||
| SSF._choose = choose_fmt; | ||||
| SSF._table = table_fmt; | ||||
| SSF.load = function(fmt, idx) { table_fmt[idx] = fmt; }; | ||||
| SSF.format = format; | ||||
| })(SSF); | ||||
							
								
								
									
										648
									
								
								ssf.md
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										648
									
								
								ssf.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,648 @@ | ||||
| # SSF | ||||
| 
 | ||||
| SpreadSheet Format (SSF) is a pure-JS library to format data using ECMA-376 | ||||
| spreadsheet format codes. | ||||
| 
 | ||||
| ## Options | ||||
| 
 | ||||
| The various API functions take an `opts` argument which control parsing.  The | ||||
| default options are described below: | ||||
| 
 | ||||
| ```js>tmp/opts.js | ||||
| /* Options */ | ||||
| var opts_fmt = {}; | ||||
| function fixopts(o){for(var y in opts_fmt) if(o[y]===undefined) o[y]=opts_fmt[y];} | ||||
| SSF.opts = opts_fmt; | ||||
| ``` | ||||
| 
 | ||||
| There are two commonly-recognized date code formats: | ||||
|  - 1900 mode (where date=0 is 1899-12-31) | ||||
|  - 1904 mode (where date=0 is 1904-01-01) | ||||
| 
 | ||||
| The difference between the the 1900 and 1904 date modes is 1462 days.  Since | ||||
| the 1904 date mode was only default in a few Mac variants of Excel (2011 uses | ||||
| 1900 mode), the default is 1900 mode.  Consistent with ECMA-376 the name is | ||||
| `date1904`: | ||||
| 
 | ||||
| ``` | ||||
| opts_fmt.date1904 = 0; | ||||
| ``` | ||||
| 
 | ||||
| The default output is a text representation (no effort to capture colors).  To | ||||
| control the output, set the `output` variable: | ||||
| 
 | ||||
| - `text`: no color (default) | ||||
| - `html`: html output using | ||||
| - `ansi`: ansi color codes (requires `colors` module) | ||||
| 
 | ||||
| ``` | ||||
| opts_fmt.output = ""; | ||||
| ``` | ||||
| 
 | ||||
| There are a few places where the specification is ambiguous or where Excel does | ||||
| not follow the spec.  They are noted in the document. | ||||
| 
 | ||||
| The `mode` option controls compatibility: | ||||
| 
 | ||||
| - `ssf`: options that the author believes makes the most sense (default) | ||||
| - `ecma`: compatibility with ECMA-376 | ||||
| - `excel`: compatibility with MS-XLSX | ||||
| 
 | ||||
| ``` | ||||
| opts_fmt.mode = ""; | ||||
| ``` | ||||
| 
 | ||||
| ## Conditional Format Codes | ||||
| 
 | ||||
| The specification is a bit unclear here.  It initially claims in §18.3.1: | ||||
| 
 | ||||
| > Up to four sections of format codes can be specified. The format codes, | ||||
| separated by semicolons, define the formats for positive numbers, negative | ||||
| numbers, zero values, and text, in that order. | ||||
| 
 | ||||
| Semicolons can be escaped with the `\` character, so we need to split on those | ||||
| semicolons that aren't prefaced by a slash or within a quoted string: | ||||
| 
 | ||||
| ```js>tmp/main.js | ||||
| function split_fmt(fmt) { | ||||
|   var out = []; | ||||
|   var in_str = -1; | ||||
|   for(var i = 0, j = 0; i < fmt.length; ++i) { | ||||
|     if(in_str != -1) { if(fmt[i] == '"') in_str = -1; continue; } | ||||
|     if(fmt[i] == "_" || fmt[i] == "*" || fmt[i] == "\\") { ++i; continue; } | ||||
|     if(fmt[i] == '"') { in_str = i; continue; } | ||||
|     if(fmt[i] != ";") continue; | ||||
|     out.push(fmt.slice(j,i)); | ||||
|     j = i+1; | ||||
|   } | ||||
|   out.push(fmt.slice(j)); | ||||
|   if(in_str !=-1) throw "Format |" + fmt + "| unterminated string at " + in_str; | ||||
|   return out; | ||||
| } | ||||
| SSF._split = split_fmt; | ||||
| ``` | ||||
| 
 | ||||
| But it also allows for conditional formatting: | ||||
| 
 | ||||
| > To set number formats that are applied only if a number meets a specified | ||||
| > condition, enclose the condition in square brackets. The condition consists | ||||
| > of a comparison operator and a value. Comparison operators include: | ||||
| > `=` Equal to; | ||||
| > `>` Greater than; | ||||
| > `<` Less than; | ||||
| > `>=` Greater than or equal to, | ||||
| > `<=` Less than or equal to, | ||||
| > and `<>` Not equal to. | ||||
| 
 | ||||
| One problem is that Excel doesn't support three conditionals.  For example: | ||||
| 
 | ||||
| ```> | ||||
| [Red][<-25]General;[Blue][>25]General;[Green][<>0]General;[Yellow]General | ||||
| ``` | ||||
| 
 | ||||
| One would expect that the format code would color all numbers that are `< -25` | ||||
| in red, all numbers `> 25` in blue, nonzero numbers between `-25` and `25` in | ||||
| green, and color `0` and text in yellow.  Excel doesn't do that. | ||||
| 
 | ||||
| The two-conditional case works in an "expected" way if you interpret the third | ||||
| clause as the case for numbers that don't fit the first two: | ||||
| 
 | ||||
| ```> | ||||
| [Red][<-25]General;[Blue][>25]General;[Green]General;[Yellow]General | ||||
| ``` | ||||
| 
 | ||||
| will render values below `-25` as Red, above `25` as Blue, Green for other | ||||
| numbers, and Yellow for text. | ||||
| 
 | ||||
| Only the text case is allowed to have the `@` text sigil.  Excel interprets it | ||||
| as the last format. | ||||
| 
 | ||||
| 
 | ||||
| ## General Number Format | ||||
| 
 | ||||
| The 'general' format for spreadsheets (identified by format code 0) is highly | ||||
| context-sensitive and the implementation tries to follow the format to the best | ||||
| of its abilities given the knowledge. | ||||
| 
 | ||||
| ```js>tmp/general.js | ||||
| var general_fmt = function(v) { | ||||
| ``` | ||||
| 
 | ||||
| Booleans are serialized in upper case: | ||||
| 
 | ||||
| ``` | ||||
|   if(typeof v === 'boolean') return v ? "TRUE" : "FALSE"; | ||||
| ``` | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| ``` | ||||
| }; | ||||
| SSF._general = general_fmt; | ||||
| ``` | ||||
| 
 | ||||
| ## Implied Number Formats | ||||
| 
 | ||||
| These are the commonly-used formats that have a special implied code. | ||||
| None of the international formats are included here. | ||||
| 
 | ||||
| ```js>tmp/consts.js | ||||
| var table_fmt = { | ||||
|   1:  '0', | ||||
|   2:  '0.00', | ||||
|   3:  '#,##0', | ||||
|   4:  '#,##0.00', | ||||
|   9:  '0%', | ||||
|   10: '0.00%', | ||||
|   11: '0.00E+00', | ||||
|   12: '# ?/?', | ||||
|   13: '# ??/??', | ||||
| ``` | ||||
| 
 | ||||
| Now Excel and other formats treat code 14 as `mm/dd/yy` (with slashes).  Given | ||||
| that the spec gives no internationalization considerations, erring on the side | ||||
| of the applications makes sense here: | ||||
| 
 | ||||
| ``` | ||||
|   14: 'mm/dd/yy', | ||||
|   15: 'd-mmm-yy', | ||||
|   16: 'd-mmm', | ||||
|   17: 'mmm-yy', | ||||
|   18: 'h:mm AM/PM', | ||||
|   19: 'h:mm:ss AM/PM', | ||||
|   20: 'h:mm', | ||||
|   21: 'h:mm:ss', | ||||
|   22: 'm/d/yy h:mm', | ||||
|   37: '#,##0 ;(#,##0)', | ||||
|   38: '#,##0 ;[Red](#,##0)', | ||||
|   39: '#,##0.00;(#,##0.00)', | ||||
|   40: '#,##0.00;[Red](#,##0.00)', | ||||
|   45: 'mm:ss', | ||||
|   46: '[h]:mm:ss', | ||||
|   47: 'mmss.0', | ||||
|   48: '##0.0E+0', | ||||
|   49: '@' | ||||
| }; | ||||
| ``` | ||||
| 
 | ||||
| These test cases were manually generated in Excel 2011 [value, code, result]: | ||||
| 
 | ||||
| ```json>test/implied.json | ||||
| [ | ||||
|   [12345.6789, 0, "12345.6789"], | ||||
|   [12345.6789, 1, "12346"], | ||||
|   [12345.6789, 2, "12345.68"], | ||||
|   [12345.6789, 3, "12,346"], | ||||
|   [12345.6789, 4, "12,345.68"], | ||||
|   [12345.6789, 9, "1234568%"], | ||||
|   [12345.6789, 10, "1234567.89%"], | ||||
|   [12345.6789, 11, "1.23E+04"], | ||||
|   [12345.6789, 12, "12345 2/3"], | ||||
|   [12345.6789, 13, "12345 55/81"], | ||||
|   [12345.6789, 14, "10/18/33"], | ||||
|   [12345.6789, 15, "18-Oct-33"], | ||||
|   [12345.6789, 16, "18-Oct"], | ||||
|   [12345.6789, 17, "Oct-33"], | ||||
|   [12345.6789, 18, "4:17 PM"], | ||||
|   [12345.6789, 19, "4:17:37 PM"], | ||||
|   [12345.6789, 20, "16:17"], | ||||
|   [12345.6789, 21, "16:17:37"], | ||||
|   [12345.6789, 22, "10/18/33 16:17"], | ||||
|   [12345.6789, 37, "12,346"], | ||||
|   [12345.6789, 38, "12,346"], | ||||
|   [12345.6789, 39, "12,345.68"], | ||||
|   [12345.6789, 40, "12,345.68"], | ||||
|   [12345.6789, 45, "17:37"], | ||||
|   [12345.6789, 46, "296296:17:37"], | ||||
|   [12345.6789, 47, "1737.0"], | ||||
|   [12345.6789, 48, "12.3E+3"], | ||||
|   [12345.6789, 49, "12345.6789"] | ||||
| ] | ||||
| ``` | ||||
| 
 | ||||
| ## Dates and Time | ||||
| 
 | ||||
| The code `ddd` displays short day-of-week and `dddd` shows long day-of-week: | ||||
| 
 | ||||
| ```js>tmp/consts.js | ||||
| var days = [ | ||||
|   ['Sun', 'Sunday'], | ||||
|   ['Mon', 'Monday'], | ||||
|   ['Tue', 'Tuesday'], | ||||
|   ['Wed', 'Wednesday'], | ||||
|   ['Thu', 'Thursday'], | ||||
|   ['Fri', 'Friday'], | ||||
|   ['Sat', 'Saturday'] | ||||
| ]; | ||||
| 
 | ||||
| ``` | ||||
| 
 | ||||
| `mmm` shows short month, `mmmm` shows long month, and `mmmmm` shows one char: | ||||
| 
 | ||||
| ``` | ||||
| var months = [ | ||||
|   ['J', 'Jan', 'January'], | ||||
|   ['F', 'Feb', 'February'], | ||||
|   ['M', 'Mar', 'March'], | ||||
|   ['A', 'Apr', 'April'], | ||||
|   ['M', 'May', 'May'], | ||||
|   ['J', 'Jun', 'June'], | ||||
|   ['J', 'Jul', 'July'], | ||||
|   ['A', 'Aug', 'August'], | ||||
|   ['S', 'Sep', 'September'], | ||||
|   ['O', 'Oct', 'October'], | ||||
|   ['N', 'Nov', 'November'], | ||||
|   ['D', 'Dec', 'December'] | ||||
| ]; | ||||
| ``` | ||||
| 
 | ||||
| ## Parsing Date and Time Codes | ||||
| 
 | ||||
| Most spreadsheet formats store dates and times as floating point numbers (where | ||||
| the integer part is a day code based on a format and the fractional part is the | ||||
| portion of a 24 hour day). | ||||
| 
 | ||||
| 
 | ||||
| ```js>tmp/date.js | ||||
| var parse_date_code = function parse_date_code(v,opts) { | ||||
|   var date = Math.floor(v), time = Math.round(86400 * (v - date)), dow=0; | ||||
|   var dout=[], out={D:date, T:time}; fixopts(opts = (opts||{})); | ||||
| ``` | ||||
| 
 | ||||
| Excel help actually recommends treating the 1904 date codes as 1900 date codes | ||||
| shifted by 1462 days. | ||||
| 
 | ||||
| ``` | ||||
|   if(opts.date1904) date += 1462; | ||||
| ``` | ||||
| 
 | ||||
| Due to a bug in Lotus 1-2-3 which was propagated by Excel and other variants, | ||||
| the year 1900 is recognized as a leap year.  JS has no way of representing that | ||||
| abomination as a `Date`, so the easiest way is to store the data as a tuple. | ||||
| 
 | ||||
| February 29, 1900 (date `60`) is recognized as a Wednesday. | ||||
| 
 | ||||
| ``` | ||||
|   if(date === 60) {dout = [1900,2,29]; dow=3;} | ||||
| ``` | ||||
| 
 | ||||
| For the other dates, using the JS date mechanism suffices. | ||||
| 
 | ||||
| ``` | ||||
|   else { | ||||
|     if(date > 60) --date; | ||||
|     /* 1 = Jan 1 1900 */ | ||||
|     var d = new Date(1900,0,1); | ||||
|     d.setDate(d.getDate() + date - 1); | ||||
|     dout = [d.getFullYear(), d.getMonth()+1,d.getDate()]; | ||||
|     dow = d.getDay(); | ||||
| ``` | ||||
| 
 | ||||
| Note that Excel opted to keep the day-of-week metric consistent with the extra | ||||
| day.  In practice, that means the days before the fake leap day are off.  For | ||||
| example, date code `55` is "Friday, February 24, 1900" when in fact it was a | ||||
| 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; | ||||
|   } | ||||
| ``` | ||||
| 
 | ||||
| Because JS dates cannot represent the bad leap day, this returns an object: | ||||
| 
 | ||||
| ``` | ||||
|   out.y = dout[0]; out.m = dout[1]; out.d = dout[2]; | ||||
|   out.S = time % 60; time = Math.floor(time / 60); | ||||
|   out.M = time % 60; time = Math.floor(time / 60); | ||||
|   out.H = time; | ||||
|   out.q = dow; | ||||
|   return out; | ||||
| }; | ||||
| SSF.parse_date_code = parse_date_code; | ||||
| ``` | ||||
| 
 | ||||
| ## Evaluating Format Strings | ||||
| 
 | ||||
| ```js>tmp/main.js | ||||
| function eval_fmt(fmt, v, opts) { | ||||
|   var out = [], o = "", i = 0, c = "", lst='t', q = {}, dt; | ||||
|   fixopts(opts = (opts || {})); | ||||
|   var hr='H'; | ||||
|   /* Tokenize */ | ||||
|   while(i < fmt.length) { | ||||
|     switch((c = fmt[i])) { | ||||
| ``` | ||||
| 
 | ||||
| Text between double-quotes are treated literally, and individual characters are | ||||
| literal if they are preceded by a slash: | ||||
| 
 | ||||
| ``` | ||||
|       case '"': /* Literal text */ | ||||
|         for(o="";fmt[++i] !== '"';) o += fmt[(fmt[i] === '\\' ? ++i : i)]; | ||||
|         out.push({t:'t', v:o}); break; | ||||
|       case '\\': out.push({t:'t', v:fmt[++i]}); ++i; break; | ||||
| ``` | ||||
| 
 | ||||
| The '@' symbol refers to the original text.  The ECMA spec is not complete, but | ||||
| Excel does not allow for '@' and non-literal text to appear in the same format. | ||||
| It seems as if they only support one mode.  (clearly this is a TODO for excel | ||||
| mode but I'm not convinced that's the right approach) | ||||
| 
 | ||||
| ``` | ||||
|       case '@': /* Text Placeholder */ | ||||
|         out.push({t:'T', v:v}); ++i; break; | ||||
| ``` | ||||
| 
 | ||||
| The date codes `m,d,y,h,s` are standard.  There are some special formats like | ||||
| `e` (era year) that have different behaviors in Japanese/Chinese locales. | ||||
| 
 | ||||
| ``` | ||||
|       /* Dates */ | ||||
|       case 'm': case 'd': case 'y': case 'h': case 's': case 'e': | ||||
|         if(!dt) dt = parse_date_code(v, opts); | ||||
|         o = fmt[i]; while(fmt[++i] === c) o+=c; | ||||
|         if(c === 'm' && lst.toLowerCase() === 'h') c = 'M'; /* m = minute */ | ||||
|         if(c === 'h') c = hr; | ||||
|         q={t:c, v:o}; out.push(q); lst = c; break; | ||||
| ``` | ||||
| 
 | ||||
| The (poorly documented) rule regarding `A/P` and `AM/PM` is that if they show up | ||||
| in the format then _all_ instances of `h` are considered 12-hour and not 24-hour | ||||
| format (even in cases like `hh AM/PM hh hh hh`). | ||||
| 
 | ||||
| However, the undocumented `H` and `HH` do appear to reset the `AM/PM` indicator. | ||||
| It is not implemented at the moment because I am not 100% sure of the rules with | ||||
| the HH/hh jazz.  TODO: investigate this further. | ||||
| 
 | ||||
| ``` | ||||
|       case 'A': | ||||
|         if(!dt) dt = parse_date_code(v, opts); | ||||
|         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"; | ||||
|         out.push(q); lst = c; break; | ||||
|       case '[': /* TODO: Fix this -- ignore all conditionals and formatting */ | ||||
|         while(fmt[i++] !== ']'); break; | ||||
|       default: | ||||
|         if("$-+/():!^&'~{}<>= ".indexOf(c) === -1) | ||||
|           throw 'unrecognized character ' + fmt[i] + ' in ' + fmt; | ||||
|         out.push({t:'t', v:c}); ++i; break; | ||||
|     } | ||||
|   } | ||||
|   /* walk backwards */ | ||||
|   for(i=out.length-1, lst='t'; i >= 0; --i) { | ||||
|     switch(out[i].t) { | ||||
|       case 'h': case 'H': out[i].t = hr; lst='h'; break; | ||||
|       case 'd': case 'y': case 's': case 'M': case 'e': lst=out[i].t; break; | ||||
|       case 'm': if(lst === 's') out[i].t = 'M'; break; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /* replace fields */ | ||||
|   for(i=0; i < out.length; ++i) { | ||||
|     switch(out[i].t) { | ||||
|       case 't': case 'T': break; | ||||
|       case 'd': case 'm': case 'y': case 'h': case 'H': case 'M': case 's': case 'A': case 'e': | ||||
|         out[i].v = write_date(out[i].t, out[i].v, dt); | ||||
|         out[i].t = 't'; break; | ||||
|       default: throw "unrecognized type " + out[i].t; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return out.map(function(x){return x.v;}).join(""); | ||||
| } | ||||
| SSF._eval = eval_fmt; | ||||
| ``` | ||||
| 
 | ||||
| 
 | ||||
| There is some overloading of the `m` character.  According to the spec: | ||||
| 
 | ||||
| > If "m" or "mm" code is used immediately after the "h" or "hh" code (for | ||||
| hours) or immediately before the "ss" code (for seconds), the application shall | ||||
| display minutes instead of the month. | ||||
| 
 | ||||
| 
 | ||||
| ```js>tmp/date.js | ||||
| var write_date = function(type, fmt, val) { | ||||
|   switch(type) { | ||||
|     case 'y': switch(fmt) { /* year */ | ||||
|       case 'y': case 'yy': return pad(val.y % 100,2); | ||||
|       default: return val.y; | ||||
|     } break; | ||||
|     case 'm': switch(fmt) { /* month */ | ||||
|       case 'm': return val.m; | ||||
|       case 'mm': return pad(val.m,2); | ||||
|       case 'mmm': return months[val.m-1][1]; | ||||
|       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 'ss': return pad(val.S, 2); | ||||
|       default: throw 'bad second format: ' + fmt; | ||||
|     } break; | ||||
| ``` | ||||
| 
 | ||||
| The `e` format behavior in excel diverges from the spec.  It claims that `ee` | ||||
| should be a two-digit year, but `ee` in excel is actually the four-digit year: | ||||
| 
 | ||||
| ``` | ||||
|     /* 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; | ||||
|   } | ||||
| }; | ||||
| ``` | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| ```js>tmp/main.js | ||||
| function choose_fmt(fmt, v) { | ||||
|   if(typeof fmt === "string") fmt = split_fmt(fmt); | ||||
|   if(typeof v !== "number") return fmt[3]; | ||||
|   return v > 0 ? fmt[0] : v < 0 ? fmt[1] : fmt[2]; | ||||
| } | ||||
| 
 | ||||
| var format = function format(fmt,v,o) { | ||||
|   fixopts(o = (o||{})); | ||||
|   if(fmt === 0) return general_fmt(v, o); | ||||
|   if(typeof fmt === 'number') fmt = table_fmt[fmt]; | ||||
|   var f = choose_fmt(fmt, v, o); | ||||
|   return eval_fmt(f, v, o); | ||||
| }; | ||||
| 
 | ||||
| ``` | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| ```js>tmp/main.js | ||||
| 
 | ||||
| SSF._choose = choose_fmt; | ||||
| SSF._table = table_fmt; | ||||
| SSF.load = function(fmt, idx) { table_fmt[idx] = fmt; }; | ||||
| SSF.format = format; | ||||
| ``` | ||||
| 
 | ||||
| ## JS Boilerplate | ||||
| 
 | ||||
| ```js>tmp/00_header.js | ||||
| var SSF; | ||||
| (function(SSF){ | ||||
| String.prototype.reverse=function(){return this.split("").reverse().join("");}; | ||||
| var _strrev = function(x) { return String(x).reverse(); }; | ||||
| function fill(c,l) { return new Array(l+1).join(c); } | ||||
| function pad(v,d){var t=String(v);return t.length>=d?t:(fill(0,d-t.length)+t);} | ||||
| ``` | ||||
| 
 | ||||
| ```js>tmp/zz_footer_n.js | ||||
| })(typeof exports !== 'undefined' ? exports : SSF); | ||||
| ``` | ||||
| 
 | ||||
| ```js>tmp/zz_footer.js | ||||
| })(SSF); | ||||
| ``` | ||||
| 
 | ||||
| ## .vocrc and post-commands | ||||
| 
 | ||||
| ```bash>tmp/post.sh | ||||
| #!/bin/bash | ||||
| npm install | ||||
| cat tmp/{00_header,opts,consts,general,date,main,zz_footer_n}.js > ssf_node.js | ||||
| cat tmp/{00_header,opts,consts,general,date,main,zz_footer}.js > ssf.js | ||||
| ``` | ||||
| 
 | ||||
| ```json>.vocrc | ||||
| { | ||||
|   "post": "bash tmp/post.sh" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ```>.gitignore | ||||
| .gitignore | ||||
| tmp/ | ||||
| node_modules/ | ||||
| .vocrc | ||||
| ``` | ||||
| 
 | ||||
| ```json>package.json | ||||
| { | ||||
|   "name": "ssf", | ||||
|   "version": "0.1.0", | ||||
|   "author": "SheetJS", | ||||
|   "description": "pure-JS library to format data using ECMA-376 spreadsheet Format Codes", | ||||
|   "keywords": [ "format", "sprintf", "spreadsheet" ], | ||||
|   "main": "ssf_node.js", | ||||
|   "dependencies": { | ||||
|     "voc":"", | ||||
|     "colors":"" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "mocha":"" | ||||
|   }, | ||||
|   "repository": { "type":"git", "url":"git://github.com/SheetJS/ssf.git" }, | ||||
|   "scripts": { | ||||
|     "test": "mocha -R spec" | ||||
|   }, | ||||
|   "bugs": { "url": "https://github.com/SheetJS/ssf/issues" }, | ||||
|   "license": "Apache-2.0", | ||||
|   "engines": { "node": ">=0.8" } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| # Test Driver | ||||
| 
 | ||||
| Travis CI is used for node testing: | ||||
| 
 | ||||
| ```>.travis.yml | ||||
| language: node_js | ||||
| node_js: | ||||
|   - "0.10" | ||||
|   - "0.8" | ||||
| before_install: | ||||
|   - "npm install -g mocha" | ||||
| ``` | ||||
| 
 | ||||
| The mocha test driver tests the implied formats: | ||||
| 
 | ||||
| ```js>test/implied.js | ||||
| /* vim: set ts=2: */ | ||||
| var SSF = require('../'); | ||||
| var fs = require('fs'), assert = require('assert'); | ||||
| var data = JSON.parse(fs.readFileSync('./test/implied.json','utf8')); | ||||
| describe('implied formats', function() { | ||||
|   data.forEach(function(d) { | ||||
|     it(d[1]+" for "+d[0], (d[1]<14||d[1]>22)?null:function(){ | ||||
|       assert.equal(SSF.format(d[1], d[0], {}), d[2]); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| The old test driver was manual: | ||||
| 
 | ||||
| ```js>tmp/test.njs | ||||
| var SSF = require('../ssf_node'); | ||||
| var x = 'd\\-mmm\\-yy\\ yyyy\\ dd\\ \\;\\ yy\\ mm\\ dd'; | ||||
| var y = 'd\\-mmm\\-yy\\ yyyy\\ dd\\ ;\\ yy\\ mm\\ dd'; | ||||
| var z = 'd\\ dd\\ ddd\\ dddd\\ m\\ mm\\ mmm\\ mmmm\\ mmmmm\\ yy\\ yyyy'; | ||||
| console.error(SSF.parse_date_code(65.9)); | ||||
| console.error(SSF.format(x, 65.9)); | ||||
| console.error(SSF.format(y, 65.9)); | ||||
| console.error() | ||||
| console.error(SSF.format(z, 55.9)); | ||||
| console.error(SSF.format(z, 55.9, {mode:"excel"})); | ||||
| console.error(SSF.format(z, 55.9)); | ||||
| console.error() | ||||
| console.error(SSF.format(z, 65.9)); | ||||
| console.error(SSF.format(z, 65.9, {mode:"excel"})); | ||||
| console.error(SSF.format(z, 65.9)); | ||||
| console.error() | ||||
| console.error(SSF.format(19, 65.9)); | ||||
| console.error(SSF.format(20, 65.9)); | ||||
| ``` | ||||
| 
 | ||||
| # LICENSE | ||||
| 
 | ||||
| ```>LICENSE | ||||
| Copyright 2013   SheetJS | ||||
| 
 | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
| 
 | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
| 
 | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| ``` | ||||
							
								
								
									
										231
									
								
								ssf_node.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										231
									
								
								ssf_node.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,231 @@ | ||||
| var SSF; | ||||
| (function(SSF){ | ||||
| String.prototype.reverse=function(){return this.split("").reverse().join("");}; | ||||
| var _strrev = function(x) { return String(x).reverse(); }; | ||||
| function fill(c,l) { return new Array(l+1).join(c); } | ||||
| function pad(v,d){var t=String(v);return t.length>=d?t:(fill(0,d-t.length)+t);} | ||||
| /* Options */ | ||||
| var opts_fmt = {}; | ||||
| function fixopts(o){for(var y in opts_fmt) if(o[y]===undefined) o[y]=opts_fmt[y];} | ||||
| SSF.opts = opts_fmt; | ||||
| opts_fmt.date1904 = 0; | ||||
| opts_fmt.output = ""; | ||||
| opts_fmt.mode = ""; | ||||
| var table_fmt = { | ||||
|   1:  '0', | ||||
|   2:  '0.00', | ||||
|   3:  '#,##0', | ||||
|   4:  '#,##0.00', | ||||
|   9:  '0%', | ||||
|   10: '0.00%', | ||||
|   11: '0.00E+00', | ||||
|   12: '# ?/?', | ||||
|   13: '# ??/??', | ||||
|   14: 'mm/dd/yy', | ||||
|   15: 'd-mmm-yy', | ||||
|   16: 'd-mmm', | ||||
|   17: 'mmm-yy', | ||||
|   18: 'h:mm AM/PM', | ||||
|   19: 'h:mm:ss AM/PM', | ||||
|   20: 'h:mm', | ||||
|   21: 'h:mm:ss', | ||||
|   22: 'm/d/yy h:mm', | ||||
|   37: '#,##0 ;(#,##0)', | ||||
|   38: '#,##0 ;[Red](#,##0)', | ||||
|   39: '#,##0.00;(#,##0.00)', | ||||
|   40: '#,##0.00;[Red](#,##0.00)', | ||||
|   45: 'mm:ss', | ||||
|   46: '[h]:mm:ss', | ||||
|   47: 'mmss.0', | ||||
|   48: '##0.0E+0', | ||||
|   49: '@' | ||||
| }; | ||||
| var days = [ | ||||
|   ['Sun', 'Sunday'], | ||||
|   ['Mon', 'Monday'], | ||||
|   ['Tue', 'Tuesday'], | ||||
|   ['Wed', 'Wednesday'], | ||||
|   ['Thu', 'Thursday'], | ||||
|   ['Fri', 'Friday'], | ||||
|   ['Sat', 'Saturday'] | ||||
| ]; | ||||
| var months = [ | ||||
|   ['J', 'Jan', 'January'], | ||||
|   ['F', 'Feb', 'February'], | ||||
|   ['M', 'Mar', 'March'], | ||||
|   ['A', 'Apr', 'April'], | ||||
|   ['M', 'May', 'May'], | ||||
|   ['J', 'Jun', 'June'], | ||||
|   ['J', 'Jul', 'July'], | ||||
|   ['A', 'Aug', 'August'], | ||||
|   ['S', 'Sep', 'September'], | ||||
|   ['O', 'Oct', 'October'], | ||||
|   ['N', 'Nov', 'November'], | ||||
|   ['D', 'Dec', 'December'] | ||||
| ]; | ||||
| var general_fmt = function(v) { | ||||
|   if(typeof v === 'boolean') return v ? "TRUE" : "FALSE"; | ||||
| }; | ||||
| SSF._general = general_fmt; | ||||
| var parse_date_code = function parse_date_code(v,opts) { | ||||
|   var date = Math.floor(v), time = Math.round(86400 * (v - date)), dow=0; | ||||
|   var dout=[], out={D:date, T:time}; fixopts(opts = (opts||{})); | ||||
|   if(opts.date1904) date += 1462; | ||||
|   if(date === 60) {dout = [1900,2,29]; dow=3;} | ||||
|   else { | ||||
|     if(date > 60) --date; | ||||
|     /* 1 = Jan 1 1900 */ | ||||
|     var d = new Date(1900,0,1); | ||||
|     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; | ||||
|   } | ||||
|   out.y = dout[0]; out.m = dout[1]; out.d = dout[2]; | ||||
|   out.S = time % 60; time = Math.floor(time / 60); | ||||
|   out.M = time % 60; time = Math.floor(time / 60); | ||||
|   out.H = time; | ||||
|   out.q = dow; | ||||
|   return out; | ||||
| }; | ||||
| SSF.parse_date_code = parse_date_code; | ||||
| var write_date = function(type, fmt, val) { | ||||
|   switch(type) { | ||||
|     case 'y': switch(fmt) { /* year */ | ||||
|       case 'y': case 'yy': return pad(val.y % 100,2); | ||||
|       default: return val.y; | ||||
|     } break; | ||||
|     case 'm': switch(fmt) { /* month */ | ||||
|       case 'm': return val.m; | ||||
|       case 'mm': return pad(val.m,2); | ||||
|       case 'mmm': return months[val.m-1][1]; | ||||
|       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 'ss': return pad(val.S, 2); | ||||
|       default: throw 'bad second format: ' + fmt; | ||||
|     } break; | ||||
|     /* 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; | ||||
|   } | ||||
| }; | ||||
| function split_fmt(fmt) { | ||||
|   var out = []; | ||||
|   var in_str = -1; | ||||
|   for(var i = 0, j = 0; i < fmt.length; ++i) { | ||||
|     if(in_str != -1) { if(fmt[i] == '"') in_str = -1; continue; } | ||||
|     if(fmt[i] == "_" || fmt[i] == "*" || fmt[i] == "\\") { ++i; continue; } | ||||
|     if(fmt[i] == '"') { in_str = i; continue; } | ||||
|     if(fmt[i] != ";") continue; | ||||
|     out.push(fmt.slice(j,i)); | ||||
|     j = i+1; | ||||
|   } | ||||
|   out.push(fmt.slice(j)); | ||||
|   if(in_str !=-1) throw "Format |" + fmt + "| unterminated string at " + in_str; | ||||
|   return out; | ||||
| } | ||||
| SSF._split = split_fmt; | ||||
| function eval_fmt(fmt, v, opts) { | ||||
|   var out = [], o = "", i = 0, c = "", lst='t', q = {}, dt; | ||||
|   fixopts(opts = (opts || {})); | ||||
|   var hr='H'; | ||||
|   /* Tokenize */ | ||||
|   while(i < fmt.length) { | ||||
|     switch((c = fmt[i])) { | ||||
|       case '"': /* Literal text */ | ||||
|         for(o="";fmt[++i] !== '"';) o += fmt[(fmt[i] === '\\' ? ++i : i)]; | ||||
|         out.push({t:'t', v:o}); break; | ||||
|       case '\\': out.push({t:'t', v:fmt[++i]}); ++i; break; | ||||
|       case '@': /* Text Placeholder */ | ||||
|         out.push({t:'T', v:v}); ++i; break; | ||||
|       /* Dates */ | ||||
|       case 'm': case 'd': case 'y': case 'h': case 's': case 'e': | ||||
|         if(!dt) dt = parse_date_code(v, opts); | ||||
|         o = fmt[i]; while(fmt[++i] === c) o+=c; | ||||
|         if(c === 'm' && lst.toLowerCase() === 'h') c = 'M'; /* m = minute */ | ||||
|         if(c === 'h') c = hr; | ||||
|         q={t:c, v:o}; out.push(q); lst = c; break; | ||||
|       case 'A': | ||||
|         if(!dt) dt = parse_date_code(v, opts); | ||||
|         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"; | ||||
|         out.push(q); lst = c; break; | ||||
|       case '[': /* TODO: Fix this -- ignore all conditionals and formatting */ | ||||
|         while(fmt[i++] !== ']'); break; | ||||
|       default: | ||||
|         if("$-+/():!^&'~{}<>= ".indexOf(c) === -1) | ||||
|           throw 'unrecognized character ' + fmt[i] + ' in ' + fmt; | ||||
|         out.push({t:'t', v:c}); ++i; break; | ||||
|     } | ||||
|   } | ||||
|   /* walk backwards */ | ||||
|   for(i=out.length-1, lst='t'; i >= 0; --i) { | ||||
|     switch(out[i].t) { | ||||
|       case 'h': case 'H': out[i].t = hr; lst='h'; break; | ||||
|       case 'd': case 'y': case 's': case 'M': case 'e': lst=out[i].t; break; | ||||
|       case 'm': if(lst === 's') out[i].t = 'M'; break; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /* replace fields */ | ||||
|   for(i=0; i < out.length; ++i) { | ||||
|     switch(out[i].t) { | ||||
|       case 't': case 'T': break; | ||||
|       case 'd': case 'm': case 'y': case 'h': case 'H': case 'M': case 's': case 'A': case 'e': | ||||
|         out[i].v = write_date(out[i].t, out[i].v, dt); | ||||
|         out[i].t = 't'; break; | ||||
|       default: throw "unrecognized type " + out[i].t; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return out.map(function(x){return x.v;}).join(""); | ||||
| } | ||||
| SSF._eval = eval_fmt; | ||||
| function choose_fmt(fmt, v) { | ||||
|   if(typeof fmt === "string") fmt = split_fmt(fmt); | ||||
|   if(typeof v !== "number") return fmt[3]; | ||||
|   return v > 0 ? fmt[0] : v < 0 ? fmt[1] : fmt[2]; | ||||
| } | ||||
| 
 | ||||
| var format = function format(fmt,v,o) { | ||||
|   fixopts(o = (o||{})); | ||||
|   if(fmt === 0) return general_fmt(v, o); | ||||
|   if(typeof fmt === 'number') fmt = table_fmt[fmt]; | ||||
|   var f = choose_fmt(fmt, v, o); | ||||
|   return eval_fmt(f, v, o); | ||||
| }; | ||||
| 
 | ||||
| SSF._choose = choose_fmt; | ||||
| SSF._table = table_fmt; | ||||
| SSF.load = function(fmt, idx) { table_fmt[idx] = fmt; }; | ||||
| SSF.format = format; | ||||
| })(typeof exports !== 'undefined' ? exports : SSF); | ||||
							
								
								
									
										11
									
								
								test/implied.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										11
									
								
								test/implied.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| /* vim: set ts=2: */ | ||||
| var SSF = require('../'); | ||||
| var fs = require('fs'), assert = require('assert'); | ||||
| var data = JSON.parse(fs.readFileSync('./test/implied.json','utf8')); | ||||
| describe('implied formats', function() { | ||||
|   data.forEach(function(d) { | ||||
|     it(d[1]+" for "+d[0], (d[1]<14||d[1]>22)?null:function(){ | ||||
|       assert.equal(SSF.format(d[1], d[0], {}), d[2]); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										30
									
								
								test/implied.json
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										30
									
								
								test/implied.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| [ | ||||
|   [12345.6789, 0, "12345.6789"], | ||||
|   [12345.6789, 1, "12346"], | ||||
|   [12345.6789, 2, "12345.68"], | ||||
|   [12345.6789, 3, "12,346"], | ||||
|   [12345.6789, 4, "12,345.68"], | ||||
|   [12345.6789, 9, "1234568%"], | ||||
|   [12345.6789, 10, "1234567.89%"], | ||||
|   [12345.6789, 11, "1.23E+04"], | ||||
|   [12345.6789, 12, "12345 2/3"], | ||||
|   [12345.6789, 13, "12345 55/81"], | ||||
|   [12345.6789, 14, "10/18/33"], | ||||
|   [12345.6789, 15, "18-Oct-33"], | ||||
|   [12345.6789, 16, "18-Oct"], | ||||
|   [12345.6789, 17, "Oct-33"], | ||||
|   [12345.6789, 18, "4:17 PM"], | ||||
|   [12345.6789, 19, "4:17:37 PM"], | ||||
|   [12345.6789, 20, "16:17"], | ||||
|   [12345.6789, 21, "16:17:37"], | ||||
|   [12345.6789, 22, "10/18/33 16:17"], | ||||
|   [12345.6789, 37, "12,346"], | ||||
|   [12345.6789, 38, "12,346"], | ||||
|   [12345.6789, 39, "12,345.68"], | ||||
|   [12345.6789, 40, "12,345.68"], | ||||
|   [12345.6789, 45, "17:37"], | ||||
|   [12345.6789, 46, "296296:17:37"], | ||||
|   [12345.6789, 47, "1737.0"], | ||||
|   [12345.6789, 48, "12.3E+3"], | ||||
|   [12345.6789, 49, "12345.6789"] | ||||
| ] | ||||
							
								
								
									
										40
									
								
								test/implied.njs
									
									
									
									
									
										Executable file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										40
									
								
								test/implied.njs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,40 @@ | ||||
| #!/usr/bin/env node | ||||
| var ssf = require("../"); | ||||
| var val = 12345.6789; | ||||
| console.log(val); | ||||
| [ | ||||
| 	"0", // 1 | ||||
| 	"0.00", // 2 | ||||
| 	"#,##0", // 3 | ||||
| 	"#,##0.00", // 4 | ||||
| 	"0%", // 9 | ||||
| 	"0.00%", // 10 | ||||
| 	"0.00E+00", // 11 | ||||
| 	"# ?/?", // 12 | ||||
| 	"# ??/??", // 13 | ||||
| 	"m/d/yy", // 14 | ||||
| 	"d-mmm-yy", // 15 | ||||
| 	"d-mmm", // 16 | ||||
| 	"mmm-yy", // 17 | ||||
| 	"h:mm AM/PM", // 18 | ||||
| 	"h:mm:ss AM/PM", // 19  | ||||
| 	"h:mm", // 20 | ||||
| 	"h:mm:ss", // 21 | ||||
| 	"m/d/yy h:mm", // 22 | ||||
| 	"#,##0 ;(#,##0)", // 37 | ||||
| 	"#,##0 ;[Red](#,##0)", // 38 | ||||
| 	"#,##0.00;(#,##0.00)", // 39 | ||||
| 	"#,##0.00;[Red](#,##0.00)", // 40 | ||||
| 	"mm:ss", // 45 | ||||
| 	"[h]:mm:ss", // 46 | ||||
| 	"mmss.0", // 47 | ||||
| 	"##0.0E+0", // 48 | ||||
| 	"@", // 49 | ||||
| 
 | ||||
| 	"General" // 0 | ||||
| ].forEach(function(x) { | ||||
| 	try { | ||||
| 		console.log(x + "|" + ssf.format(x,val,{})); | ||||
| 	} catch (e) { } | ||||
| 	 | ||||
| }); | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user