Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finagle gcd and lcm to satisfy a multiplicative identity #28

Merged
merged 1 commit into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/__tests__/fraction-rawify.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
Expand Down
170 changes: 166 additions & 4 deletions src/__tests__/fraction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -356,7 +432,57 @@ 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('has geometric lcm that works with unity (left)', () => {
const fraction = new Fraction(1, 2);
expect(fraction.lcr(1)!.equals(1)).toBe(true);
});

it('has geometric lcm that works with unity (right)', () => {
const fraction = new Fraction(1);
expect(fraction.lcr(2)!.equals(1)).toBe(true);
});

it('has geometric lcm that works with unity (both)', () => {
const fraction = new Fraction(1);
expect(fraction.lcr(1)!.equals(1)).toBe(true);
});

it('satisfies the gcr/lcr 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 gcr/lcr 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)', () => {
Expand Down Expand Up @@ -512,4 +638,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);
});
});
67 changes: 55 additions & 12 deletions src/fraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,45 @@ 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;
}
}

/**
* 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 : a;
}

/**
Expand All @@ -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;
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -983,6 +1007,10 @@ export class Fraction {
/**
* Calculate the greatest common radical between two rational numbers if it exists.
*
* Never returns a subunitary result.
*
* Treats unity as the identity element: gcr(1, x) = gcr(x, 1) = x
*
* Examples:
* ```ts
* new Fraction(8).gcr(4) // 2
Expand Down Expand Up @@ -1025,6 +1053,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;
Expand Down Expand Up @@ -1057,6 +1093,10 @@ 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).
*
* Returns a subunitary result if only one of the inputs is subunitary, superunitary otherwise.
*
* Examples:
* ```ts
* new Fraction(8).lcr(4) // 64
Expand All @@ -1069,10 +1109,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);
}

/**
Expand Down
Loading