From 89a840693b7d79a46cb3e16a3bd5750096c70dff Mon Sep 17 00:00:00 2001 From: Yarchik Date: Thu, 18 Jun 2026 13:15:04 +0100 Subject: [PATCH] fix: round ties towards +Infinity in roundTo to match round() `roundTo` rounded an exact half by magnitude (half away from zero), while `round` rounds half towards +Infinity (mirroring Math.round, as documented). Because rounding to the nearest multiple of 1 is the same as integer rounding, `x.round()` and `x.roundTo(1)` must agree, but they disagreed on every negative half: new Fraction(-1, 2).round() // 0 new Fraction(-1, 2).roundTo(1) // -1 (now 0) new Fraction(-3, 2).round() // -1 new Fraction(-3, 2).roundTo(1) // -2 (now -1) Apply the same tie-breaking `round` uses: only round a half up when the value is non-negative, so the half is broken towards +Infinity regardless of the multiple. --- dist/fraction.js | 5 +++-- dist/fraction.min.js | 2 +- dist/fraction.mjs | 5 +++-- src/fraction.js | 5 +++-- tests/fraction.test.js | 19 +++++++++++++++++++ 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/dist/fraction.js b/dist/fraction.js index 816d5db..2de186d 100644 --- a/dist/fraction.js +++ b/dist/fraction.js @@ -856,9 +856,10 @@ Fraction.prototype = { const d = this['d'] * P['n']; const r = n % d; - // round(n / d) = ifloor(n / d) + 2(n % d) >= d ? 1 : 0 + // Break ties towards +Infinity, like round(): round up on an exact half + // only when the value is non-negative, so roundTo(1) matches round(). let k = ifloor(n / d); - if (r + r >= d) { + if ((this['s'] >= C_ZERO ? C_ONE : C_ZERO) + r + r > d) { k++; } return newFraction(this['s'] * k * P['n'], P['d']); diff --git a/dist/fraction.min.js b/dist/fraction.min.js index 97b02ee..15c872a 100644 --- a/dist/fraction.min.js +++ b/dist/fraction.min.js @@ -15,7 +15,7 @@ this.n*e.n,this.d*e.d)},div:function(a,b){q(a,b);return n(this.s*e.s*this.n*e.d, this.d,this.n)},pow:function(a,b){q(a,b);if(e.d===h)return e.se.s*e.n*this.d},gte:function(a,b){q(a,b);return this.s*this.n*e.d>=e.s*e.n*this.d},compare:function(a,b){q(a,b);a=this.s*this.n*e.d-e.s*e.n*this.d;return(gg&&this.s>=g?h:g),a)},floor:function(a){a=t**BigInt(a||0);return n(u(this.s*a*this.n/ -this.d)-(a*this.n%this.d>g&&this.s=g?h:g)+a*this.n%this.d*p>this.d?h:g),a)},roundTo:function(a,b){q(a,b);var d=this.n*e.d;a=this.d*e.n;b=d%a;d=u(d/a);b+b>=a&&d++;return n(this.s*d*e.n,e.d)},divisible:function(a,b){q(a,b);return e.n===g?!1:this.n*e.d%(e.n*this.d)===g},valueOf:function(){return Number(this.s*this.n)/Number(this.d)},toString:function(a=15){let b=this.n,d=this.d;var c;a:{for(c=d;c%p===g;c/= +this.d)-(a*this.n%this.d>g&&this.s=g?h:g)+a*this.n%this.d*p>this.d?h:g),a)},roundTo:function(a,b){q(a,b);var d=this.n*e.d;a=this.d*e.n;b=d%a;d=u(d/a);(this.s>=g?h:g)+b+b>a&&d++;return n(this.s*d*e.n,e.d)},divisible:function(a,b){q(a,b);return e.n===g?!1:this.n*e.d%(e.n*this.d)===g},valueOf:function(){return Number(this.s*this.n)/Number(this.d)},toString:function(a=15){let b=this.n,d=this.d;var c;a:{for(c=d;c%p===g;c/= p);for(;c%z===g;c/=z);if(c===h)c=g;else{for(var f=t%c,k=1;f!==h;k++)if(f=f*t%c,2E3g;k=k*k%d,l>>=h)l&h&&(m=m*k%d);k=m;for(l=0;300>l;l++){if(f===k){f=BigInt(l);break a}f=f*t%d;k=k*t%d}f=0}k=f;f=this.sg&&(c+=f,c+=" ",b%=d);c=c+b+"/"+d}return c},toLatex:function(a=!1){let b=this.n,d=this.d,c=this.sg&&(c+=f,b%=d);c=c+"\\frac{"+b+"}{"+d;c+="}"}return c},toContinued:function(){let a=this.n,b=this.d;const d=[];for(;b;){d.push(u(a/b));const c=a%b;a=b;b=c}return d},simplify:function(a=.001){a=BigInt(Math.ceil(1/a));const b=this.abs(),d=b.toContinued();for(let f=1;f= d ? 1 : 0 + // Break ties towards +Infinity, like round(): round up on an exact half + // only when the value is non-negative, so roundTo(1) matches round(). let k = ifloor(n / d); - if (r + r >= d) { + if ((this['s'] >= C_ZERO ? C_ONE : C_ZERO) + r + r > d) { k++; } return newFraction(this['s'] * k * P['n'], P['d']); diff --git a/src/fraction.js b/src/fraction.js index 4292c48..06c642e 100644 --- a/src/fraction.js +++ b/src/fraction.js @@ -862,9 +862,10 @@ Fraction.prototype = { const d = this['d'] * P['n']; const r = n % d; - // round(n / d) = ifloor(n / d) + 2(n % d) >= d ? 1 : 0 + // Break ties towards +Infinity, like round(): round up on an exact half + // only when the value is non-negative, so roundTo(1) matches round(). let k = ifloor(n / d); - if (r + r >= d) { + if ((this['s'] >= C_ZERO ? C_ONE : C_ZERO) + r + r > d) { k++; } return newFraction(this['s'] * k * P['n'], P['d']); diff --git a/tests/fraction.test.js b/tests/fraction.test.js index b65833c..8d85edf 100644 --- a/tests/fraction.test.js +++ b/tests/fraction.test.js @@ -1518,6 +1518,25 @@ var tests = [{ fn: "roundTo", param: "1/2", expect: "-3.5" +}, { + // a tie must round the same way as round() (half towards +Infinity) + label: "-1/2 round to multiple of 1", + set: "-1/2", + fn: "roundTo", + param: "1", + expect: "0" +}, { + label: "-3/2 round to multiple of 1", + set: "-3/2", + fn: "roundTo", + param: "1", + expect: "-1" +}, { + label: "-1/4 round to multiple of 1/2", + set: "-1/4", + fn: "roundTo", + param: "1/2", + expect: "0" }, { label: "log_2(8)", set: "8",