Skip to content

Commit

Permalink
Implement geometric analogues of modulo, gcd and division
Browse files Browse the repository at this point in the history
  • Loading branch information
frostburn committed Jan 25, 2024
1 parent 4833129 commit becec98
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 0 deletions.
85 changes: 85 additions & 0 deletions src/__tests__/fraction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
128 changes: 128 additions & 0 deletions src/fraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}
}

0 comments on commit becec98

Please sign in to comment.