From 8f8fcfeb795b4bb385bbb627a0169b4967f4fdb6 Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Tue, 27 Feb 2024 17:09:45 +0200 Subject: [PATCH] Fix safe limit blowing with moderately complex fractions --- src/__tests__/fraction.spec.ts | 53 ++++++++++++++++++++++++++++ src/fraction.ts | 64 +++++++++++++++++++++++++--------- 2 files changed, 100 insertions(+), 17 deletions(-) diff --git a/src/__tests__/fraction.spec.ts b/src/__tests__/fraction.spec.ts index 9ee9f94..653ff89 100644 --- a/src/__tests__/fraction.spec.ts +++ b/src/__tests__/fraction.spec.ts @@ -447,4 +447,57 @@ describe('Fraction', () => { ); expect(one.equals(1)).toBe(true); }); + + it('adds terms with large denominators', () => { + const a = new Fraction('123456789/94906267'); + const b = new Fraction('987654321/94906267'); + expect(a.add(b).equals('1111111110/94906267')).toBe(true); + }); + + it('subtracts terms with large denominators', () => { + const a = new Fraction('987654321/94906267'); + const b = new Fraction('123456789/94906267'); + expect(a.sub(b).equals('864197532/94906267')); + }); + + it('lens-adds terms with large numerators', () => { + const a = new Fraction('94906267/123456789'); + const b = new Fraction('94906267/987654321'); + expect(a.lensAdd(b).equals('94906267/1111111110')).toBe(true); + }); + + it('lens-subtracts terms with large numerators', () => { + const a = new Fraction('94906267/123456789'); + const b = new Fraction('94906267/987654321'); + expect(a.lensSub(b).equals('-94906267/864197532')).toBe(true); + }); + + it('mods terms with large denominators', () => { + const a = new Fraction('123456789/94906267'); + const b = new Fraction('987654321/94906267'); + expect(b.mod(a).equals('9/94906267')).toBe(true); + }); + + it('mmods terms with large denominators', () => { + const a = new Fraction('123456789/94906267'); + const b = new Fraction('987654321/94906267'); + expect(b.mmod(a).equals('9/94906267')).toBe(true); + }); + + it('checks divisibility of complex fractions', () => { + const a = new Fraction('123456789/94906267'); + expect(a.mul(21).divisible(a)).toBe(true); + }); + + it('computes gcd of factors with large denominators', () => { + const a = new Fraction('123456789/94906267'); + const b = new Fraction('987654321/94906267'); + expect(a.gcd(b).equals('9/94906267')).toBe(true); + }); + + it('computes lcm of factors with with large numerators', () => { + const a = new Fraction('94906267/123456789'); + const b = new Fraction('94906267/987654321'); + expect(a.lcm(b).equals('94906267/9')).toBe(true); + }); }); diff --git a/src/fraction.ts b/src/fraction.ts index f4f6415..c4c88b6 100644 --- a/src/fraction.ts +++ b/src/fraction.ts @@ -480,7 +480,12 @@ export class Fraction { **/ add(other: FractionValue) { const {s, n, d} = new Fraction(other); - return new Fraction(this.s * this.n * d + s * n * this.d, this.d * d); + // Must pre-reduce to avoid blowing the limits + const denominator = lcm(this.d, d); + return new Fraction( + this.s * this.n * (denominator / this.d) + s * n * (denominator / d), + denominator + ); } /** @@ -490,7 +495,12 @@ export class Fraction { **/ sub(other: FractionValue) { const {s, n, d} = new Fraction(other); - return new Fraction(this.s * this.n * d - s * n * this.d, this.d * d); + // Must pre-reduce to avoid blowing the limits + const denominator = lcm(this.d, d); + return new Fraction( + this.s * this.n * (denominator / this.d) - s * n * (denominator / d), + denominator + ); } /** @@ -500,11 +510,16 @@ export class Fraction { */ lensAdd(other: FractionValue) { const {s, n, d} = new Fraction(other); - if (!n) { + if (!n || !this.n) { // Based on behavior in the limit where both terms become zero. return new Fraction({s: 0, n: 0, d: 1}); } - return new Fraction(this.s * this.n * s * n, this.n * d + n * this.d); + // Must pre-reduce to avoid blowing the limits + const numerator = lcm(this.n, n); + return new Fraction( + this.s * s * numerator, + (numerator / n) * d + (numerator / this.n) * this.d + ); } /** @@ -514,11 +529,16 @@ export class Fraction { */ lensSub(other: FractionValue) { const {s, n, d} = new Fraction(other); - if (!n) { + if (!n || !this.n) { // Based on behavior in the limit where both terms become zero. return new Fraction({s: 0, n: 0, d: 1}); } - return new Fraction(this.s * this.n * s * n, n * this.d - this.n * d); + // Must pre-reduce to avoid blowing the limits + const numerator = lcm(this.n, n); + return new Fraction( + this.s * s * numerator, + (numerator / this.n) * this.d - (numerator / n) * d + ); } /** @@ -562,10 +582,12 @@ export class Fraction { * Ex: new Fraction("4.'3'").mod("7/8") => (13/3) % (7/8) = (5/6) **/ mod(other: FractionValue) { - other = new Fraction(other); + const {n, d} = new Fraction(other); + // Must pre-reduce to avoid blowing the limits + const denominator = lcm(this.d, d); return new Fraction( - (this.s * (other.d * this.n)) % (other.n * this.d), - this.d * other.d + (this.s * ((denominator / this.d) * this.n)) % (n * (denominator / d)), + denominator ); } @@ -575,10 +597,12 @@ export class Fraction { * Ex: new Fraction("-4.'3'").mmod("7/8") => (-13/3) % (7/8) = (1/24) **/ mmod(other: FractionValue) { - other = new Fraction(other); + const {n, d} = new Fraction(other); + // Must pre-reduce to avoid blowing the limits + const denominator = lcm(this.d, d); return new Fraction( - mmod(this.s * (other.d, this.n), other.n * this.d), - this.d * other.d + mmod(this.s * ((denominator / this.d) * this.n), n * (denominator / d)), + denominator ); } @@ -705,8 +729,14 @@ export class Fraction { */ divisible(other: FractionValue) { try { - other = new Fraction(other); - return !(!(other.n * this.d) || (this.n * other.d) % (other.n * this.d)); + const {n, d} = new Fraction(other); + const nFactor = gcd(this.n, n); + const dFactor = gcd(this.d, d); + return !( + !n || + ((this.n / nFactor) * (d / dFactor)) % + ((n / nFactor) * (this.d / dFactor)) + ); } catch { return false; } @@ -719,7 +749,7 @@ export class Fraction { */ gcd(other: FractionValue) { const {n, d} = new Fraction(other); - return new Fraction(gcd(n, this.n) * gcd(d, this.d), d * this.d); + return new Fraction(gcd(n, this.n), lcm(this.d, d)); } /** @@ -729,10 +759,10 @@ export class Fraction { */ lcm(other: FractionValue) { const {n, d} = new Fraction(other); - if (n === 0 && this.n === 0) { + if (!n && !this.n) { return new Fraction({s: 0, n: 0, d: 1}); } - return new Fraction(n * this.n, gcd(n, this.n) * gcd(d, this.d)); + return new Fraction(lcm(n, this.n), gcd(d, this.d)); } /**