version bump 0.5.7: addressing extraneous '['

- extraneous '[' does not cause infinite loop
- dates follow excel form (`yyyyyy` treated as `yyyy`)
- more general exponential form (more tests)
- unreachable default cases removed
- 100% test coverage
- added test_min and cov_min targets
This commit is contained in:
SheetJS 2014-02-11 14:20:34 -05:00
parent 71a974653d
commit a866c9eabf
9 changed files with 85 additions and 40 deletions

View File

@ -5,6 +5,9 @@ ssf: ssf.md
test:
npm test
test_min:
MINTEST=1 npm test
.PHONY: lint
lint:
jshint ssf.js test/
@ -12,9 +15,13 @@ lint:
.PHONY: cov
cov: tmp/coverage.html
tmp/coverage.html: ssf.md
tmp/coverage.html: ssf
mocha --require blanket -R html-cov > tmp/coverage.html
.PHONY: cov_min
cov_min:
MINTEST=1 make cov
.PHONY: coveralls
coveralls:
mocha --require blanket --reporter mocha-lcov-reporter | ./node_modules/coveralls/bin/coveralls.js

View File

@ -1,6 +1,6 @@
{
"name": "ssf",
"version": "0.5.6",
"version": "0.5.7",
"author": "SheetJS",
"description": "pure-JS library to format data using ECMA-376 spreadsheet Format Codes",
"keywords": [ "format", "sprintf", "spreadsheet" ],

22
ssf.js
View File

@ -5,7 +5,7 @@ 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));}
SSF.version = '0.5.6';
SSF.version = '0.5.7';
/* Options */
var opts_fmt = {};
function fixopts(o){for(var y in opts_fmt) if(o[y]===undefined) o[y]=opts_fmt[y];}
@ -146,23 +146,20 @@ var write_date = function(type, fmt, val) {
switch(type) {
case 'y': switch(fmt) { /* year */
case 'y': case 'yy': return pad(val.y % 100,2);
case 'yyy': case 'yyyy': return pad(val.y % 10000,4);
default: throw 'bad year format: ' + fmt;
default: return pad(val.y % 10000,4);
}
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;
default: return months[val.m-1][2];
}
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;
default: return days[val.q][1];
}
case 'h': switch(fmt) { /* 12-hour */
case 'h': return 1+(val.H+11)%12;
@ -195,7 +192,6 @@ var write_date = function(type, fmt, val) {
} return fmt.length === 3 ? o : pad(o, 2);
/* TODO: handle the ECMA spec format ee -> yy */
case 'e': { return val.y; } break;
default: throw 'bad format type ' + type + ' in ' + fmt;
}
};
/*jshint +W086 */
@ -211,7 +207,7 @@ 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 == '##0.0E+0') {
if(fmt.match(/^#+0.0E\+0$/)) {
var period = fmt.indexOf("."); if(period === -1) period=fmt.indexOf('E');
var ee = (Number(val.toExponential(0).substr(2+(val<0))))%period;
if(ee < 0) ee += period;
@ -329,7 +325,8 @@ function eval_fmt(fmt, v, opts, flen) {
out.push(q); lst = c; break;
case '[': /* TODO: Fix this -- ignore all conditionals and formatting */
o = c;
while(fmt[i++] !== ']') o += fmt[i];
while(fmt[i++] !== ']' && i < fmt.length) o += fmt[i];
if(o.substr(-1) !== ']') throw 'unterminated "[" block: |' + o + '|';
if(o.match(/\[[HhMmSs]*\]/)) {
if(!dt) dt = parse_date_code(v, opts);
if(!dt) return "";
@ -368,8 +365,8 @@ function eval_fmt(fmt, v, opts, flen) {
/* 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':
case 't': case 'T': case ' ': case 'D': break;
case 'd': case 'm': case 'y': case 'h': case 'H': case 'M': case 's': case 'e': case 'Z':
out[i].v = write_date(out[i].t, out[i].v, dt);
out[i].t = 't'; break;
case 'n': case '(': case '?':
@ -382,7 +379,6 @@ function eval_fmt(fmt, v, opts, flen) {
out[i].t = 't';
i = jj-1; break;
case 'G': out[i].t = 't'; out[i].v = general_fmt(v,opts); break;
default: console.error(out); throw "unrecognized type " + out[i].t;
}
}
return out.map(function(x){return x.v;}).join("");

71
ssf.md
View File

@ -376,7 +376,7 @@ For exponents, get the exponent and mantissa and format them separately:
For the special case of engineering notation, "shift" the decimal:
```
if(fmt == '##0.0E+0') {
if(fmt.match(/^#+0.0E\+0$/)) {
var period = fmt.indexOf("."); if(period === -1) period=fmt.indexOf('E');
var ee = (Number(val.toExponential(0).substr(2+(val<0))))%period;
if(ee < 0) ee += period;
@ -589,7 +589,8 @@ 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];
while(fmt[i++] !== ']' && i < fmt.length) o += fmt[i];
if(o.substr(-1) !== ']') throw 'unterminated "[" block: |' + o + '|';
if(o.match(/\[[HhMmSs]*\]/)) {
if(!dt) dt = parse_date_code(v, opts);
if(!dt) return "";
@ -663,8 +664,8 @@ The default magic characters are listed in subsubsections 18.8.30-31 of ECMA376:
/* 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':
case 't': case 'T': case ' ': case 'D': break;
case 'd': case 'm': case 'y': case 'h': case 'H': case 'M': case 's': case 'e': case 'Z':
out[i].v = write_date(out[i].t, out[i].v, dt);
out[i].t = 't'; break;
case 'n': case '(': case '?':
@ -683,7 +684,12 @@ positive when there is an explicit hyphen before it (e.g. `#,##0.0;-#,##0.0`):
out[i].t = 't';
i = jj-1; break;
case 'G': out[i].t = 't'; out[i].v = general_fmt(v,opts); break;
default: console.error(out); throw "unrecognized type " + out[i].t;
```
The default case should not be reachable. In testing, add the line
`default: console.error(out); throw "unrecognized type " + out[i].t;`
```
}
}
return out.map(function(x){return x.v;}).join("");
@ -706,23 +712,35 @@ var write_date = function(type, fmt, val) {
switch(type) {
case 'y': switch(fmt) { /* year */
case 'y': case 'yy': return pad(val.y % 100,2);
case 'yyy': case 'yyyy': return pad(val.y % 10000,4);
default: throw 'bad year format: ' + fmt;
```
Apparently, even `yyyyyyyyyyyyyyyyyyyy` is a 4 digit year
```
default: return pad(val.y % 10000,4);
}
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;
```
Strangely enough, `mmmmmmmmmmmmmmmmmmmm` is treated as the full month name:
```
default: return months[val.m-1][2];
}
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;
```
Strangely enough, `dddddddddddddddddddd` is treated as the full day name:
```
default: return days[val.q][1];
}
case 'h': switch(fmt) { /* 12-hour */
case 'h': return 1+(val.H+11)%12;
@ -766,7 +784,12 @@ 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;
default: throw 'bad format type ' + type + ' in ' + fmt;
```
There is no input to the function that ends up triggering the default behavior:
it is not exported and is only called when the type is in `ymdhHMsZe`
```
}
};
/*jshint +W086 */
@ -939,6 +962,9 @@ ssf: ssf.md
test:
npm test
test_min:
MINTEST=1 npm test
.PHONY: lint
lint:
jshint ssf.js test/
@ -951,8 +977,12 @@ Coverage tests use [blanket](http://npm.im/blanket):
.PHONY: cov
cov: tmp/coverage.html
tmp/coverage.html: ssf.md
tmp/coverage.html: ssf
mocha --require blanket -R html-cov > tmp/coverage.html
.PHONY: cov_min
cov_min:
MINTEST=1 make cov
```
Coveralls.io support
@ -967,7 +997,7 @@ coveralls:
```json>package.json
{
"name": "ssf",
"version": "0.5.6",
"version": "0.5.7",
"author": "SheetJS",
"description": "pure-JS library to format data using ECMA-376 spreadsheet Format Codes",
"keywords": [ "format", "sprintf", "spreadsheet" ],
@ -1105,9 +1135,9 @@ function doit(data) {
}
describe('time formats', function() { doit(times.slice(0,1000)); });
describe('date formats', function() {
doit(dates);
doit(process.env.MINTEST ? dates.slice(0,1000) : dates);
it('should fail for bad formats', function() {
var bad = ['yyyyy', 'mmmmmm', 'ddddd'];
var bad = [];
var chk = function(fmt){ return function(){ SSF.format(fmt,0); }; };
bad.forEach(function(fmt){assert.throws(chk(fmt));});
});
@ -1124,7 +1154,7 @@ 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) {
for(var w = 1; w < 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("|");
@ -1133,7 +1163,7 @@ function doit(d, headers) {
}
describe('exponential formats', function() {
var headers = data[0].split("\t");
for(var j=14/* TODO: start from 1 */;j<data.length;++j) {
for(var j=1;j<data.length;++j) {
if(!data[j]) return;
doit(data[j].replace(/#{255}/g,"").split("\t"), headers);
}
@ -1159,6 +1189,11 @@ describe('oddities', function() {
}
});
});
it('should fail for bad formats', function() {
var bad = ['##,##'];
var chk = function(fmt){ return function(){ SSF.format(fmt,0); }; };
bad.forEach(function(fmt){assert.throws(chk(fmt));});
});
});
```

View File

@ -23,9 +23,9 @@ function doit(data) {
}
describe('time formats', function() { doit(times.slice(0,1000)); });
describe('date formats', function() {
doit(dates);
doit(process.env.MINTEST ? dates.slice(0,1000) : dates);
it('should fail for bad formats', function() {
var bad = ['yyyyy', 'mmmmmm', 'ddddd'];
var bad = [];
var chk = function(fmt){ return function(){ SSF.format(fmt,0); }; };
bad.forEach(function(fmt){assert.throws(chk(fmt));});
});

View File

@ -5,7 +5,7 @@ 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) {
for(var w = 1; w < 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("|");
@ -14,7 +14,7 @@ function doit(d, headers) {
}
describe('exponential formats', function() {
var headers = data[0].split("\t");
for(var j=14/* TODO: start from 1 */;j<data.length;++j) {
for(var j=1;j<data.length;++j) {
if(!data[j]) return;
doit(data[j].replace(/#{255}/g,"").split("\t"), headers);
}

View File

@ -15,13 +15,13 @@ value #0.0E+0 ##0.0E+0 ###0.0E+0 ####0.0E+0
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
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
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
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

1 value #0.0E+0 ##0.0E+0 ###0.0E+0 ####0.0E+0
15 1.23456789 1.2E+0 1.2E+0 1.2E+0 1.2E+0
16 12.3456789 12.3E+0 12.3E+0 12.3E+0 12.3E+0
17 123.456789 1.2E+2 123.5E+0 123.5E+0 123.5E+0
18 1234.56789 12.3E+2 |12.3E+2 1.2E+3 1234.6E+0 1234.6E+0
19 12345.6789 1.2E+4 12.3E+3 1.2E+4 12345.7E+0
20 123456.789 12.3E+4 123.5E+3 12.3E+4 1.2E+5
21 1234567.89 1.2E+6 1.2E+6 123.5E+4 12.3E+5
22 12345678.9 12.3E+6 12.3E+6 1234.6E+4 |1234.6E+4 123.5E+5
23 123456789 1.2E+8 123.5E+6 1.2E+8 1234.6E+5
24 1234567890 12.3E+8 1.2E+9 12.3E+8 12345.7E+5 |12345.7E+5
25 12345678900 1.2E+10 12.3E+9 123.5E+8 1.2E+10
26 123456789000 12.3E+10 123.5E+9 1234.6E+8 12.3E+10
27 1234567890000 1.2E+12 1.2E+12 1.2E+12 123.5E+10

View File

@ -14,4 +14,9 @@ describe('oddities', function() {
}
});
});
it('should fail for bad formats', function() {
var bad = ['##,##'];
var chk = function(fmt){ return function(){ SSF.format(fmt,0); }; };
bad.forEach(function(fmt){assert.throws(chk(fmt));});
});
});

View File

@ -41,7 +41,9 @@
["hh:mmm:sss", [0.7]],
["hh:mm:sss", [0.7]],
["[hhh]", [0.7]],
["[", [0.7]],
["A/P", [0.7, "P"]],
["e", [0.7, "1900"]],
["123", [0.7, "123"], [0, "123"], ["sheetjs", "sheetjs"]],
["\"foo\";\"bar\";\"baz\";\"qux\";\"foobar\"", [1], [0], [-1], ["sheetjs"]]
]