version bump 0.3.0: tolerance adjustment

Note: The Aberth algorithm is sensitive to input errors

New tests trigger an infinite loop under old tolerance, work fine with new.
This commit is contained in:
SheetJS 2014-01-09 04:01:35 -05:00
parent 804bc32c2c
commit e0b3a1e75c
11 changed files with 3994507 additions and 163 deletions

@ -1,4 +1,4 @@
Copyright (C) 2013 SheetJS
Copyright (C) 2013-2014 SheetJS
The MIT License (MIT)

@ -2,5 +2,5 @@ frac.js: frac.md
voc frac.md
.PHONY: test
test:
test:
mocha -R spec

@ -37,3 +37,15 @@ For example:
`frac.cont` implements the Aberth algorithm (input and output specifications
match the original `frac` function)
## Tests
Tests generated from Excel have 4 columns. To produce a similar test:
- Column A contains the raw values
- Column B format "Up to one digit (1/4)"
- Column C format "Up to two digits (21/25)"
- Column D format "Up to three digits (312/943)"
[![githalytics.com alpha](https://cruel-carlota.pagodabox.com/731e31b3a26382ccd5d213b9e74ea552 "githalytics.com")](http://githalytics.com/SheetJS/frac)

72
frac.js

@ -1,42 +1,42 @@
/* frac.js (C) 2013 SheetJS -- http://sheetjs.com */
/* frac.js (C) 2013-2014 SheetJS -- http://sheetjs.com */
var frac = function(x, D, mixed) {
var n1 = Math.floor(x), d1 = 1;
var n2 = n1+1, d2 = 1;
if(x !== n1) while(d1 <= D && d2 <= D) {
var m = (n1 + n2) / (d1 + d2);
if(x === m) {
if(d1 + d2 <= D) { d1+=d2; n1+=n2; d2=D+1; }
else if(d1 > d2) d2=D+1;
else d1=D+1;
break;
}
else if(x < m) { n2 = n1+n2; d2 = d1+d2; }
else { n1 = n1+n2; d1 = d1+d2; }
var n1 = Math.floor(x), d1 = 1;
var n2 = n1+1, d2 = 1;
if(x !== n1) while(d1 <= D && d2 <= D) {
var m = (n1 + n2) / (d1 + d2);
if(x === m) {
if(d1 + d2 <= D) { d1+=d2; n1+=n2; d2=D+1; }
else if(d1 > d2) d2=D+1;
else d1=D+1;
break;
}
if(d1 > D) { d1 = d2; n1 = n2; }
if(!mixed) return [0, n1, d1];
var q = Math.floor(n1/d1);
return [q, n1 - q*d1, d1];
else if(x < m) { n2 = n1+n2; d2 = d1+d2; }
else { n1 = n1+n2; d1 = d1+d2; }
}
if(d1 > D) { d1 = d2; n1 = n2; }
if(!mixed) return [0, n1, d1];
var q = Math.floor(n1/d1);
return [q, n1 - q*d1, d1];
};
frac.cont = function cont(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];
};
if(typeof module !== 'undefined') module.exports = frac;

190
frac.md

@ -10,74 +10,74 @@ The JS implementation walks through the algorithm.
# JS Implementation
In this version, the return value is `[quotient, numerator, denominator]`,
where `quotient == 0` for improper fractions. The interpretation is
In this version, the return value is `[quotient, numerator, denominator]`,
where `quotient == 0` for improper fractions. The interpretation is
`x ~ quotient + numerator / denominator` where `0 <= numerator < denominator`
and `quotient <= x` for negative `x`.
```js>frac.js
/* frac.js (C) 2013 SheetJS -- http://sheetjs.com */
/* frac.js (C) 2013-2014 SheetJS -- http://sheetjs.com */
var frac = function(x, D, mixed) {
```
The goal is to maintain a feasible fraction (with bounded denominator) below
the target and another fraction above the target. The lower bound is
the target and another fraction above the target. The lower bound is
`floor(x) / 1` and the upper bound is `(floor(x) + 1) / 1`. We keep track of
the numerators and denominators separately:
```
var n1 = Math.floor(x), d1 = 1;
var n2 = n1+1, d2 = 1;
var n1 = Math.floor(x), d1 = 1;
var n2 = n1+1, d2 = 1;
```
If `x` is not integral, we bisect using mediants until a denominator exceeds
our target:
```
if(x !== n1) while(d1 <= D && d2 <= D) {
if(x !== n1) while(d1 <= D && d2 <= D) {
```
The mediant is the sum of the numerators divided by the sum of demoninators:
```
var m = (n1 + n2) / (d1 + d2);
var m = (n1 + n2) / (d1 + d2);
```
If we happened to stumble upon the exact value, then we choose the closer one
(the mediant if the denominator is within bounds, or the bound with the larger
denominator)
denominator)
```
if(x === m) {
if(d1 + d2 <= D) { d1+=d2; n1+=n2; d2=D+1; }
else if(d1 > d2) d2=D+1;
else d1=D+1;
break;
}
if(x === m) {
if(d1 + d2 <= D) { d1+=d2; n1+=n2; d2=D+1; }
else if(d1 > d2) d2=D+1;
else d1=D+1;
break;
}
```
Otherwise shrink the range:
```
else if(x < m) { n2 = n1+n2; d2 = d1+d2; }
else { n1 = n1+n2; d1 = d1+d2; }
}
else if(x < m) { n2 = n1+n2; d2 = d1+d2; }
else { n1 = n1+n2; d1 = d1+d2; }
}
```
At this point, `d1 > D` or `d2 > D` (but not both -- keep track of how `d1` and
`d2` change). So we merely return the desired values:
```
if(d1 > D) { d1 = d2; n1 = n2; }
if(!mixed) return [0, n1, d1];
var q = Math.floor(n1/d1);
return [q, n1 - q*d1, d1];
if(d1 > D) { d1 = d2; n1 = n2; }
if(!mixed) return [0, n1, d1];
var q = Math.floor(n1/d1);
return [q, n1 - q*d1, d1];
};
```
## Continued Fraction Method
The continued fraction technique is employed by various spreadsheet programs.
The continued fraction technique is employed by various spreadsheet programs.
Note that this technique is inferior to the mediant method (at least, according
to the desired goal of most accurately approximating the floating point number)
@ -90,64 +90,64 @@ frac.cont = function cont(x, D, mixed) {
Note that the variables are implicitly indexed at `k` (so `B` refers to `b_k`):
```
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;
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;
```
> Iterate
> ... for k = 0,1,...,K, where K is the first instance of k where
> ... for k = 0,1,...,K, where K is the first instance of k where
> either q_{k+1} > Q or b_{k+1} is undefined (b_k = a_k).
```
while(Q_1 < D) {
while(Q_1 < D) {
```
> a_k = [b_k], i.e., the greatest integer <= b_k
```
A = B|0;
A = B|0;
```
> p_k = a_k p_{k-1} + p_{k-2}
> q_k = a_k q_{k-1} + q_{k-2}
```
P = A * P_1 + P_2;
Q = A * Q_1 + Q_2;
P = A * P_1 + P_2;
Q = A * Q_1 + Q_2;
```
> b_{k+1} = (b_{k} - a_{k})^{-1}
> b_{k+1} = (b_{k} - a_{k})^{-1}
```
if((B - A) < 0.0000000001) break;
if((B - A) < 0.0000000005) break;
```
At the end of each iteration, advance `k` by one step:
```
B = 1 / (B - A);
P_2 = P_1; P_1 = P;
Q_2 = Q_1; Q_1 = Q;
}
```
B = 1 / (B - A);
P_2 = P_1; P_1 = P;
Q_2 = Q_1; Q_1 = Q;
}
```
In case we end up overstepping, walk back an iteration or two:
In case we end up overstepping, walk back an iteration or two:
```
if(Q > D) { Q = Q_1; P = P_1; }
if(Q > D) { Q = Q_2; P = P_2; }
if(Q > D) { Q = Q_1; P = P_1; }
if(Q > D) { Q = Q_2; P = P_2; }
```
The final result is `r = (sgn x)p_k / q_k`:
```
if(!mixed) return [0, sgn * P, Q];
var q = Math.floor(sgn * P/Q);
return [q, sgn*P - q*Q, Q];
if(!mixed) return [0, sgn * P, Q];
var q = Math.floor(sgn * P/Q);
return [q, sgn*P - q*Q, Q];
};
```
@ -163,34 +163,44 @@ if(typeof module !== 'undefined') module.exports = frac;
var fs = require('fs'), assert = require('assert');
var frac;
describe('source', function() { it('should load', function() { frac = require('./'); }); });
var xltestfiles=[
['xl.00001.tsv', 10000],
['xl.0001.tsv', 10000],
['xl.001.tsv', 10000],
['xl.01.tsv', 10000]
];
function line(o,j,m) {
it(j, function(done) {
var d, q, qq;
for(var i = j*100; i < m-3 && i < (j+1)*100; ++i) {
d = o[i].split("\t");
function xlline(o,j,m,w) {
it(j, function(done) {
var d, q, qq;
for(var i = j*w; i < m-3 && i < (j+1)*w; ++i) {
d = o[i].split("\t");
q = frac.cont(Number(d[0]), 9, true);
qq = (q[0]||q[1]) ? (q[0] || "") + " " + (q[1] ? q[1] + "/" + q[2] : " ") : "0 ";
assert.equal(qq, d[1], d[1] + " 1");
q = frac.cont(Number(d[0]), 9, true);
qq = (q[0]||q[1]) ? (q[0] || "") + " " + (q[1] ? q[1] + "/" + q[2] : " ") : "0 ";
assert.equal(qq, d[1], d[1] + " 1");
q = frac.cont(Number(d[0]), 99, true);
qq = (q[0]||q[1]) ? (q[0] || "") + " " + (q[1] ? (q[1] < 10 ? " " : "") + q[1] + "/" + q[2] + (q[2]<10?" ":"") : " ") : "0 ";
assert.equal(qq, d[2], d[2] + " 2");
q = frac.cont(Number(d[0]), 99, true);
qq = (q[0]||q[1]) ? (q[0] || "") + " " + (q[1] ? (q[1] < 10 ? " " : "") + q[1] + "/" + q[2] + (q[2]<10?" ":"") : " ") : "0 ";
assert.equal(qq, d[2], d[2] + " 2");
q = frac.cont(Number(d[0]), 999, true);
qq = (q[0]||q[1]) ? (q[0] || "") + " " + (q[1] ? (q[1] < 100 ? " " : "") + (q[1] < 10 ? " " : "") + q[1] + "/" + q[2] + (q[2]<10?" ":"") + (q[2]<100?" ":""): " ") : "0 ";
assert.equal(qq, d[3], d[3] + " 3");
}
done();
});
q = frac.cont(Number(d[0]), 999, true);
qq = (q[0]||q[1]) ? (q[0] || "") + " " + (q[1] ? (q[1] < 100 ? " " : "") + (q[1] < 10 ? " " : "") + q[1] + "/" + q[2] + (q[2]<10?" ":"") + (q[2]<100?" ":""): " ") : "0 ";
assert.equal(qq, d[3], d[3] + " 3");
}
done();
});
}
function parsetest(o) {
for(var j = 0, m = o.length-3; j < m/100; ++j) line(o,j,m);
function parsexl(f,w) {
if(!fs.existsSync(f)) return;
var o = fs.readFileSync(f, 'utf-8').split("\n");
for(var j = 0, m = o.length-3; j < m/w; ++j) xlline(o,j,m,w);
}
describe('xl.00001.tsv', function() {
var o = fs.readFileSync('./test_files/xl.00001.tsv', 'utf-8').split("\n");
parsetest(o);
xltestfiles.forEach(function(x) {
var f = './test_files/' + x[0];
describe(x[0], function() {
parsexl(f,x[1]);
});
});
```
@ -201,7 +211,7 @@ frac.js: frac.md
voc frac.md
.PHONY: test
test:
test:
mocha -R spec
```
@ -209,24 +219,32 @@ test:
```json>package.json
{
"name": "frac",
"version": "0.2.1",
"author": "SheetJS",
"description": "Rational approximation with bounded denominator",
"keywords": [ "math", "fraction", "rational", "approximation" ],
"main": "./frac.js",
"dependencies": {},
"devDependencies": {"mocha":""},
"repository": {
"type":"git",
"url": "git://github.com/SheetJS/frac.git"
},
"scripts": {
"test": "make test"
},
"bugs": { "url": "https://github.com/SheetJS/frac/issues" },
"engines": { "node": ">=0.8" }
"name": "frac",
"version": "0.3.0",
"author": "SheetJS",
"description": "Rational approximation with bounded denominator",
"keywords": [ "math", "fraction", "rational", "approximation" ],
"main": "./frac.js",
"dependencies": {},
"devDependencies": {"mocha":"","voc":""},
"repository": {
"type":"git",
"url": "git://github.com/SheetJS/frac.git"
},
"scripts": {
"test": "make test"
},
"bugs": { "url": "https://github.com/SheetJS/frac/issues" },
"engines": { "node": ">=0.8" }
}
```
And to make sure that test files are not included in npm:
```>.npmignore
./test_files
```
```>.gitignore
.gitignore
.npmignore
```

@ -1,19 +1,19 @@
{
"name": "frac",
"version": "0.2.1",
"author": "SheetJS",
"description": "Rational approximation with bounded denominator",
"keywords": [ "math", "fraction", "rational", "approximation" ],
"main": "./frac.js",
"dependencies": {},
"devDependencies": {"mocha":""},
"repository": {
"type":"git",
"url": "git://github.com/SheetJS/frac.git"
},
"scripts": {
"test": "make test"
},
"bugs": { "url": "https://github.com/SheetJS/frac/issues" },
"engines": { "node": ">=0.8" }
"name": "frac",
"version": "0.3.0",
"author": "SheetJS",
"description": "Rational approximation with bounded denominator",
"keywords": [ "math", "fraction", "rational", "approximation" ],
"main": "./frac.js",
"dependencies": {},
"devDependencies": {"mocha":"","voc":""},
"repository": {
"type":"git",
"url": "git://github.com/SheetJS/frac.git"
},
"scripts": {
"test": "make test"
},
"bugs": { "url": "https://github.com/SheetJS/frac/issues" },
"engines": { "node": ">=0.8" }
}

54
test.js

@ -1,32 +1,42 @@
var fs = require('fs'), assert = require('assert');
var frac;
describe('source', function() { it('should load', function() { frac = require('./'); }); });
var xltestfiles=[
['xl.00001.tsv', 10000],
['xl.0001.tsv', 10000],
['xl.001.tsv', 10000],
['xl.01.tsv', 10000]
];
function line(o,j,m) {
it(j, function(done) {
var d, q, qq;
for(var i = j*100; i < m-3 && i < (j+1)*100; ++i) {
d = o[i].split("\t");
function xlline(o,j,m,w) {
it(j, function(done) {
var d, q, qq;
for(var i = j*w; i < m-3 && i < (j+1)*w; ++i) {
d = o[i].split("\t");
q = frac.cont(Number(d[0]), 9, true);
qq = (q[0]||q[1]) ? (q[0] || "") + " " + (q[1] ? q[1] + "/" + q[2] : " ") : "0 ";
assert.equal(qq, d[1], d[1] + " 1");
q = frac.cont(Number(d[0]), 9, true);
qq = (q[0]||q[1]) ? (q[0] || "") + " " + (q[1] ? q[1] + "/" + q[2] : " ") : "0 ";
assert.equal(qq, d[1], d[1] + " 1");
q = frac.cont(Number(d[0]), 99, true);
qq = (q[0]||q[1]) ? (q[0] || "") + " " + (q[1] ? (q[1] < 10 ? " " : "") + q[1] + "/" + q[2] + (q[2]<10?" ":"") : " ") : "0 ";
assert.equal(qq, d[2], d[2] + " 2");
q = frac.cont(Number(d[0]), 99, true);
qq = (q[0]||q[1]) ? (q[0] || "") + " " + (q[1] ? (q[1] < 10 ? " " : "") + q[1] + "/" + q[2] + (q[2]<10?" ":"") : " ") : "0 ";
assert.equal(qq, d[2], d[2] + " 2");
q = frac.cont(Number(d[0]), 999, true);
qq = (q[0]||q[1]) ? (q[0] || "") + " " + (q[1] ? (q[1] < 100 ? " " : "") + (q[1] < 10 ? " " : "") + q[1] + "/" + q[2] + (q[2]<10?" ":"") + (q[2]<100?" ":""): " ") : "0 ";
assert.equal(qq, d[3], d[3] + " 3");
}
done();
});
q = frac.cont(Number(d[0]), 999, true);
qq = (q[0]||q[1]) ? (q[0] || "") + " " + (q[1] ? (q[1] < 100 ? " " : "") + (q[1] < 10 ? " " : "") + q[1] + "/" + q[2] + (q[2]<10?" ":"") + (q[2]<100?" ":""): " ") : "0 ";
assert.equal(qq, d[3], d[3] + " 3");
}
done();
});
}
function parsetest(o) {
for(var j = 0, m = o.length-3; j < m/100; ++j) line(o,j,m);
function parsexl(f,w) {
if(!fs.existsSync(f)) return;
var o = fs.readFileSync(f, 'utf-8').split("\n");
for(var j = 0, m = o.length-3; j < m/w; ++j) xlline(o,j,m,w);
}
describe('xl.00001.tsv', function() {
var o = fs.readFileSync('./test_files/xl.00001.tsv', 'utf-8').split("\n");
parsetest(o);
xltestfiles.forEach(function(x) {
var f = './test_files/' + x[0];
describe(x[0], function() {
parsexl(f,x[1]);
});
});

File diff suppressed because it is too large Load Diff

1048576
test_files/xl.0001.tsv Normal file

File diff suppressed because it is too large Load Diff

1048576
test_files/xl.001.tsv Normal file

File diff suppressed because it is too large Load Diff

1048576
test_files/xl.01.tsv Normal file

File diff suppressed because it is too large Load Diff