diff --git a/Makefile b/Makefile index 4f5333e..fb506df 100644 --- a/Makefile +++ b/Makefile @@ -65,10 +65,16 @@ dist-deps: ## Python .PHONY: pylint -pylint: frac.py +pylint: frac.py $(wildcard test_*.py) pep8 $^ .PHONY: pypi pypi: frac.py python setup.py sdist upload +.PHONY: pytest pypytest +pytest: pylint + py.test -v --durations=5 + +pypytest: pylint + pypy $$(which py.test) -v --durations=5 diff --git a/README.md b/README.md index 74a1ee3..ff856de 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ described by Aberth in "A method for exact computation with rational numbers". ## Installation +### JS + With [npm](https://www.npmjs.org/package/frac): $ npm install frac @@ -21,9 +23,15 @@ 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_FRAC` +### Python + +From [PyPI](https://pypi.python.org/pypi/frac): + + $ pip install frac + ## Usage -The exported `frac` function takes three arguments: +In all cases, the relevant function takes 3 arguments: - `x` the number we wish to approximate - `D` the maximum denominator @@ -33,18 +41,48 @@ The return value is an array of the form `[quot, num, den]` where `quot==0` for improper fractions. `quot <= x` for mixed fractions, which may lead to some unexpected results when rendering negative numbers. +### JS + +The exported `frac` function implements the Mediant method. + +`frac.cont` implements the Aberth algorithm + For example: -``` +```js > // var frac = require('frac'); // uncomment this line if in node -> frac(Math.PI,100); // [ 0, 22, 7 ] -> frac(Math.PI,100,true); // [ 3, 1, 7 ] -> frac(-Math.PI,100); // [ 0, -22, 7 ] -> frac(-Math.PI,100,true); // [ -4, 6, 7 ] // the approximation is (-4) + (6/7) +> frac(1.3, 9); // [ 0, 9, 7 ] // 1.3 ~ 9/7 +> frac(1.3, 9, true); // [ 1, 2, 7 ] // 1.3 ~ 1 + 2/7 +> frac(-1.3, 9); // [ 0, -9, 7 ] // -1.3 ~ -9/7 +> frac(-1.3, 9, true); // [ -2, 5, 7 ] // -1.3 ~ -2 + 5/7 + +> frac.cont(1.3, 9); // [ 0, 4, 3 ] // 1.3 ~ 4/3 +> frac.cont(1.3, 9, true); // [ 1, 1, 3 ] // 1.3 ~ 1 + 1/3 +> frac.cont(-1.3, 9); // [ 0, -4, 3 ] // -1.3 ~ -4/3 +> frac.cont(-1.3, 9, true); // [ -2, 2, 3 ] // -1.3 ~ -2 + 2/3 ``` -`frac.cont` implements the Aberth algorithm (input and output specifications -match the original `frac` function) + +### Python + +`frac.med` implements Mediant method. + +`frac.cont` implements Aberth algorithm + +For example: + +```py +>>> import frac +>>> frac.med(1.3, 9) # [ 0, 9, 7 ] +>>> frac.med(1.3, 9, True) # [ 1, 2, 7 ] +>>> frac.med(-1.3, 9) # [ 0, -9, 7 ] +>>> frac.med(-1.3, 9, True) # [ -2, 5, 7 ] + +>>> frac.cont(1.3, 9) # [ 0, 4, 3 ] +>>> frac.cont(1.3, 9, True) # [ 1, 1, 3 ] +>>> frac.cont(-1.3, 9) # [ 0, -4, 3 ] +>>> frac.cont(-1.3, 9, True) # [ -2, 2, 3 ] +``` ## Testing @@ -68,4 +106,4 @@ granted by the Apache 2.0 license are reserved by the Original Author. [![Coverage Status](http://img.shields.io/coveralls/SheetJS/frac/master.svg)](https://coveralls.io/r/SheetJS/frac?branch=master) -[![githalytics.com alpha](https://cruel-carlota.pagodabox.com/731e31b3a26382ccd5d213b9e74ea552 "githalytics.com")](http://githalytics.com/SheetJS/frac) +[![Analytics](https://ga-beacon.appspot.com/UA-36810333-1/SheetJS/frac?pixel)](https://github.com/SheetJS/frac) diff --git a/dist/frac.js b/dist/frac.js index 4412674..27a2330 100644 --- a/dist/frac.js +++ b/dist/frac.js @@ -33,8 +33,7 @@ frac.cont = function cont(x, D, mixed) { 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(Q > D) { if(Q_1 > D) { Q = Q_2; P = P_2; } else { Q = Q_1; P = P_1; } } if(!mixed) return [0, sgn * P, Q]; var q = Math.floor(sgn * P/Q); return [q, sgn*P - q*Q, Q]; diff --git a/dist/frac.min.js b/dist/frac.min.js index fb64e84..cf06dcf 100644 --- a/dist/frac.min.js +++ b/dist/frac.min.js @@ -1,2 +1,2 @@ /* frac.js (C) 2012-2015 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(xD){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=Math.floor(B);while(Q_1D){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"&&typeof DO_NOT_EXPORT_FRAC==="undefined")module.exports=frac; +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(xD){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=Math.floor(B);while(Q_1D){if(Q_1>D){Q=Q_2;P=P_2}else{Q=Q_1;P=P_1}}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"&&typeof DO_NOT_EXPORT_FRAC==="undefined")module.exports=frac; diff --git a/dist/frac.min.map b/dist/frac.min.map index 5e829ea..ae6bfaf 100644 --- a/dist/frac.min.map +++ b/dist/frac.min.map @@ -1 +1 @@ -{"version":3,"file":"dist/frac.min.js","sources":["frac.js"],"names":["frac","x","D","mixed","n1","Math","floor","d1","n2","d2","m","q","cont","sgn","B","P_2","P_1","P","Q_2","Q_1","Q","A","module","DO_NOT_EXPORT_FRAC","exports"],"mappings":";AACA,GAAIA,MAAO,SAASC,EAAGC,EAAGC,OACxB,GAAIC,IAAKC,KAAKC,MAAML,GAAIM,GAAK,CAC7B,IAAIC,IAAKJ,GAAG,EAAGK,GAAK,CACpB,IAAGR,IAAMG,GAAI,MAAMG,IAAML,GAAKO,IAAMP,EAAG,CACrC,GAAIQ,IAAKN,GAAKI,KAAOD,GAAKE,GAC1B,IAAGR,IAAMS,EAAG,CACV,GAAGH,GAAKE,IAAMP,EAAG,CAAEK,IAAIE,EAAIL,KAAII,EAAIC,IAAGP,EAAE,MACnC,IAAGK,GAAKE,GAAIA,GAAGP,EAAE,MACjBK,IAAGL,EAAE,CACV,WAEG,IAAGD,EAAIS,EAAG,CAAEF,GAAKJ,GAAGI,EAAIC,IAAKF,GAAGE,OAChC,CAAEL,GAAKA,GAAGI,EAAID,IAAKA,GAAGE,IAE7B,GAAGF,GAAKL,EAAG,CAAEK,GAAKE,EAAIL,IAAKI,GAC3B,IAAIL,MAAO,OAAQ,EAAGC,GAAIG,GAC1B,IAAII,GAAIN,KAAKC,MAAMF,GAAGG,GACtB,QAAQI,EAAGP,GAAKO,EAAEJ,GAAIA,IAExBP,MAAKY,KAAO,QAASA,MAAKX,EAAGC,EAAGC,OAC9B,GAAIU,KAAMZ,EAAI,GAAK,EAAI,CACvB,IAAIa,GAAIb,EAAIY,GACZ,IAAIE,KAAM,EAAGC,IAAM,EAAGC,EAAI,CAC1B,IAAIC,KAAM,EAAGC,IAAM,EAAGC,EAAI,CAC1B,IAAIC,GAAIhB,KAAKC,MAAMQ,EACnB,OAAMK,IAAMjB,EAAG,CACbmB,EAAIhB,KAAKC,MAAMQ,EACfG,GAAII,EAAIL,IAAMD,GACdK,GAAIC,EAAIF,IAAMD,GACd,IAAIJ,EAAIO,EAAK,MAAc,KAC3BP,GAAI,GAAKA,EAAIO,EACbN,KAAMC,GAAKA,KAAMC,CACjBC,KAAMC,GAAKA,KAAMC,EAEnB,GAAGA,EAAIlB,EAAG,CAAEkB,EAAID,GAAKF,GAAID,IACzB,GAAGI,EAAIlB,EAAG,CAAEkB,EAAIF,GAAKD,GAAIF,IACzB,IAAIZ,MAAO,OAAQ,EAAGU,IAAMI,EAAGG,EAC/B,IAAIT,GAAIN,KAAKC,MAAMO,IAAMI,EAAEG,EAC3B,QAAQT,EAAGE,IAAII,EAAIN,EAAES,EAAGA,GAE1B,UAAUE,UAAW,mBAAsBC,sBAAuB,YAAaD,OAAOE,QAAUxB"} \ No newline at end of file +{"version":3,"file":"dist/frac.min.js","sources":["frac.js"],"names":["frac","x","D","mixed","n1","Math","floor","d1","n2","d2","m","q","cont","sgn","B","P_2","P_1","P","Q_2","Q_1","Q","A","module","DO_NOT_EXPORT_FRAC","exports"],"mappings":";AACA,GAAIA,MAAO,SAASC,EAAGC,EAAGC,OACxB,GAAIC,IAAKC,KAAKC,MAAML,GAAIM,GAAK,CAC7B,IAAIC,IAAKJ,GAAG,EAAGK,GAAK,CACpB,IAAGR,IAAMG,GAAI,MAAMG,IAAML,GAAKO,IAAMP,EAAG,CACrC,GAAIQ,IAAKN,GAAKI,KAAOD,GAAKE,GAC1B,IAAGR,IAAMS,EAAG,CACV,GAAGH,GAAKE,IAAMP,EAAG,CAAEK,IAAIE,EAAIL,KAAII,EAAIC,IAAGP,EAAE,MACnC,IAAGK,GAAKE,GAAIA,GAAGP,EAAE,MACjBK,IAAGL,EAAE,CACV,WAEG,IAAGD,EAAIS,EAAG,CAAEF,GAAKJ,GAAGI,EAAIC,IAAKF,GAAGE,OAChC,CAAEL,GAAKA,GAAGI,EAAID,IAAKA,GAAGE,IAE7B,GAAGF,GAAKL,EAAG,CAAEK,GAAKE,EAAIL,IAAKI,GAC3B,IAAIL,MAAO,OAAQ,EAAGC,GAAIG,GAC1B,IAAII,GAAIN,KAAKC,MAAMF,GAAGG,GACtB,QAAQI,EAAGP,GAAKO,EAAEJ,GAAIA,IAExBP,MAAKY,KAAO,QAASA,MAAKX,EAAGC,EAAGC,OAC9B,GAAIU,KAAMZ,EAAI,GAAK,EAAI,CACvB,IAAIa,GAAIb,EAAIY,GACZ,IAAIE,KAAM,EAAGC,IAAM,EAAGC,EAAI,CAC1B,IAAIC,KAAM,EAAGC,IAAM,EAAGC,EAAI,CAC1B,IAAIC,GAAIhB,KAAKC,MAAMQ,EACnB,OAAMK,IAAMjB,EAAG,CACbmB,EAAIhB,KAAKC,MAAMQ,EACfG,GAAII,EAAIL,IAAMD,GACdK,GAAIC,EAAIF,IAAMD,GACd,IAAIJ,EAAIO,EAAK,MAAc,KAC3BP,GAAI,GAAKA,EAAIO,EACbN,KAAMC,GAAKA,KAAMC,CACjBC,KAAMC,GAAKA,KAAMC,EAEnB,GAAGA,EAAIlB,EAAG,CAAE,GAAGiB,IAAMjB,EAAG,CAAEkB,EAAIF,GAAKD,GAAIF,QAAY,CAAEK,EAAID,GAAKF,GAAID,KAClE,IAAIb,MAAO,OAAQ,EAAGU,IAAMI,EAAGG,EAC/B,IAAIT,GAAIN,KAAKC,MAAMO,IAAMI,EAAEG,EAC3B,QAAQT,EAAGE,IAAII,EAAIN,EAAES,EAAGA,GAE1B,UAAUE,UAAW,mBAAsBC,sBAAuB,YAAaD,OAAOE,QAAUxB"} \ No newline at end of file diff --git a/frac.flow.js b/frac.flow.js index b60e8a9..234aff9 100644 --- a/frac.flow.js +++ b/frac.flow.js @@ -33,8 +33,7 @@ frac.cont = function cont(x/*:number*/, D/*:number*/, mixed/*:?boolean*/)/*:Arra 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(Q > D) { if(Q_1 > D) { Q = Q_2; P = P_2; } else { Q = Q_1; P = P_1; } } if(!mixed) return [0, sgn * P, Q]; var q = Math.floor(sgn * P/Q); return [q, sgn*P - q*Q, Q]; diff --git a/frac.js b/frac.js index 4412674..27a2330 100644 --- a/frac.js +++ b/frac.js @@ -33,8 +33,7 @@ frac.cont = function cont(x, D, mixed) { 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(Q > D) { if(Q_1 > D) { Q = Q_2; P = P_2; } else { Q = Q_1; P = P_1; } } if(!mixed) return [0, sgn * P, Q]; var q = Math.floor(sgn * P/Q); return [q, sgn*P - q*Q, Q]; diff --git a/frac.md b/frac.md index fc9a89e..ce6abd5 100644 --- a/frac.md +++ b/frac.md @@ -147,11 +147,10 @@ At the end of each iteration, advance `k` by one step: } ``` -In case we end up overstepping, walk back an iteration or two: +In case we end up overstepping, walk back to the last valid iteration: ``` - if(Q > D) { Q = Q_1; P = P_1; } - if(Q > D) { Q = Q_2; P = P_2; } + if(Q > D) { if(Q_1 > D) { Q = Q_2; P = P_2; } else { Q = Q_1; P = P_1; } } ``` The final result is `r = (sgn x)p_k / q_k`: @@ -185,7 +184,7 @@ var xltestfiles=[ ]; function xlline(o,j,m,w) { - it(j, function(done) { + it(j, function() { var d, q, qq; for(var i = j*w; i < m-3 && i < (j+1)*w; ++i) { d = o[i].split("\t"); @@ -202,7 +201,6 @@ function xlline(o,j,m,w) { 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 parsexl(f,w) { @@ -241,7 +239,7 @@ xltestfiles.forEach(function(x) { ```json>package.json { "name": "frac", - "version": "1.0.1", + "version": "1.0.2", "author": "SheetJS", "description": "Rational approximation with bounded denominator", "keywords": [ "math", "fraction", "rational", "approximation" ], diff --git a/frac.py b/frac.py index af8d5d0..49771d7 100644 --- a/frac.py +++ b/frac.py @@ -16,18 +16,16 @@ All functions take 3 arguments: The return value is a list of 3 elements: [quotient, numerator, denominator] """ -import math as Math - def med(x, D, mixed=False): """Generate fraction representation using Mediant method""" - n1, d1 = int(Math.floor(x)), 1 + n1, d1 = int(x), 1 n2, d2 = n1+1, 1 m = 0. if x != n1: while d1 <= D and d2 <= D: m = float(n1 + n2) / (d1 + d2) - if(x == m): + if x == m: if d1 + d2 <= D: n1, d1 = n1 + n2, d1 + d2 d2 = D + 1 @@ -50,23 +48,24 @@ def med(x, D, mixed=False): def cont(x, D, mixed=False): """Generate fraction representation using Aberth method""" - sgn = -1 if x < 0 else 1 B = abs(x) - P_2, P_1, P = 0, 1, 0 - Q_2, Q_1, Q = 1, 0, 0 + I = int + P_2, P_1, P, Q_2, Q_1, Q = 0, 1, 0, 1, 0, 0 while Q_1 < D: - A = int(Math.floor(B)) + A = I(B) P = A * P_1 + P_2 Q = A * Q_1 + Q_2 - if ((B - A) < 0.0000000005): + 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): - P, Q = P_1, Q_1 - if(Q > D): - P, Q = P_2, Q_2 + if Q > D: + if Q_1 <= D: + P, Q = P_1, Q_1 + else: + P, Q = P_2, Q_2 + sgn = -1 if x < 0 else 1 if not mixed: return [0, sgn * P, Q] q = divmod(sgn * P, Q) diff --git a/package.json b/package.json index 6d39806..c9831fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "frac", - "version": "1.0.1", + "version": "1.0.2", "author": "SheetJS", "description": "Rational approximation with bounded denominator", "keywords": [ "math", "fraction", "rational", "approximation" ], diff --git a/setup.py b/setup.py index 84fc761..19bd1c8 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from distutils.core import setup setup( name='frac', - version='0.0.1', + version='1.0.2', author='SheetJS', author_email='dev@sheetjs.com', url='http://oss.sheetjs.com/frac', @@ -20,9 +20,10 @@ This module can generate fraction representations using: license = "Apache-2.0", keywords = ['frac', 'fraction', 'rational', 'approximation'], classifiers = [ - 'Development Status :: 3 - Alpha', + 'Development Status :: 5 - Production/Stable' 'License :: OSI Approved :: Apache Software License', - 'Topic :: Office/Business', + 'Operating System :: OS Independent', + 'Topic :: Scientific/Engineering :: Mathematics', 'Topic :: Utilities', ] ) diff --git a/test.js b/test.js index c5a2a2f..221cf91 100644 --- a/test.js +++ b/test.js @@ -10,7 +10,7 @@ var xltestfiles=[ ]; function xlline(o,j,m,w) { - it(j, function(done) { + it(j, function() { var d, q, qq; for(var i = j*w; i < m-3 && i < (j+1)*w; ++i) { d = o[i].split("\t"); @@ -27,7 +27,6 @@ function xlline(o,j,m,w) { 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 parsexl(f,w) { diff --git a/test_all.py b/test_all.py new file mode 100644 index 0000000..93e05ff --- /dev/null +++ b/test_all.py @@ -0,0 +1,105 @@ +import frac +import pytest + +xltestfiles = [ + ['xl.00001.tsv', 10000], + ['xl.0001.tsv', 10000], + ['xl.001.tsv', 10000], + ['xl.01.tsv', 10000], + ['oddities.tsv', 25] +] + +mediant_tests = [ + [0.1, 9, False, 0, 1, 9], + [0.2, 9, False, 0, 1, 5], + [0.3, 9, False, 0, 2, 7], + [0.4, 9, False, 0, 2, 5], + [0.5, 9, False, 0, 1, 2], + [0.6, 9, False, 0, 3, 5], + [0.7, 9, False, 0, 5, 7], + [0.8, 9, False, 0, 4, 5], + [0.9, 9, False, 0, 8, 9], + [1.0, 9, False, 0, 1, 1], + [1.0, 9, True, 1, 0, 1], + [1.7, 9, True, 1, 5, 7], + [1.7, 9, False, 0, 12, 7] +] + + +def test_mediant_tenths(): + for t in mediant_tests: + assert frac.med(t[0], t[1], t[2]) == [t[3], t[4], t[5]] + + +def make9(q): + S = str + if q[0] or q[1]: + qq = (S(q[0]) if q[0] else "") + qq += " " + qq += ("/".join([S(q[1]), S(q[2])]) if q[1] else " ") + return qq + else: + return "0 " + + +def make99(q): + S = str + if q[0] or q[1]: + qq = (S(q[0]) if q[0] else "") + qq += " " + if q[1]: + if q[1] < 10: + qq += " " + qq += ("/".join([S(q[1]), S(q[2])])) + if q[2] < 10: + qq += " " + else: + qq += " " + return qq + else: + return "0 " + + +def make999(q): + S = str + if q[0] or q[1]: + qq = (S(q[0]) if q[0] else "") + qq += " " + if q[1]: + if q[1] < 10: + qq += " " + elif q[1] < 100: + qq += " " + qq += "/".join([S(q[1]), S(q[2])]) + if q[2] < 10: + qq += " " + elif q[2] < 100: + qq += " " + else: + qq += " " + return qq + else: + return "0 " + + +def xlline(o, j, m, w): + for i in xrange(j*w, min(m-3, (j+1) * w)): + d = o[i].split("\t") + dd = float(d[0]) + assert make9(frac.cont(dd, 9, True)) == d[1] + assert make99(frac.cont(dd, 99, True)) == d[2] + assert make999(frac.cont(dd, 999, True)) == d[3] + + +def parsexl(f, w): + oo = open(f) + d = oo.readlines() + o = filter(lambda x: len(x) > 0, map(lambda x: x.strip("\n"), d)) + m = len(o)-3 + for j in xrange(int(m/w)): + xlline(o, j, m, w) + + +@pytest.mark.parametrize("filename,step", xltestfiles) +def test_file(filename, step): + parsexl('./test_files/' + filename, step)