Skip to content

Commit

Permalink
Finagle gcd and lcm to satisfy a multiplicative identity
Browse files Browse the repository at this point in the history
Replicate something similar for radical/radicand analogues.
Extend gcd, lcm and mmod to bigints.
Fix zero fraction normalization.

ref #27
  • Loading branch information
frostburn committed Apr 20, 2024
1 parent c3734ab commit 6bf980e
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 18 deletions.
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

0 comments on commit 6bf980e

Please sign in to comment.