Skip to content

Commit

Permalink
WIP: Finagle gcd and lcm to satisfy a multiplicative identity
Browse files Browse the repository at this point in the history
Replicate something similar for radicand analogues.
Fix zero fraction normalization.

ref #27
  • Loading branch information
frostburn committed Apr 20, 2024
1 parent c3734ab commit faedaf6
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 13 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
125 changes: 124 additions & 1 deletion src/__tests__/fraction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)', () => {
Expand Down Expand Up @@ -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);
});
});
48 changes: 38 additions & 10 deletions src/fraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}

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

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

/**
Expand Down

0 comments on commit faedaf6

Please sign in to comment.