From 1f6492468e00386b9def94b3d4b1803d763c62ce Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Sat, 20 Apr 2024 13:11:12 +0300 Subject: [PATCH] Finagle gcd and lcm to satisfy a multiplicative identity Replicate something similar for radical/radicand analogues. Extend gcd, lcm and mmod to bigints. Fix zero fraction normalization. ref #27 --- src/__tests__/fraction-rawify.spec.ts | 4 +- src/__tests__/fraction.spec.ts | 155 +++++++++++++++++++++++++- src/fraction.ts | 63 +++++++++-- 3 files changed, 204 insertions(+), 18 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..5f61014 100644 --- a/src/__tests__/fraction.spec.ts +++ b/src/__tests__/fraction.spec.ts @@ -2,21 +2,82 @@ import {describe, it, expect} from 'vitest'; import {Fraction, gcd, lcm, mmod} from '../fraction'; describe('gcd', () => { - it('can find the greates common divisor of 12 and 15', () => { + it('can find the greatest common divisor of 12 and 15 (number)', () => { expect(gcd(12, 15)).toBe(3); }); + + it('can find the greatest common divisor of 12 and 15 (bigint)', () => { + expect(gcd(12n, 15n)).toBe(3n); + }); + + 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); + }); + + it('has an identity element (bigint)', () => { + expect(gcd(12n, 0n)).toBe(12n); + }); }); describe('lcm', () => { - it('can find the least common multiple of 6 and 14', () => { + it('can find the least common multiple of 6 and 14 (number)', () => { expect(lcm(6, 14)).toBe(42); }); + + it('can find the least common multiple of 6 and 14 (bigint)', () => { + expect(lcm(6n, 14n)).toBe(42n); + }); + + 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); + }); + + it('works with zero (bigint)', () => { + expect(lcm(0n, 12n)).toBe(0n); + }); +}); + +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 + ); + // This works, though. + const x = BigInt(i); + const y = BigInt(j); + expect(gcd(x, y) * lcm(x, y)).toBe(x * y); + } + } + }); }); describe('mmod', () => { - it('works with negative numbers', () => { + it('works with negative numbers (number)', () => { expect(mmod(-5, 3)).toBe(1); }); + + it('works with negative numbers (bigint)', () => { + expect(mmod(-5n, 3n)).toBe(1n); + }); }); describe('Fraction', () => { @@ -303,6 +364,21 @@ describe('Fraction', () => { expect(fraction.gcr(Math.random())).toBeNull(); }); + it('treats unity as the identity in geometric gcd (left)', () => { + const fraction = new Fraction(12); + expect(fraction.gcr(1)!.equals(12)).toBe(true); + }); + + it('treats unity as the identity in geometric gcd (right)', () => { + const fraction = new Fraction(1); + expect(fraction.gcr(12)!.equals(12)).toBe(true); + }); + + it('treats unity as the identity in 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 +432,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 is subunitary for a subunitary argument 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 +623,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..ee8bcd4 100644 --- a/src/fraction.ts +++ b/src/fraction.ts @@ -14,16 +14,25 @@ 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. */ -export function gcd(a: number, b: number): number { +export function gcd(a: number, b: number): number; +export function gcd(a: bigint, b: bigint): bigint; +export function gcd(a: number | bigint, b: typeof a): typeof a { if (!a) return b; if (!b) return a; while (true) { + // XXX: TypeScript trips up here for no reason. + // @ts-ignore a %= b; if (!a) return b; + // @ts-ignore b %= a; if (!b) return a; } @@ -31,12 +40,19 @@ 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); +export function lcm(a: number, b: number): number; +export function lcm(a: bigint, b: bigint): bigint; +export function lcm(a: number | bigint, b: typeof a): typeof a { + // @ts-ignore + return a ? (a / gcd(a, b)) * b : typeof a === 'number' ? 0 : 0n; } /** @@ -45,7 +61,10 @@ export function lcm(a: number, b: number): number { * @param b The divisor. * @returns The remainder of Euclidean division of a by b. */ -export function mmod(a: number, b: number) { +export function mmod(a: number, b: number): number; +export function mmod(a: bigint, b: bigint): bigint; +export function mmod(a: number | bigint, b: typeof a): typeof a { + // @ts-ignore return ((a % b) + b) % b; } @@ -193,11 +212,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 +881,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 +896,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 +1007,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 +1051,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 +1091,8 @@ export class Fraction { /** * Calculate the least common radicand between two rational numbers if it exists. * + * If either of the inputs is unitary returns unity (1). + * * Examples: * ```ts * new Fraction(8).lcr(4) // 64 @@ -1069,10 +1105,13 @@ export class Fraction { if (radical === null) { return null; } + if (radical.isUnity()) { + return new Fraction(1); + } const base = 1 / 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); } /**