2013-12-14 07:11:37 +00:00
|
|
|
# Target
|
|
|
|
|
|
|
|
In all languages, the target is a function that takes 3 parameters:
|
|
|
|
|
|
|
|
- `x` the number we wish to approximate
|
|
|
|
- `D` the maximum denominator
|
2015-04-22 04:14:03 +00:00
|
|
|
- `mixed` if true, return a mixed fraction; if false, improper
|
2013-12-14 07:11:37 +00:00
|
|
|
|
|
|
|
The JS implementation walks through the algorithm.
|
|
|
|
|
|
|
|
# JS Implementation
|
|
|
|
|
2015-04-22 04:14:03 +00:00
|
|
|
In this version, the return value is `[quotient, numerator, denominator]`. The
|
|
|
|
interpretation is `x ~ quotient + numerator / denominator`
|
2013-12-14 07:11:37 +00:00
|
|
|
|
2015-04-22 04:14:03 +00:00
|
|
|
- For improper fractions, `quotient == 0`.
|
|
|
|
|
|
|
|
- For proper fractions, `0 <= numerator < denominator` and `quotient <= x`
|
|
|
|
|
|
|
|
For negative `x`, this deviates from how some people and systems interpret mixed
|
|
|
|
fractions. Some interpret `a b/c` as `sgn(a) * [abs(a) + b/c]`. To replicate
|
|
|
|
that behavior, pass the absolute value to frac and prepend a "-" if negative.
|
|
|
|
|
|
|
|
```js>frac.flow.js
|
2016-01-16 06:37:17 +00:00
|
|
|
/* frac.js (C) 2012-present SheetJS -- http://sheetjs.com */
|
2016-09-23 05:43:23 +00:00
|
|
|
var frac = function frac(x/*:number*/, D/*:number*/, mixed/*:?boolean*/)/*:Array<number>*/ {
|
2013-12-14 07:11:37 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
The goal is to maintain a feasible fraction (with bounded denominator) below
|
2014-01-09 09:01:35 +00:00
|
|
|
the target and another fraction above the target. The lower bound is
|
2013-12-14 07:11:37 +00:00
|
|
|
`floor(x) / 1` and the upper bound is `(floor(x) + 1) / 1`. We keep track of
|
|
|
|
the numerators and denominators separately:
|
|
|
|
|
|
|
|
```
|
2014-01-09 09:01:35 +00:00
|
|
|
var n1 = Math.floor(x), d1 = 1;
|
|
|
|
var n2 = n1+1, d2 = 1;
|
2013-12-14 07:11:37 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
If `x` is not integral, we bisect using mediants until a denominator exceeds
|
|
|
|
our target:
|
|
|
|
|
|
|
|
```
|
2014-01-09 09:01:35 +00:00
|
|
|
if(x !== n1) while(d1 <= D && d2 <= D) {
|
2013-12-14 07:11:37 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
The mediant is the sum of the numerators divided by the sum of demoninators:
|
|
|
|
|
|
|
|
```
|
2014-01-09 09:01:35 +00:00
|
|
|
var m = (n1 + n2) / (d1 + d2);
|
2013-12-14 07:11:37 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
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
|
2014-01-09 09:01:35 +00:00
|
|
|
denominator)
|
2013-12-14 07:11:37 +00:00
|
|
|
|
|
|
|
```
|
2014-01-09 09:01:35 +00:00
|
|
|
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;
|
|
|
|
}
|
2013-12-14 07:11:37 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
Otherwise shrink the range:
|
|
|
|
|
|
|
|
```
|
2014-01-09 09:01:35 +00:00
|
|
|
else if(x < m) { n2 = n1+n2; d2 = d1+d2; }
|
|
|
|
else { n1 = n1+n2; d1 = d1+d2; }
|
|
|
|
}
|
2013-12-14 07:11:37 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
```
|
2014-01-09 09:01:35 +00:00
|
|
|
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];
|
2013-12-14 07:11:37 +00:00
|
|
|
};
|
|
|
|
```
|
|
|
|
|
2013-12-25 04:06:06 +00:00
|
|
|
## Continued Fraction Method
|
|
|
|
|
2014-01-09 09:01:35 +00:00
|
|
|
The continued fraction technique is employed by various spreadsheet programs.
|
2013-12-25 04:06:06 +00:00
|
|
|
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)
|
|
|
|
|
2015-04-22 04:14:03 +00:00
|
|
|
```js>frac.flow.js
|
|
|
|
frac.cont = function cont(x/*:number*/, D/*:number*/, mixed/*:?boolean*/)/*:Array<number>*/ {
|
2013-12-25 04:06:06 +00:00
|
|
|
```
|
|
|
|
|
2016-09-23 05:43:23 +00:00
|
|
|
> Record the sign of x, take `b0=|x|, p_{-2}=0, p_{-1}=1, q_{-2}=1, q_{-1}=0`
|
2013-12-25 04:06:06 +00:00
|
|
|
|
|
|
|
Note that the variables are implicitly indexed at `k` (so `B` refers to `b_k`):
|
|
|
|
|
|
|
|
```
|
2014-01-09 09:01:35 +00:00
|
|
|
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;
|
2014-01-12 06:44:07 +00:00
|
|
|
```
|
|
|
|
|
2016-09-23 05:43:23 +00:00
|
|
|
`A` should be the floor of `B`. Originally the bit-or trick was used, but this
|
|
|
|
is not correct for the range `B>=2**32`.
|
2014-01-12 06:44:07 +00:00
|
|
|
|
|
|
|
```
|
|
|
|
var A = Math.floor(B);
|
2013-12-25 04:06:06 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
> Iterate
|
|
|
|
|
2016-09-23 05:43:23 +00:00
|
|
|
> ... 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`).
|
2013-12-25 04:06:06 +00:00
|
|
|
|
|
|
|
```
|
2014-01-09 09:01:35 +00:00
|
|
|
while(Q_1 < D) {
|
2013-12-25 04:06:06 +00:00
|
|
|
```
|
|
|
|
|
2016-09-23 05:43:23 +00:00
|
|
|
> `a_k = [b_k]`, i.e., the greatest integer `<= b_k`
|
2013-12-25 04:06:06 +00:00
|
|
|
|
|
|
|
```
|
2014-01-12 06:44:07 +00:00
|
|
|
A = Math.floor(B);
|
2013-12-25 04:06:06 +00:00
|
|
|
```
|
|
|
|
|
2016-09-23 05:43:23 +00:00
|
|
|
> `p_k = a_k p_{k-1} + p_{k-2}`
|
|
|
|
> `q_k = a_k q_{k-1} + q_{k-2}`
|
2013-12-25 04:06:06 +00:00
|
|
|
|
|
|
|
```
|
2014-01-09 09:01:35 +00:00
|
|
|
P = A * P_1 + P_2;
|
|
|
|
Q = A * Q_1 + Q_2;
|
2013-12-25 04:06:06 +00:00
|
|
|
```
|
|
|
|
|
2016-09-23 05:43:23 +00:00
|
|
|
> `b_{k+1} = (b_{k} - a_{k})^{-1}`
|
2013-12-25 04:06:06 +00:00
|
|
|
|
|
|
|
```
|
2016-09-23 05:43:23 +00:00
|
|
|
if((B - A) < 0.00000005) break;
|
2013-12-25 04:06:06 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
At the end of each iteration, advance `k` by one step:
|
|
|
|
|
2014-01-09 09:01:35 +00:00
|
|
|
```
|
|
|
|
B = 1 / (B - A);
|
|
|
|
P_2 = P_1; P_1 = P;
|
|
|
|
Q_2 = Q_1; Q_1 = Q;
|
|
|
|
}
|
2013-12-25 04:06:06 +00:00
|
|
|
```
|
|
|
|
|
2015-05-05 06:19:23 +00:00
|
|
|
In case we end up overstepping, walk back to the last valid iteration:
|
2013-12-25 04:06:06 +00:00
|
|
|
|
|
|
|
```
|
2015-05-05 06:19:23 +00:00
|
|
|
if(Q > D) { if(Q_1 > D) { Q = Q_2; P = P_2; } else { Q = Q_1; P = P_1; } }
|
2013-12-25 04:06:06 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
The final result is `r = (sgn x)p_k / q_k`:
|
|
|
|
|
|
|
|
```
|
2014-01-09 09:01:35 +00:00
|
|
|
if(!mixed) return [0, sgn * P, Q];
|
|
|
|
var q = Math.floor(sgn * P/Q);
|
|
|
|
return [q, sgn*P - q*Q, Q];
|
2013-12-25 04:06:06 +00:00
|
|
|
};
|
|
|
|
```
|
|
|
|
|
2013-12-14 07:11:37 +00:00
|
|
|
Finally we put some export jazz:
|
|
|
|
|
|
|
|
```
|
2015-04-22 04:14:03 +00:00
|
|
|
/*:: declare var DO_NOT_EXPORT_FRAC: any; */
|
2014-05-01 03:21:53 +00:00
|
|
|
if(typeof module !== 'undefined' && typeof DO_NOT_EXPORT_FRAC === 'undefined') module.exports = frac;
|
2013-12-14 07:11:37 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
# Tests
|
|
|
|
|
|
|
|
```js>test.js
|
2013-12-26 04:14:09 +00:00
|
|
|
var fs = require('fs'), assert = require('assert');
|
2013-12-14 07:11:37 +00:00
|
|
|
var frac;
|
|
|
|
describe('source', function() { it('should load', function() { frac = require('./'); }); });
|
2014-01-09 09:01:35 +00:00
|
|
|
var xltestfiles=[
|
|
|
|
['xl.00001.tsv', 10000],
|
|
|
|
['xl.0001.tsv', 10000],
|
|
|
|
['xl.001.tsv', 10000],
|
2014-01-12 06:44:07 +00:00
|
|
|
['xl.01.tsv', 10000],
|
|
|
|
['oddities.tsv', 25]
|
2014-01-09 09:01:35 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
function xlline(o,j,m,w) {
|
2016-09-23 05:43:23 +00:00
|
|
|
it(j.toString(), function() {
|
|
|
|
var d, q, qq, f = 0.1;
|
|
|
|
var q0 = 0, q1 = 0, q2 = 0
|
2014-01-09 09:01:35 +00:00
|
|
|
for(var i = j*w; i < m-3 && i < (j+1)*w; ++i) {
|
|
|
|
d = o[i].split("\t");
|
2016-09-23 05:43:23 +00:00
|
|
|
if(d.length < 3) continue;
|
|
|
|
f = parseFloat(d[0]);
|
2014-01-09 09:01:35 +00:00
|
|
|
|
2016-09-23 05:43:23 +00:00
|
|
|
q = frac.cont(f, 9, true);
|
|
|
|
q0 = q[0]; q1 = q[1]; q2 = q[2];
|
|
|
|
qq = (q0!=0||q1!=0) ? (q0!=0 ? q0.toString() : "") + " " + (q1!=0 ? q1.toString() + "/" + q2.toString() : " ") : "0 ";
|
2014-01-09 09:01:35 +00:00
|
|
|
assert.equal(qq, d[1], d[1] + " 1");
|
|
|
|
|
2016-09-23 05:43:23 +00:00
|
|
|
q = frac.cont(f, 99, true);
|
|
|
|
qq = (q[0]!=0||q[1]!=0) ? (q[0]!=0 ? q[0].toString() : "") + " " + (q[1]!=0 ? (q[1] < 10 ? " " : "") + q[1].toString() + "/" + q[2].toString() + (q[2]<10?" ":"") : " ") : "0 ";
|
2014-01-09 09:01:35 +00:00
|
|
|
assert.equal(qq, d[2], d[2] + " 2");
|
|
|
|
|
2016-09-23 05:43:23 +00:00
|
|
|
q = frac.cont(f, 999, true);
|
|
|
|
qq = (q[0]!=0||q[1]!=0) ? (q[0]!=0 ? q[0].toString() : "") + " " + (q[1]!=0 ? (q[1] < 100 ? " " : "") + (q[1] < 10 ? " " : "") + q[1].toString() + "/" + q[2].toString() + (q[2]<10?" ":"") + (q[2]<100?" ":""): " ") : "0 ";
|
2014-01-09 09:01:35 +00:00
|
|
|
assert.equal(qq, d[3], d[3] + " 3");
|
|
|
|
}
|
|
|
|
});
|
2013-12-26 04:14:09 +00:00
|
|
|
}
|
2014-01-09 09:01:35 +00:00
|
|
|
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);
|
2013-12-26 04:14:09 +00:00
|
|
|
}
|
2014-05-01 02:32:25 +00:00
|
|
|
function cmp(a,b) { assert.equal(a.length,b.length); for(var i = 0; i != a.length; ++i) assert.equal(a[i], b[i]); }
|
|
|
|
describe('mediant', function() {
|
|
|
|
it('should do the right thing for tenths', function() {
|
|
|
|
cmp(frac(0.1,9,false),[0,1,9]);
|
|
|
|
cmp(frac(0.2,9,false),[0,1,5]);
|
|
|
|
cmp(frac(0.3,9,false),[0,2,7]);
|
|
|
|
cmp(frac(0.4,9,false),[0,2,5]);
|
|
|
|
cmp(frac(0.5,9,false),[0,1,2]);
|
|
|
|
cmp(frac(0.6,9,false),[0,3,5]);
|
|
|
|
cmp(frac(0.7,9,false),[0,5,7]);
|
|
|
|
cmp(frac(0.8,9,false),[0,4,5]);
|
|
|
|
cmp(frac(0.9,9,false),[0,8,9]);
|
|
|
|
cmp(frac(1.0,9,false),[0,1,1]);
|
|
|
|
cmp(frac(1.0,9,true), [1,0,1]);
|
|
|
|
cmp(frac(1.7,9,true), [1,5,7]);
|
|
|
|
cmp(frac(1.7,9,false),[0,12,7]);
|
|
|
|
});
|
|
|
|
});
|
2014-01-09 09:01:35 +00:00
|
|
|
xltestfiles.forEach(function(x) {
|
|
|
|
var f = './test_files/' + x[0];
|
|
|
|
describe(x[0], function() {
|
|
|
|
parsexl(f,x[1]);
|
|
|
|
});
|
2013-12-26 04:14:09 +00:00
|
|
|
});
|
2013-12-14 07:11:37 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
## Node Ilk
|
|
|
|
|
|
|
|
```json>package.json
|
|
|
|
{
|
2014-01-09 09:01:35 +00:00
|
|
|
"name": "frac",
|
2017-07-28 03:19:00 +00:00
|
|
|
"version": "1.1.0",
|
2014-01-09 09:01:35 +00:00
|
|
|
"author": "SheetJS",
|
|
|
|
"description": "Rational approximation with bounded denominator",
|
|
|
|
"keywords": [ "math", "fraction", "rational", "approximation" ],
|
2014-05-01 03:21:53 +00:00
|
|
|
"main": "frac.js",
|
|
|
|
"dependencies": {
|
2017-07-28 03:19:00 +00:00
|
|
|
"voc":"~1.0.0"
|
2014-05-01 03:21:53 +00:00
|
|
|
},
|
|
|
|
"devDependencies": {
|
2017-07-28 03:19:00 +00:00
|
|
|
"mocha":"~2.5.3"
|
2014-05-01 03:21:53 +00:00
|
|
|
},
|
|
|
|
"repository": { "type":"git", "url":"git://github.com/SheetJS/frac.git" },
|
2014-01-09 09:01:35 +00:00
|
|
|
"scripts": {
|
2016-09-23 05:43:23 +00:00
|
|
|
"test": "make test"
|
2014-01-09 09:01:35 +00:00
|
|
|
},
|
2014-05-01 02:32:25 +00:00
|
|
|
"config": {
|
|
|
|
"blanket": {
|
|
|
|
"pattern": "frac.js"
|
|
|
|
}
|
|
|
|
},
|
2017-07-28 03:19:00 +00:00
|
|
|
"homepage": "http://oss.sheetjs.com/frac",
|
2014-01-09 09:01:35 +00:00
|
|
|
"bugs": { "url": "https://github.com/SheetJS/frac/issues" },
|
2014-05-01 03:21:53 +00:00
|
|
|
"license": "Apache-2.0",
|
2014-01-09 09:01:35 +00:00
|
|
|
"engines": { "node": ">=0.8" }
|
2013-12-14 07:11:37 +00:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2014-01-09 09:01:35 +00:00
|
|
|
And to make sure that test files are not included in npm:
|
2014-01-17 20:25:17 +00:00
|
|
|
|
2014-01-09 09:01:35 +00:00
|
|
|
```>.npmignore
|
2014-01-12 06:44:07 +00:00
|
|
|
test_files/*.tsv
|
2017-07-28 03:19:00 +00:00
|
|
|
ctest/
|
|
|
|
test.js
|
|
|
|
Makefile
|
2014-01-17 20:25:17 +00:00
|
|
|
.gitignore
|
2017-07-28 03:19:00 +00:00
|
|
|
.npmignore
|
2014-01-17 20:25:17 +00:00
|
|
|
node_modules/
|
2014-05-01 02:32:25 +00:00
|
|
|
coverage.html
|
2015-04-22 04:14:03 +00:00
|
|
|
.travis.yml
|
|
|
|
.jshintrc
|
|
|
|
.jscs.json
|
|
|
|
.flowconfig
|
|
|
|
misc/
|
|
|
|
*.sheetjs
|
|
|
|
*.pyc
|
|
|
|
build/
|
|
|
|
MANIFEST
|
|
|
|
*.gz
|
|
|
|
*.tgz
|
2017-07-28 03:19:00 +00:00
|
|
|
*.py
|
|
|
|
*.html
|
2014-01-09 09:01:35 +00:00
|
|
|
```
|
2013-12-14 07:11:37 +00:00
|
|
|
|
2014-01-17 20:25:17 +00:00
|
|
|
Don't include the node modules in git:
|
|
|
|
|
2014-01-09 09:01:35 +00:00
|
|
|
```>.gitignore
|
|
|
|
.gitignore
|
2014-01-17 20:25:17 +00:00
|
|
|
node_modules/
|
2014-05-01 02:32:25 +00:00
|
|
|
coverage.html
|
2015-04-22 04:14:03 +00:00
|
|
|
*.sheetjs
|
|
|
|
*.pyc
|
|
|
|
build/
|
|
|
|
MANIFEST
|
|
|
|
*.gz
|
|
|
|
*.tgz
|
2014-01-09 09:01:35 +00:00
|
|
|
```
|