From 70716d0f73233130163fb3c93c47b5985e38eb18 Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Wed, 29 Nov 2023 20:14:37 +0200 Subject: [PATCH] Implement conversion between BigInt and monzo ref #9 --- package.json | 3 ++ src/__tests__/monzo.spec.ts | 39 +++++++++++++++ src/monzo.ts | 99 +++++++++++++++++++++++++++++++++++-- src/primes.ts | 5 ++ tsconfig.json | 6 ++- 5 files changed, 147 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 27c38cd..ee92a86 100644 --- a/package.json +++ b/package.json @@ -46,5 +46,8 @@ }, "dependencies": { "fraction.js": "^4.2.0" + }, + "engines":{ + "node": ">=10.6.0" } } diff --git a/src/__tests__/monzo.spec.ts b/src/__tests__/monzo.spec.ts index dbfc3ae..1c47cad 100644 --- a/src/__tests__/monzo.spec.ts +++ b/src/__tests__/monzo.spec.ts @@ -1,6 +1,7 @@ import {describe, it, expect} from 'vitest'; import {Fraction} from '../fraction'; import { + monzoToBigInt, monzoToFraction, primeLimit, toMonzo, @@ -41,6 +42,18 @@ describe('Monzo converter', () => { it('throws for zero', () => { expect(() => toMonzo(0)).toThrow(); }); + + it('can break down a big integer to its prime components', () => { + const monzo = toMonzo(BigInt('360000000000000000000000')); + expect(monzo[0]).toBe(24); + expect(monzo[1]).toBe(2); + expect(monzo[2]).toBe(22); + expect( + BigInt(2) ** BigInt(monzo[0]) * + BigInt(3) ** BigInt(monzo[1]) * + BigInt(5) ** BigInt(monzo[2]) + ).toBe(BigInt('360000000000000000000000')); + }); }); describe('Fraction to monzo converter', () => { @@ -83,6 +96,24 @@ describe('Fraction to monzo converter', () => { it('throws for zero (no vector part)', () => { expect(() => toMonzoAndResidual(0, 0)).toThrow(); }); + + it('leaves a residue if everything cannot be converted', () => { + const [monzo, residual] = toMonzoAndResidual( + BigInt('123456789000000000000'), + 3 + ); + expect(residual).toBe(BigInt(13717421)); + expect(monzo).toHaveLength(3); + expect(monzo[0]).toBe(12); + expect(monzo[1]).toBe(2); + expect(monzo[2]).toBe(12); + expect( + BigInt(2) ** BigInt(monzo[0]) * + BigInt(3) ** BigInt(monzo[1]) * + BigInt(5) ** BigInt(monzo[2]) * + residual + ).toBe(BigInt('123456789000000000000')); + }); }); describe('Monzo to fraction converter', () => { @@ -93,6 +124,14 @@ describe('Monzo to fraction converter', () => { }); }); +describe('Monzo to BigInt converter', () => { + it('multiplies the prime components', () => { + expect(monzoToBigInt([30, 20, 10])).toBe( + BigInt('36561584400629760000000000') + ); + }); +}); + describe('Prime limit calculator', () => { it('knows that the limit of 1 is 1', () => { expect(primeLimit(1)).toBe(1); diff --git a/src/monzo.ts b/src/monzo.ts index 77c464b..1ec5961 100644 --- a/src/monzo.ts +++ b/src/monzo.ts @@ -1,5 +1,5 @@ import {Fraction, FractionValue} from './fraction'; -import {PRIMES} from './primes'; +import {BIG_INT_PRIMES, PRIMES} from './primes'; /** * Array of integers representing the exponents of prime numbers in the unique factorization of a rational number. @@ -140,9 +140,12 @@ export function rescale(target: Monzo, amount: number) { * @param n Rational number to convert to a monzo. * @returns The monzo representing `n`. */ -export function toMonzo(n: FractionValue): Monzo { +export function toMonzo(n: FractionValue | bigint): Monzo { + if (typeof n === 'bigint') { + return bigIntToMonzo(n); + } if (typeof n !== 'number') { - n = new Fraction(n); + n = new Fraction(n as FractionValue); return sub(toMonzo(n.n), toMonzo(n.d)); } if (n < 1 || Math.round(n) !== n) { @@ -189,16 +192,60 @@ export function toMonzo(n: FractionValue): Monzo { } } +function bigIntToMonzo(n: bigint) { + if (n < 1n) { + throw new Error('Cannot convert non-positive big integer to monzo'); + } + if (n === 1n) { + return []; + } + const result = [0]; + + // Accumulate increasingly complex factors into the probe + // until it reaches the input value. + let probe = 1n; + let limitIndex = 0; + + while (true) { + const lastProbe = probe; + probe *= BIG_INT_PRIMES[limitIndex]; + if (n % probe) { + probe = lastProbe; + result.push(0); + limitIndex++; + if (limitIndex >= BIG_INT_PRIMES.length) { + throw new Error('Out of primes'); + } + } else if (n === probe) { + result[limitIndex]++; + return result; + } else { + result[limitIndex]++; + } + } +} + /** * Extract the exponents of the prime factors of a rational number. * @param n Rational number to convert to a monzo. * @param numberOfComponents Number of components in the result. * @returns The monzo representing `n` and a multiplicative residue that cannot be represented in the given limit. */ +export function toMonzoAndResidual( + n: bigint, + numberOfComponents: number +): [Monzo, bigint]; export function toMonzoAndResidual( n: FractionValue, numberOfComponents: number -): [Monzo, Fraction] { +): [Monzo, Fraction]; +export function toMonzoAndResidual( + n: FractionValue | bigint, + numberOfComponents: number +): [Monzo, Fraction] | [Monzo, bigint] { + if (typeof n === 'bigint') { + return bigIntToMonzoAndResidual(n, numberOfComponents); + } n = new Fraction(n); const numerator = n.n; const denominator = n.d; @@ -237,6 +284,30 @@ export function toMonzoAndResidual( return [result, (n as Fraction).div(new Fraction(nProbe, dProbe))]; } +function bigIntToMonzoAndResidual( + n: bigint, + numberOfComponents: number +): [Monzo, bigint] { + if (n < 1n) { + throw new Error('Cannot numbers smaller than one to monzo'); + } + + let probe = 1n; + + const result = Array(numberOfComponents).fill(-1); + for (let i = 0; i < numberOfComponents; ++i) { + let lastProbe; + do { + lastProbe = probe; + probe *= BIG_INT_PRIMES[i]; + result[i]++; + } while (n % probe === 0n); + probe = lastProbe; + } + + return [result, n / probe]; +} + /** * Convert a monzo to the fraction it represents. * @param monzo Iterable of prime exponents. @@ -258,6 +329,26 @@ export function monzoToFraction(monzo: Iterable) { return new Fraction(numerator, denominator); } +/** + * Convert a monzo to the BigInt it represents. + * @param monzo Iterable of prime exponents. + * @returns BigInt representation of the monzo. + */ +export function monzoToBigInt(monzo: Iterable) { + let result = 1n; + let index = 0; + for (const component of monzo) { + if (component > 0) { + result *= BIG_INT_PRIMES[index] ** BigInt(component); + } + if (component < 0) { + throw new Error('Cannot produce big fractions'); + } + index++; + } + return result; +} + /** * Calculate the prime limit of an integer or a fraction. * @param n Integer or fraction to calculate prime limit for. diff --git a/src/primes.ts b/src/primes.ts index 2308b16..046942e 100644 --- a/src/primes.ts +++ b/src/primes.ts @@ -96,3 +96,8 @@ const NATS_TO_CENTS = 1200 / Math.LN2; */ export const PRIME_CENTS = LOG_PRIMES.map(logPrime => logPrime * NATS_TO_CENTS); PRIME_CENTS[0] = 1200; // Ensure that octaves are exact. + +/** + * BigInt representation of the primes. + */ +export const BIG_INT_PRIMES = PRIMES.map(BigInt); diff --git a/tsconfig.json b/tsconfig.json index bb51c39..c811974 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,11 @@ "extends": "./node_modules/gts/tsconfig-google.json", "compilerOptions": { "rootDir": "./src", - "outDir": "dist" + "outDir": "dist", + "lib": [ + "es2020" + ], + "target": "es2020", }, "include": [ "src/index.ts"