diff --git a/LICENSE b/LICENSE
index 06704f7..6d41660 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright 2013   SheetJS
+Copyright (C) 2013-2014   SheetJS
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index a42db69..98ef786 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,10 @@ In node:
 
     var SSF = require('ssf');
 
+The script will manipulate `module.exports` if available (e.g. in a CommonJS 
+`require` context).  This is not always desirable.  To prevent the behavior, 
+define `DO_NOT_EXPORT_SSF`:
+
 ## Usage
 
 `.load(fmt, idx)` sets custom formats (generally indices above `164`)
@@ -25,6 +29,13 @@ In node:
 `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:
+
+- `D,T`: Date (`[val]`) Time (`{val}`)
+- `y,m,d`: Year, Month, Day
+- `H,M,S,u`: (0-23)Hour, Minute, Second, Sub-second
+- `q`: Day of Week (0=Sunday, 1=Monday, ..., 5=Friday, 6=Saturday)
+
 ## Notes
 
 Format code 14 in the spec is broken; the correct format is 'mm/dd/yy' (dashes,
@@ -33,3 +44,10 @@ not spaces)
 ## License
 
 Apache 2.0
+
+## Tests
+
+[![Build Status](https://travis-ci.org/SheetJS/frac.png?branch=master)](https://travis-ci.org/SheetJS/frac)
+
+[![githalytics.com alpha](https://cruel-carlota.pagodabox.com/c1dac903f4b43f82a529bc8df145d085 "githalytics.com")](http://githalytics.com/SheetJS/ssf)
+
diff --git a/bin/ssf.njs b/bin/ssf.njs
new file mode 100755
index 0000000..4329b71
--- /dev/null
+++ b/bin/ssf.njs
@@ -0,0 +1,10 @@
+#!/usr/bin/env node
+/* ssf.js (C) 2013-2014 SheetJS -- http://sheetjs.com */
+var SSF = require('../');
+var argv = process.argv.slice(2);
+if(argv.length < 2 || argv[0] == "-h" || argv[0] == "--help") {
+	console.error("usage: ssf <format> <value>");
+	console.error("output: format_as_string|format_as_number");
+	process.exit(0);
+}
+console.log(SSF.format(argv[0],argv[1]) + "|" + SSF.format(argv[0],+(argv[1])));
diff --git a/package.json b/package.json
index c9a722f..498b94b 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,10 @@
 {
   "name": "ssf",
-  "version": "0.4.1",
+  "version": "0.5.0",
   "author": "SheetJS",
   "description": "pure-JS library to format data using ECMA-376 spreadsheet Format Codes",
   "keywords": [ "format", "sprintf", "spreadsheet" ],
-  "main": "ssf_node.js",
+  "main": "ssf.js",
   "dependencies": {
     "voc":"",
     "colors":""
@@ -16,6 +16,9 @@
   "scripts": {
     "test": "mocha -R spec"
   },
+  "bin": {
+    "ssf": "./bin/ssf.njs"
+  },
   "bugs": { "url": "https://github.com/SheetJS/ssf/issues" },
   "license": "Apache-2.0",
   "engines": { "node": ">=0.8" }
diff --git a/ssf.js b/ssf.js
index d01bd5f..43299de 100644
--- a/ssf.js
+++ b/ssf.js
@@ -1,4 +1,4 @@
-/* ssf.js (C) 2013 SheetJS -- http://sheetjs.com */
+/* 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("");};
@@ -66,25 +66,25 @@ var months = [
   ['D', 'Dec', 'December']
 ];
 var frac = function frac(x, D, mixed) {
-    var sgn = x < 0 ? -1 : 1;
-    var B = x * sgn;
-    var P_2 = 0, P_1 = 1, P = 0;
-    var Q_2 = 1, Q_1 = 0, Q = 0;
-    var A = B|0;
-    while(Q_1 < D) {
-        A = B|0;
-        P = A * P_1 + P_2;
-        Q = A * Q_1 + Q_2;
-        if((B - A) < 0.0000000001) break;
-        B = 1 / (B - A);
-        P_2 = P_1; P_1 = P;
-        Q_2 = Q_1; Q_1 = Q;
-    }
-    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];
-    var q = Math.floor(sgn * P/Q);
-    return [q, sgn*P - q*Q, Q];
+  var sgn = x < 0 ? -1 : 1;
+  var B = x * sgn;
+  var P_2 = 0, P_1 = 1, P = 0;
+  var Q_2 = 1, Q_1 = 0, Q = 0;
+  var A = B|0;
+  while(Q_1 < D) {
+    A = B|0;
+    P = A * P_1 + P_2;
+    Q = A * Q_1 + Q_2;
+    if((B - A) < 0.0000000005) break;
+    B = 1 / (B - A);
+    P_2 = P_1; P_1 = P;
+    Q_2 = Q_1; Q_1 = Q;
+  }
+  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];
+  var q = Math.floor(sgn * P/Q);
+  return [q, sgn*P - q*Q, Q];
 };
 var general_fmt = function(v) {
   if(typeof v === 'boolean') return v ? "TRUE" : "FALSE";
@@ -338,7 +338,7 @@ function eval_fmt(fmt, v, opts, flen) {
 }
 SSF._eval = eval_fmt;
 function choose_fmt(fmt, v, o) {
-  if(typeof fmt === 'number') fmt = table_fmt[fmt];
+  if(typeof fmt === 'number') fmt = ((o&&o.table) ? o.table : table_fmt)[fmt];
   if(typeof fmt === "string") fmt = split_fmt(fmt);
   var l = fmt.length;
   switch(fmt.length) {
@@ -353,7 +353,7 @@ 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 === 'number') fmt = table_fmt[fmt];
+  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);
   return eval_fmt(f[1], v, o, f[0]);
@@ -363,5 +363,8 @@ SSF._choose = choose_fmt;
 SSF._table = table_fmt;
 SSF.load = function(fmt, idx) { table_fmt[idx] = fmt; };
 SSF.format = format;
+SSF.get_table = function() { return table_fmt; };
+SSF.load_table = function(tbl) { for(var i=0; i!=0x0188; ++i) if(table_fmt[i]) SSF.load(i, table_fmt[i]); };
 };
 make_ssf(SSF);
+if(typeof module !== 'undefined' && typeof DO_NOT_EXPORT_SSF === 'undefined') module.exports = SSF;
diff --git a/ssf.md b/ssf.md
index 54eb58b..a52738d 100644
--- a/ssf.md
+++ b/ssf.md
@@ -8,7 +8,7 @@ spreadsheet format codes.
 The various API functions take an `opts` argument which control parsing.  The
 default options are described below:
 
-```js>tmp/opts.js
+```js>tmp/10_opts.js
 /* Options */
 var opts_fmt = {};
 function fixopts(o){for(var y in opts_fmt) if(o[y]===undefined) o[y]=opts_fmt[y];}
@@ -63,7 +63,7 @@ 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
+```js>tmp/90_main.js
 function split_fmt(fmt) {
   var out = [];
   var in_str = -1;
@@ -124,7 +124,7 @@ 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
+```js>tmp/40_general.js
 var general_fmt = function(v) {
 ```
 
@@ -178,7 +178,7 @@ SSF._general = general_fmt;
 These are the commonly-used formats that have a special implied code.
 None of the international formats are included here.
 
-```js>tmp/consts.js
+```js>tmp/20_consts.js
 var table_fmt = {
   1:  '0',
   2:  '0.00',
@@ -221,7 +221,7 @@ of the applications makes sense here:
 
 The code `ddd` displays short day-of-week and `dddd` shows long day-of-week:
 
-```js>tmp/consts.js
+```js>tmp/20_consts.js
 var days = [
   ['Sun', 'Sunday'],
   ['Mon', 'Monday'],
@@ -260,7 +260,7 @@ 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
+```js>tmp/50_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, u:86400*(v-date)-time}; fixopts(opts = (opts||{}));
@@ -323,7 +323,7 @@ SSF.parse_date_code = parse_date_code;
 
 ## Evaluating Number Formats
 
-```js>tmp/number.js
+```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 write_num = function(type, fmt, val) {
@@ -331,7 +331,7 @@ var write_num = function(type, fmt, val) {
 
 For parentheses, explicitly resolve the sign issue:
 
-```js>tmp/number.js
+```js>tmp/60_number.js
   if(type === '(') {
     var ffmt = fmt.replace(/\( */,"").replace(/ \)/,"").replace(/\)/,"");
     if(val >= 0) return write_num('n', ffmt, val);
@@ -342,7 +342,7 @@ For parentheses, explicitly resolve the sign issue:
 
 Percentage values should be physically shifted:
 
-```js>tmp/number.js
+```js>tmp/60_number.js
   var mul = 0, o;
   fmt = fmt.replace(/%/g,function(x) { mul++; return ""; });
   if(mul !== 0) return write_num(type, fmt, val * Math.pow(10,2*mul)) + fill("%",mul);
@@ -376,7 +376,7 @@ TODO: localize the currency:
 
 Fractions with known denominator are resolved by rounding:
 
-```js>tmp/number.js
+```js>tmp/60_number.js
   var r, ff, aval = val < 0 ? -val : val, sign = val < 0 ? "-" : "";
   if((r = fmt.match(/# (\?+) \/ (\d+)/))) {
     var den = Number(r[2]), rnd = Math.round(aval * den), base = Math.floor(rnd/den);
@@ -387,7 +387,7 @@ Fractions with known denominator are resolved by rounding:
 
 The default cases are hard-coded.  TODO: actually parse them
 
-```js>tmp/number.js
+```js>tmp/60_number.js
   switch(fmt) {
     case "0": return Math.round(val);
     case "0.0": o = Math.round(val*10);
@@ -403,7 +403,7 @@ The default cases are hard-coded.  TODO: actually parse them
 
 The frac helper function is used for fraction formats (defined below).
 
-```js>tmp/number.js
+```js>tmp/60_number.js
     case "# ? / ?": ff = frac(aval, 9, true); return sign + (ff[0]||"") + " " + (ff[1] === 0 ? "   " : ff[1] + "/" + ff[2]);
     case "# ?? / ??": ff = frac(aval, 99, true); return sign + (ff[0]||"") + " " + (ff[1] ? pad(ff[1],2," ") + "/" + rpad(ff[2],2," ") : "     ");
     case "# ??? / ???": ff = frac(aval, 999, true); return sign + (ff[0]||"") + " " + (ff[1] ? pad(ff[1],3," ") + "/" + rpad(ff[2],3," ") : "       ");
@@ -415,7 +415,7 @@ The frac helper function is used for fraction formats (defined below).
 
 ## Evaluating Format Strings
 
-```js>tmp/main.js
+```js>tmp/90_main.js
 function eval_fmt(fmt, v, opts, flen) {
   var out = [], o = "", i = 0, c = "", lst='t', q = {}, dt;
   fixopts(opts = (opts || {}));
@@ -615,7 +615,7 @@ hours) or immediately before the "ss" code (for seconds), the application shall
 display minutes instead of the month.
 
 
-```js>tmp/date.js
+```js>tmp/50_date.js
 var write_date = function(type, fmt, val) {
   if(val < 0) return "";
   switch(type) {
@@ -685,9 +685,9 @@ should be a two-digit year, but `ee` in excel is actually the four-digit year:
 Based on the value, `choose_fmt` picks the right format string.  If formats have
 explicit negative specifications, those values should be passed as positive:
 
-```js>tmp/main.js
+```js>tmp/90_main.js
 function choose_fmt(fmt, v, o) {
-  if(typeof fmt === 'number') fmt = table_fmt[fmt];
+  if(typeof fmt === 'number') fmt = ((o&&o.table) ? o.table : table_fmt)[fmt];
   if(typeof fmt === "string") fmt = split_fmt(fmt);
   var l = fmt.length;
   switch(fmt.length) {
@@ -712,18 +712,18 @@ 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 === 'number') fmt = table_fmt[fmt];
+  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);
   return eval_fmt(f[1], v, o, f[0]);
 };
 
 ```
 
+The methods beginning with an underscore are subject to change and should not be
+used directly in programs.
 
-
-
-
-```js>tmp/main.js
+```js>tmp/90_main.js
 
 SSF._choose = choose_fmt;
 SSF._table = table_fmt;
@@ -731,38 +731,45 @@ SSF.load = function(fmt, idx) { table_fmt[idx] = fmt; };
 SSF.format = format;
 ```
 
+To support multiple SSF tables:  
+
+```
+SSF.get_table = function() { return table_fmt; };
+SSF.load_table = function(tbl) { for(var i=0; i!=0x0188; ++i) if(table_fmt[i]) SSF.load(i, table_fmt[i]); };
+```
+
 ## Fraction Library
 
 The implementation is from [our frac library](https://github.com/SheetJS/frac/):
 
-```js>tmp/frac.js
+```js>tmp/30_frac.js
 var frac = function frac(x, D, mixed) {
-    var sgn = x < 0 ? -1 : 1;
-    var B = x * sgn;
-    var P_2 = 0, P_1 = 1, P = 0;
-    var Q_2 = 1, Q_1 = 0, Q = 0;
-    var A = B|0;
-    while(Q_1 < D) {
-        A = B|0;
-        P = A * P_1 + P_2;
-        Q = A * Q_1 + Q_2;
-        if((B - A) < 0.0000000001) break;
-        B = 1 / (B - A);
-        P_2 = P_1; P_1 = P;
-        Q_2 = Q_1; Q_1 = Q;
-    }
-    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];
-    var q = Math.floor(sgn * P/Q);
-    return [q, sgn*P - q*Q, Q];
+  var sgn = x < 0 ? -1 : 1;
+  var B = x * sgn;
+  var P_2 = 0, P_1 = 1, P = 0;
+  var Q_2 = 1, Q_1 = 0, Q = 0;
+  var A = B|0;
+  while(Q_1 < D) {
+    A = B|0;
+    P = A * P_1 + P_2;
+    Q = A * Q_1 + Q_2;
+    if((B - A) < 0.0000000005) break;
+    B = 1 / (B - A);
+    P_2 = P_1; P_1 = P;
+    Q_2 = Q_1; Q_1 = Q;
+  }
+  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];
+  var q = Math.floor(sgn * P/Q);
+  return [q, sgn*P - q*Q, Q];
 };
 ```
 
 ## JS Boilerplate
 
 ```js>tmp/00_header.js
-/* ssf.js (C) 2013 SheetJS -- http://sheetjs.com */
+/* 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("");};
@@ -772,14 +779,10 @@ function pad(v,d,c){var t=String(v);return t.length>=d?t:(fill(c||0,d-t.length)+
 function rpad(v,d,c){var t=String(v);return t.length>=d?t:(t+fill(c||0,d-t.length));}
 ```
 
-```js>tmp/zz_footer_n.js
-};
-make_ssf(typeof exports !== 'undefined' ? exports : SSF);
-```
-
-```js>tmp/zz_footer.js
+```js>tmp/99_footer.js
 };
 make_ssf(SSF);
+if(typeof module !== 'undefined' && typeof DO_NOT_EXPORT_SSF === 'undefined') module.exports = SSF;
 ```
 
 ## .vocrc and post-commands
@@ -787,9 +790,7 @@ make_ssf(SSF);
 ```bash>tmp/post.sh
 #!/bin/bash
 npm install
-cat tmp/{00_header,opts,consts,frac,general,date,number,main,zz_footer_n}.js > ssf_node.js
-cat tmp/{00_header,opts,consts,frac,general,date,number,main,zz_footer}.js > ssf.js
-
+cat tmp/*.js > ssf.js
 ```
 
 ```json>.vocrc
@@ -817,11 +818,11 @@ test:
 ```json>package.json
 {
   "name": "ssf",
-  "version": "0.4.1",
+  "version": "0.5.0",
   "author": "SheetJS",
   "description": "pure-JS library to format data using ECMA-376 spreadsheet Format Codes",
   "keywords": [ "format", "sprintf", "spreadsheet" ],
-  "main": "ssf_node.js",
+  "main": "ssf.js",
   "dependencies": {
     "voc":"",
     "colors":""
@@ -833,6 +834,9 @@ test:
   "scripts": {
     "test": "mocha -R spec"
   },
+  "bin": {
+    "ssf": "./bin/ssf.njs"
+  },
   "bugs": { "url": "https://github.com/SheetJS/ssf/issues" },
   "license": "Apache-2.0",
   "engines": { "node": ">=0.8" }
@@ -905,33 +909,10 @@ describe('fractional formats', function() {
 });
 ```
 
-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
+Copyright (C) 2013-2014   SheetJS
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/ssf_node.js b/ssf_node.js
deleted file mode 100644
index 7a95ef9..0000000
--- a/ssf_node.js
+++ /dev/null
@@ -1,366 +0,0 @@
-/* ssf.js (C) 2013 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(); };
-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));}
-/* 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: 'm/d/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 frac = function frac(x, D, mixed) {
-    var sgn = x < 0 ? -1 : 1;
-    var B = x * sgn;
-    var P_2 = 0, P_1 = 1, P = 0;
-    var Q_2 = 1, Q_1 = 0, Q = 0;
-    var A = B|0;
-    while(Q_1 < D) {
-        A = B|0;
-        P = A * P_1 + P_2;
-        Q = A * Q_1 + Q_2;
-        if((B - A) < 0.0000000001) break;
-        B = 1 / (B - A);
-        P_2 = P_1; P_1 = P;
-        Q_2 = Q_1; Q_1 = Q;
-    }
-    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];
-    var q = Math.floor(sgn * P/Q);
-    return [q, sgn*P - q*Q, Q];
-};
-var general_fmt = function(v) {
-  if(typeof v === 'boolean') return v ? "TRUE" : "FALSE";
-  if(typeof v === 'number') {
-    var o, V = v < 0 ? -v : v;
-    if(V >= 0.1 && V < 1) o = v.toPrecision(9);
-    else if(V >= 0.01 && V < 0.1) o = v.toPrecision(8);
-    else if(V >= 0.001 && V < 0.01) o = v.toPrecision(7);
-    else if(V >= 0.0001 && V < 0.001) o = v.toPrecision(6);
-    else if(V >= Math.pow(10,10) && V < Math.pow(10,11)) o = v.toFixed(10).substr(0,12);
-    else if(V > Math.pow(10,-9) && V < Math.pow(10,11)) {
-      o = v.toFixed(12).replace(/(\.[0-9]*[1-9])0*$/,"$1").replace(/\.$/,""); 
-      if(o.length > 11+(v<0?1:0)) o = v.toPrecision(10);
-      if(o.length > 11+(v<0?1:0)) o = v.toExponential(5);
-    } 
-    else {
-      o = v.toFixed(11).replace(/(\.[0-9]*[1-9])0*$/,"$1");
-      if(o.length > 11 + (v<0?1:0)) o = v.toPrecision(6); 
-    }
-    o = o.replace(/(\.[0-9]*[1-9])0+e/,"$1e").replace(/\.0*e/,"e");
-    return o.replace("e","E").replace(/\.0*$/,"").replace(/\.([0-9]*[^0])0*$/,".$1").replace(/(E[+-])([0-9])$/,"$1"+"0"+"$2");
-  }
-  if(typeof v === 'string') return v;
-  throw "unsupported value in General format: " + v;
-};
-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, u:86400*(v-date)-time}; fixopts(opts = (opts||{}));
-  if(opts.date1904) date += 1462;
-  if(date === 60) {dout = [1900,2,29]; dow=3;}
-  else if(date === 0) {dout = [1900,1,0]; dow=6;}
-  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) {
-  if(val < 0) return "";
-  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);
-      case 'ss.0': return pad(val.S,2) + "." + Math.round(10*val.u);
-      default: throw 'bad second format: ' + fmt;
-    } break;
-    case 'Z': switch(fmt) {
-      case '[h]': return val.D*24+val.H;
-      default: throw 'bad abstime 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;
-  }
-};
-String.prototype.reverse = function() { return this.split("").reverse().join(""); };
-var commaify = function(s) { return s.reverse().replace(/.../g,"$&,").reverse().replace(/^,/,""); };
-var write_num = function(type, fmt, val) {
-  if(type === '(') {
-    var ffmt = fmt.replace(/\( */,"").replace(/ \)/,"").replace(/\)/,"");
-    if(val >= 0) return write_num('n', ffmt, val);
-    return '(' + write_num('n', ffmt, -val) + ')';
-  }
-  var mul = 0, o;
-  fmt = fmt.replace(/%/g,function(x) { mul++; return ""; });
-  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 == '##0.0E+0') {
-      var ee = Number(val.toExponential(0).substr(3))%3;
-      o = (val/Math.pow(10,ee%3)).toPrecision(idx+1+(ee%3)).replace(/^([+-]?)([0-9]*)\.([0-9]*)[Ee]/,function($$,$1,$2,$3) { return $1 + $2 + $3.substr(0,ee) + "." + $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");
-    return o.replace("e","E");
-  }
-  if(fmt[0] === "$") return "$"+write_num(type,fmt.substr(fmt[1]==' '?2:1),val);
-  var r, ff, aval = val < 0 ? -val : val, sign = val < 0 ? "-" : "";
-  if((r = fmt.match(/# (\?+) \/ (\d+)/))) {
-    var den = Number(r[2]), rnd = Math.round(aval * den), base = Math.floor(rnd/den);
-    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));
-  }
-  switch(fmt) {
-    case "0": return Math.round(val);
-    case "0.0": o = Math.round(val*10);
-      return String(o/10).replace(/^([^\.]+)$/,"$1.0").replace(/\.$/,".0");
-    case "0.00": o = Math.round(val*100);
-      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 "#,##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);
-    case "# ? / ?": ff = frac(aval, 9, true); return sign + (ff[0]||"") + " " + (ff[1] === 0 ? "   " : ff[1] + "/" + ff[2]);
-    case "# ?? / ??": ff = frac(aval, 99, true); return sign + (ff[0]||"") + " " + (ff[1] ? pad(ff[1],2," ") + "/" + rpad(ff[2],2," ") : "     ");
-    case "# ??? / ???": ff = frac(aval, 999, true); return sign + (ff[0]||"") + " " + (ff[1] ? pad(ff[1],3," ") + "/" + rpad(ff[2],3," ") : "       ");
-    default:
-  }
-  throw new Error("unsupported format |" + 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, flen) {
-  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] !== '"' && i < fmt.length;) o += fmt[i];
-        out.push({t:'t', v:o}); ++i; break;
-      case '\\': var w = fmt[++i], t = "()".indexOf(w) === -1 ? 't' : w;
-        out.push({t:t, v:w}); ++i; break;
-      case '_': out.push({t:'t', v:" "}); i+=2; 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(v < 0) return "";
-        if(!dt) dt = parse_date_code(v, opts);
-        o = fmt[i]; while(fmt[++i] === 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;
-        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 */
-        o = c;
-        while(fmt[i++] !== ']') o += fmt[i];
-        if(o == "[h]") out.push({t:'Z', v:o});
-        break;
-      /* Numbers */
-      case '0': case '#':
-        o = c; while("0#?.,E+-%".indexOf(c=fmt[++i]) > -1) o += c;
-        out.push({t:'n', v:o}); break;
-      case '?':
-        o = fmt[i]; while(fmt[++i] === c) o+=c;
-        q={t:c, v:o}; out.push(q); lst = c; break;
-      case '*': ++i; if(fmt[i] == ' ') ++i; break; // **
-      case '(': case ')': out.push({t:(flen===1?'t':c),v:c}); ++i; break;
-      case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
-        o = fmt[i]; while("0123456789".indexOf(fmt[++i]) > -1) o+=fmt[i];
-        out.push({t:'D', v:o}); break;
-      case ' ': out.push({t:c,v:c}); ++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': case ' ': break;
-      case 'd': case 'm': case 'y': case 'h': case 'H': case 'M': case 's': case 'A': case 'e': case 'Z':
-        out[i].v = write_date(out[i].t, out[i].v, dt);
-        out[i].t = 't'; break;
-      case 'n': case '(':
-        var jj = i+1;
-        while(out[jj] && ("? D".indexOf(out[jj].t) > -1 || out[i].t == '(' && (out[jj].t == ')' || out[jj].t == 'n') || out[jj].t == 't' && (out[jj].v == '/' || out[jj].v == '$' || (out[jj].v == ' ' && (out[jj+1]||{}).t == '?')))) {
-          if(out[jj].v!==' ') out[i].v += ' ' + out[jj].v;
-          delete out[jj]; ++jj;
-        }
-        out[i].v = write_num(out[i].t, out[i].v, v);
-        out[i].t = 't';
-        i = jj; 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, o) {
-  if(typeof fmt === 'number') fmt = table_fmt[fmt];
-  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 4: break;
-    default: throw "cannot find right format for |" + fmt + "|";
-  }
-  if(typeof v !== "number") return [fmt.length, fmt[3]];
-  return [l, v > 0 ? fmt[0] : v < 0 ? fmt[1] : fmt[2]];
-}
-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 === 'number') fmt = table_fmt[fmt];
-  var f = choose_fmt(fmt, v, o);
-  return eval_fmt(f[1], v, o, f[0]);
-};
-
-SSF._choose = choose_fmt;
-SSF._table = table_fmt;
-SSF.load = function(fmt, idx) { table_fmt[idx] = fmt; };
-SSF.format = format;
-};
-make_ssf(typeof exports !== 'undefined' ? exports : SSF);