Skip to content

Commit

Permalink
Merge pull request #13 from xenharmonic-devs/logdivision
Browse files Browse the repository at this point in the history
Implement geometric analogues of modulo, gcd, lcm, division and rounding.
  • Loading branch information
frostburn authored Jan 26, 2024
2 parents 4833129 + 3866165 commit c2c93bb
Show file tree
Hide file tree
Showing 2 changed files with 314 additions and 0 deletions.
138 changes: 138 additions & 0 deletions src/__tests__/fraction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,4 +233,142 @@ 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);
});

// This can easily produce unrepresentable fractions.
it.skip('has a geometric modulo (random)', () => {
const fraction = new Fraction(Math.random());
expect(fraction.geoMod(Math.random()).compare(1)).toBeLessThan(0);
});

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);
});

// Apparently this can "succeed" even though it should be exceedingly unlikely...
it.skip('has a geometric gcd (random)', () => {
const fraction = new Fraction(Math.random());
expect(fraction.gcr(Math.random())).toBeNull();
});

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);
});

// Apparently this can "succeed" even though it should be exceedingly unlikely...
it.skip('has logdivision (random)', () => {
const fraction = new Fraction(Math.random());
expect(fraction.log(Math.random())).toBeNull();
});

it('has geometric lcm (integers)', () => {
const fraction = new Fraction(27);
expect(fraction.lcr(81)!.equals(531441)).toBe(true);
});

it('has a geometric lcm (fractions)', () => {
const fraction = new Fraction(9, 16);
expect(fraction.lcr('64/27')!.equals('4096/729')).toBe(true);
});

it('has geometric rounding (integers)', () => {
const fraction = new Fraction(17);
expect(fraction.geoRoundTo(2)!.equals(16)).toBe(true);
});

it('has geometric rounding (positive/negative)', () => {
const fraction = new Fraction(7);
expect(fraction.geoRoundTo(-2)!.equals(4)).toBe(true);
});

it('has geometric rounding (incompatible negative/positive)', () => {
const fraction = new Fraction(-7);
expect(fraction.geoRoundTo(2)).toBeNull();
});

it('has geometric rounding (negative)', () => {
const fraction = new Fraction(-7);
expect(fraction.geoRoundTo(-2)!.equals(-8)).toBe(true);
});

it('has geometric rounding (fractions)', () => {
const fraction = new Fraction(3, 2);
expect(fraction.geoRoundTo('10/9')!.equals('10000/6561')).toBe(true);
});
});
176 changes: 176 additions & 0 deletions src/fraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -697,4 +697,180 @@ 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);

if (on === od) {
throw new Error('Geometric modulo by 1');
}

let octaves = Math.floor(Math.log(n / d) / Math.log(on / od));

if (isNaN(octaves) || !isFinite(octaves)) {
throw new Error('Unable to calculate geometric modulo.');
}

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) {
if (n * od >= d * on) {
octaves++;
n *= od;
d *= on;
}
if (n < d) {
octaves--;
n *= on;
d *= od;
}
} else {
if (n * od <= d * on) {
octaves++;
n *= od;
d *= on;
}
if (n > d) {
octaves--;
n *= on;
d *= od;
}
}

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;
}

/**
* Calculates the geometric absolute value
*
* Ex: new Fraction(2, 3).gabs() => 3/2
**/
gabs() {
if (this.n < this.d) {
return new Fraction({n: this.d, d: this.n});
}
return this.abs();
}

/**
* Calculate the greatest common radical between two rational numbers
*
* Ex: new Fraction(8).gcr(4) => 2
*/
gcr(other: FractionValue, maxIter = 100) {
let a = this.gabs();
let b = new Fraction(other).gabs();
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});
}

/**
* Calculate the least common radicand between two rational numbers
*
* Ex: new Fraction(8).gcr(4) => 64
*/
lcr(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);
return radical.pow(Math.abs(n * d));
}

/**
* Rounds a rational number to a power of another rational number
*
* Ex: new Fraction('5/4').geoRoundTo("9/8") => 81 / 64
*/
geoRoundTo(other: FractionValue) {
const other_ = new Fraction(other);
let exponent = Math.log(this.n / this.d) / Math.log(other_.n / other_.d);
if (this.s === 0) {
return this.clone();
}
if (this.s < 0) {
if (other_.s > 0) {
return null;
}
exponent = Math.round((exponent + 1) * 0.5) * 2 - 1;
} else if (other_.s < 0) {
exponent = Math.round(exponent * 0.5) * 2;
} else {
exponent = Math.round(exponent);
}
return other_.pow(exponent);
}
}

0 comments on commit c2c93bb

Please sign in to comment.