From faedaf6d81a312d377d2d05bfb3d4690c82241b4 Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Sat, 20 Apr 2024 13:11:12 +0300 Subject: [PATCH] WIP: Finagle gcd and lcm to satisfy a multiplicative identity Replicate something similar for radicand analogues. Fix zero fraction normalization. ref #27 --- src/__tests__/fraction-rawify.spec.ts | 4 +- src/__tests__/fraction.spec.ts | 125 +++++++++++++++++++++++++- src/fraction.ts | 48 +++++++--- 3 files changed, 164 insertions(+), 13 deletions(-) diff --git a/src/__tests__/fraction-rawify.spec.ts b/src/__tests__/fraction-rawify.spec.ts index 0febcb2..be8a15a 100644 --- a/src/__tests__/fraction-rawify.spec.ts +++ b/src/__tests__/fraction-rawify.spec.ts @@ -989,14 +989,14 @@ const tests = [ set: -3, fn: 'lcm', param: 3, - expect: '3', + expect: '-3', // Deviates from rawify's convention }, { label: 'lcm(3,-3)', set: 3, fn: 'lcm', param: -3, - expect: '3', + expect: '-3', // Deviates from rawify's convention }, { label: 'lcm(0,3)', diff --git a/src/__tests__/fraction.spec.ts b/src/__tests__/fraction.spec.ts index 322f720..9e95f88 100644 --- a/src/__tests__/fraction.spec.ts +++ b/src/__tests__/fraction.spec.ts @@ -5,12 +5,49 @@ describe('gcd', () => { it('can find the greates common divisor of 12 and 15', () => { expect(gcd(12, 15)).toBe(3); }); + + it('has an identity element (left)', () => { + expect(gcd(12, 0)).toBe(12); + }); + + it('has an identity element (right)', () => { + expect(gcd(0, 12)).toBe(12); + }); + + it('has an identity element (self)', () => { + expect(gcd(0, 0)).toBe(0); + }); }); describe('lcm', () => { it('can find the least common multiple of 6 and 14', () => { expect(lcm(6, 14)).toBe(42); }); + + it('works with zero (left)', () => { + expect(lcm(0, 12)).toBe(0); + }); + + it('works with zero (right)', () => { + expect(lcm(12, 0)).toBe(0); + }); + + it('works with zero (both)', () => { + expect(lcm(0, 0)).toBe(0); + }); +}); + +describe('gcd with lcm', () => { + it('satisfies the identity for small integers', () => { + for (let i = -10; i <= 10; ++i) { + for (let j = -10; j <= 10; ++j) { + // We need to bypass (+0).toBe(-0) here... + expect(gcd(i, j) * lcm(i, j) === i * j, `failed with ${i}, ${j}`).toBe( + true + ); + } + } + }); }); describe('mmod', () => { @@ -303,6 +340,21 @@ describe('Fraction', () => { expect(fraction.gcr(Math.random())).toBeNull(); }); + it('treats unity as the identity if geometric gcd (left)', () => { + const fraction = new Fraction(12); + expect(fraction.gcr(1)!.equals(12)).toBe(true); + }); + + it('treats unity as the identity if geometric gcd (right)', () => { + const fraction = new Fraction(1); + expect(fraction.gcr(12)!.equals(12)).toBe(true); + }); + + it('treats unity as the identity if geometric gcd (self)', () => { + const fraction = new Fraction(1); + expect(fraction.gcr(1)!.equals(1)).toBe(true); + }); + it('has logdivision (integers)', () => { const fraction = new Fraction(9); expect(fraction.log(3)!.equals(2)).toBe(true); @@ -356,7 +408,42 @@ describe('Fraction', () => { it('has a geometric lcm (fractions)', () => { const fraction = new Fraction(9, 16); - expect(fraction.lcr('64/27')!.equals('4096/729')).toBe(true); + // The result for a subunitary argument is subunitary by convention. + expect(fraction.lcr('64/27')!.equals('729/4096')).toBe(true); + }); + + it('satisfies the identity for small integers when it exists', () => { + for (let i = 1; i <= 10; ++i) { + for (let j = 1; j <= 10; ++j) { + const gcr = new Fraction(i).gcr(j); + if (gcr === null) { + continue; + } + const lcr = new Fraction(i).lcr(j); + expect(lcr!.log(i)!.equals(new Fraction(j).log(gcr)!)).toBe(true); + } + } + }); + + it('satisfies the identity between a small integer and a particular when it exists', () => { + for (let i = 1; i <= 10; ++i) { + const particular = new Fraction(i).inverse(); + // Starting from 2 to avoid logdivision by unity. + for (let j = 2; j <= 10; ++j) { + const gcr = particular.gcr(j); + if (gcr === null) { + continue; + } + const lcr = particular.lcr(j)!; + expect(lcr.log(particular)!.equals(new Fraction(j).log(gcr)!)).toBe( + true + ); + + expect(new Fraction(j).gcr(particular)!.equals(gcr)).toBe(true); + expect(new Fraction(j).lcr(particular)!.equals(lcr)).toBe(true); + expect(lcr!.log(j)!.equals(particular.log(gcr)!)).toBe(true); + } + } }); it('has geometric rounding (integers)', () => { @@ -512,4 +599,40 @@ describe('Fraction', () => { const b = new Fraction('94906267/987654321'); expect(a.lcm(b).equals('94906267/9')).toBe(true); }); + + it('satisfies the multiplicative identity between gcd and lcm for small integers', () => { + for (let i = -10; i <= 10; ++i) { + for (let j = -10; j <= 10; ++j) { + const f = new Fraction(i); + expect( + f + .gcd(j) + .mul(f.lcm(j)) + .equals(i * j), + `failed with ${i}, ${j}` + ).toBe(true); + } + } + }); + + it('normalizes zero (integer)', () => { + const fraction = new Fraction(0); + expect(fraction.s).toBe(0); + expect(fraction.n).toBe(0); + expect(fraction.d).toBe(1); + }); + + it('normalizes zero (numerator)', () => { + const fraction = new Fraction({n: -0, d: 1}); + expect(fraction.s).toBe(0); + expect(fraction.n).toBe(0); + expect(fraction.d).toBe(1); + }); + + it('normalizes zero (denominator)', () => { + const fraction = new Fraction({n: 0, d: -1}); + expect(fraction.s).toBe(0); + expect(fraction.n).toBe(0); + expect(fraction.d).toBe(1); + }); }); diff --git a/src/fraction.ts b/src/fraction.ts index 8500f8c..a9416e5 100644 --- a/src/fraction.ts +++ b/src/fraction.ts @@ -14,6 +14,10 @@ const MAX_CYCLE_LENGTH = 128; /** * Greatest common divisor of two integers. + * + * Zero is treated as the identity element: gcd(0, x) = gcd(x, 0) = x + * + * The sign of the result is essentially random for negative inputs. * @param a The first integer. * @param b The second integer. * @returns The largest integer that divides a and b. @@ -31,12 +35,16 @@ export function gcd(a: number, b: number): number { /** * Least common multiple of two integers. + * + * Return zero if either of the arguments is zero. + * + * Satisfies a * b = gcd * lcm. See {@link gcd} for consequences on negative inputs. * @param a The first integer. * @param b The second integer. * @returns The smallest integer that both a and b divide. */ export function lcm(a: number, b: number): number { - return (Math.abs(a) / gcd(a, b)) * Math.abs(b); + return a ? (a / gcd(a, b)) * b : 0; } /** @@ -193,11 +201,13 @@ export class Fraction { } else { this.s = 1; } - if (numerator.n < 0) { + if (numerator.d < 0) { this.s = -this.s; } - if (numerator.d < 0) { + if (numerator.n < 0) { this.s = -this.s; + } else if (numerator.n === 0) { + this.s = 0; } this.n = Math.abs(numerator.n); this.d = Math.abs(numerator.d); @@ -860,6 +870,8 @@ export class Fraction { /** * Calculates the fractional gcd of two rational numbers. (i.e. both this and other is divisible by the result) * + * Always returns a non-negative result. + * * Example: * ```ts * new Fraction(5,8).gcd("3/7") // 1/56 @@ -873,17 +885,18 @@ export class Fraction { /** * Calculates the fractional lcm of two rational numbers. (i.e. the result is divisible by both this and other) * + * Has the same sign as the product of the rational numbers. + * * Example: * ```ts * new Fraction(5,8).gcd("3/7") // 15 * ``` */ lcm(other: FractionValue) { - const {n, d} = new Fraction(other); - if (!n && !this.n) { - return new Fraction({s: 0, n: 0, d: 1}); - } - return new Fraction(lcm(n, this.n), gcd(d, this.d)); + const {s, n, d} = new Fraction(other); + const result = new Fraction(lcm(n, this.n), gcd(d, this.d)); + result.s = this.s * s; + return result; } /** @@ -983,6 +996,8 @@ export class Fraction { /** * Calculate the greatest common radical between two rational numbers if it exists. * + * Treats unity as the identity element: gcr(1, x) = gcr(x, 1) = x + * * Examples: * ```ts * new Fraction(8).gcr(4) // 2 @@ -1025,6 +1040,14 @@ export class Fraction { */ log(other: FractionValue, maxIter = 100) { const other_ = new Fraction(other); + if (other_.isUnity()) { + if (this.isUnity()) { + // This convention follows from an identity between gcr and lcr + // Not entirely well-founded, but not entirely wrong either. + return new Fraction(1); + } + return null; + } const radical = this.gcr(other_, maxIter); if (radical === null) { return null; @@ -1057,6 +1080,8 @@ export class Fraction { /** * Calculate the least common radicand between two rational numbers if it exists. * + * If either of the inputs is unitary return unity (1). + * * Examples: * ```ts * new Fraction(8).lcr(4) // 64 @@ -1069,10 +1094,13 @@ export class Fraction { if (radical === null) { return null; } - const base = 1 / Math.log(radical.n / radical.d); + if (radical.isUnity()) { + return new Fraction(1); + } + const base = 1 / Math.abs(Math.log(radical.n / radical.d)); const n = Math.round(Math.log(this.n / this.d) * base); const d = Math.round(Math.log(other_.n / other_.d) * base); - return radical.pow(Math.abs(n * d)); + return radical.pow(n * d); } /**