diff --git a/src/__tests__/fraction.spec.ts b/src/__tests__/fraction.spec.ts index 5a66434..40259cb 100644 --- a/src/__tests__/fraction.spec.ts +++ b/src/__tests__/fraction.spec.ts @@ -233,4 +233,89 @@ describe('Fraction', () => { const y = new Fraction(x); expect(y.valueOf()).toBeCloseTo(x); }); + + it('has a geometric modulo (integers)', () => { + const fraction = new Fraction(5); + expect(fraction.geoMod(2).equals('5/4')).toBe(true); + }); + + it('has a geometric modulo (fractions)', () => { + const fraction = new Fraction(19, 5); + expect(fraction.geoMod('3/2').equals('152/135')).toBe(true); + }); + + it('has a geometric modulo (sub-unity)', () => { + const fraction = new Fraction(7); + expect(fraction.geoMod('1/2').equals('7/8')).toBe(true); + }); + + it('has a geometric modulo (negative numbers)', () => { + const fraction = new Fraction(11); + expect(fraction.geoMod(-2).equals('-11/8')).toBe(true); + }); + + it('has a geometric modulo (unity)', () => { + const fraction = new Fraction(1); + expect(fraction.geoMod(3).equals(1)).toBe(true); + }); + + it('has a geometric modulo (self)', () => { + const fraction = new Fraction(4, 3); + expect(fraction.geoMod('4/3').equals(1)).toBe(true); + }); + + it('has a geometric gcd (integers)', () => { + const fraction = new Fraction(8); + expect(fraction.gcr(4)!.equals(2)).toBe(true); + }); + + it('has a geometric gcd (unrelated integers)', () => { + const fraction = new Fraction(9); + expect(fraction.gcr(4)).toBeNull(); + }); + + it('has a geometric gcd (fractions)', () => { + const fraction = new Fraction(1024, 243); + expect(fraction.gcr('27/64')!.equals('4/3')).toBe(true); + }); + + it('has logdivision (integers)', () => { + const fraction = new Fraction(9); + expect(fraction.log(3)!.equals(2)).toBe(true); + }); + + it('has logdivision (negatives)', () => { + const fraction = new Fraction(-8); + expect(fraction.log(-2)!.equals(3)).toBe(true); + }); + + it('has logdivision (positive/negative)', () => { + const fraction = new Fraction(4); + expect(fraction.log(-2)!.equals(2)).toBe(true); + }); + + it('has logdivision (incompatible negatives)', () => { + const fraction = new Fraction(-4); + expect(fraction.log(-2)).toBeNull(); + }); + + it('has logdivision (negative/positive)', () => { + const fraction = new Fraction(-4); + expect(fraction.log(2)).toBeNull(); + }); + + it('has logdivision (negative result)', () => { + const fraction = new Fraction(1, 16); + expect(fraction.log(2)!.equals(-4)).toBe(true); + }); + + it('has logdivision (unrelated integers)', () => { + const fraction = new Fraction(15); + expect(fraction.log(2)).toBeNull(); + }); + + it('has logdivision (fractions)', () => { + const fraction = new Fraction(64, 27); + expect(fraction.log('16/9')?.equals('3/2')).toBe(true); + }); }); diff --git a/src/fraction.ts b/src/fraction.ts index c6efccb..a239072 100644 --- a/src/fraction.ts +++ b/src/fraction.ts @@ -697,4 +697,132 @@ export class Fraction { } return new Fraction(n * this.n, gcd(n, this.n) * gcd(d, this.d)); } + + /** + * Geometrically reduce a rational number until it's between 1 and the other a.k.a. geometric modulo + * + * Ex: new Fraction(5,1).geoMod(2) => 5/4 + */ + geoMod(other: FractionValue) { + let {s, n, d} = this; + const {s: os, n: on, d: od} = new Fraction(other); + let octaves = Math.floor(Math.log(n / d) / Math.log(on / od)); + + if (isNaN(octaves) || !isFinite(octaves)) { + throw new Error('Geometric modulo by 1 or other issue.'); + } + + if (octaves > 0) { + n *= od ** octaves; + d *= on ** octaves; + } else if (octaves < 0) { + n *= on ** octaves; + d *= od ** octaves; + } + + // Fine-tune to fix floating point issues. + if (on > od) { + while (n * od >= d * on) { + octaves++; + n *= od; + d *= on; + } + while (n < d) { + octaves--; + d *= od; + n *= on; + } + } else if (on < od) { + while (n * od <= d * on) { + octaves++; + n *= od; + d *= on; + } + while (n > d) { + octaves--; + d *= od; + n *= on; + } + } else { + throw new Error('Geometric modulo by 1.'); + } + + s *= os ** octaves; + + return new Fraction({s, n, d}); + } + + /** + * Check if the rational number is 1 + */ + isUnity() { + return this.s === 1 && this.n === 1 && this.d === 1; + } + + /** + * Calculate the greatest common radical between two rational numbers + * + * Ex: new Fraction(8).gcr(4) => 2 + */ + gcr(other: FractionValue, maxIter = 100) { + let a = this.clone(); + a.s = 1; + if (a.n < a.d) { + [a.d, a.n] = [a.n, a.d]; + } + let b = new Fraction(other); + b.s = 1; + if (b.n < b.d) { + [b.d, b.n] = [b.n, b.d]; + } + if (a.isUnity()) return b; + if (b.isUnity()) return a; + for (let i = 0; i < maxIter; ++i) { + try { + a = a.geoMod(b); + if (a.isUnity()) return b; + b = b.geoMod(a); + if (b.isUnity()) return a; + } catch { + return null; + } + } + return null; + } + + /** + * Calculate the logarithm of a rational number in the base of another a.k.a. logdivision + * + * Ex: new Fraction(64,27).log("16/9") => 3/2 + */ + log(other: FractionValue, maxIter = 100) { + const other_ = new Fraction(other); + const radical = this.gcr(other_, maxIter); + if (radical === null) { + return null; + } + + 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); + + if (other_.s < 0) { + if (d % 2 === 0) { + return null; + } + if (n % 2) { + if (this.s > 0) { + return null; + } + } else { + if (this.s < 0) { + return null; + } + } + } else if (this.s < 0) { + return null; + } + + return new Fraction({n, d}); + } }