From 1b04e6b96cc4d05eda07fe3addb03158f40cf995 Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Sun, 24 Mar 2024 20:16:31 +0200 Subject: [PATCH] Implement a relative metric for fraction simplification --- src/__tests__/fraction.spec.ts | 16 ++++++++++++++-- src/fraction.ts | 28 ++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/__tests__/fraction.spec.ts b/src/__tests__/fraction.spec.ts index 653ff89..322f720 100644 --- a/src/__tests__/fraction.spec.ts +++ b/src/__tests__/fraction.spec.ts @@ -161,12 +161,24 @@ describe('Fraction', () => { } }); - it('can simplify a random number', () => { - const value = Math.random() * 2; + it('can simplify a random number using an absolute metric', () => { + const value = Math.random() * 4 - 2; const fraction = new Fraction(value); expect(fraction.simplify().valueOf()).toBeCloseTo(value); }); + it('can simplify a random number using a relative metric', () => { + const value = + Math.exp(Math.random() * 20 - 10) * + (Math.floor(2 * Math.random()) * 2 - 1); + const fraction = new Fraction(value); + const simplified = fraction.simplifyRelative().valueOf(); + expect(Math.sign(simplified)).toBe(Math.sign(value)); + expect( + Math.abs(Math.log(Math.abs(simplified)) - Math.log(Math.abs(value))) + ).toBeLessThanOrEqual((Math.LN2 / 1200) * 3.5); + }); + it('can parse a repeated decimal', () => { const fraction = new Fraction("3.'3'"); expect(fraction.s).toBe(1); diff --git a/src/fraction.ts b/src/fraction.ts index b07b07e..7303f20 100644 --- a/src/fraction.ts +++ b/src/fraction.ts @@ -1,6 +1,7 @@ // I'm rolling my own because fraction.js has trouble with TypeScript https://github.com/rawify/Fraction.js/issues/72 // -Lumi +import {valueToCents} from './conversion'; import {PRIMES} from './primes'; export type UnsignedFraction = {n: number; d: number}; @@ -395,7 +396,8 @@ export class Fraction { } /** - * Return a convergent of this fraction that is within the given tolerance. + * Return a convergent of this fraction that is within the given absolute tolerance. + * @param epsilon Absolute tolerance for error. */ simplify(epsilon = 0.001) { const abs = this.abs(); @@ -408,7 +410,29 @@ export class Fraction { s = s.inverse().add(cont[k]); } - if (Math.abs(s.valueOf() - absValue) < epsilon) { + if (Math.abs(s.valueOf() - absValue) <= epsilon) { + return new Fraction({s: this.s, n: s.n, d: s.d} as Fraction); + } + } + return this.clone(); + } + + /** + * Return a convergent of this fraction that is within the given relative tolerance measured in cents. + * @param tolerance Relative tolerance measured in cents. + */ + simplifyRelative(tolerance = 3.5) { + const abs = this.abs(); + const cont = abs.toContinued(); + const absCents = valueToCents(abs.valueOf()); + + for (let i = 1; i < cont.length; i++) { + let s = new Fraction({s: 1, n: cont[i - 1], d: 1} as Fraction); + for (let k = i - 2; k >= 0; k--) { + s = s.inverse().add(cont[k]); + } + + if (Math.abs(valueToCents(s.valueOf()) - absCents) <= tolerance) { return new Fraction({s: this.s, n: s.n, d: s.d} as Fraction); } }