From 2cdc38fc56ca00e702e775e6c4c0e1882e0d014b 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 | 46 ++++++++++++ src/monzo.ts | 140 ++++++++++++++++++++++++++++++++++-- src/primes.ts | 5 ++ tsconfig.json | 6 +- 5 files changed, 195 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2d0c001..6736ac3 100644 --- a/package.json +++ b/package.json @@ -43,5 +43,8 @@ "doc": "typedoc src/index.ts . --name xen-dev-utils", "prebenchmark": "tsc -p tsconfig-benchmark.json", "benchmark": "node benchmarks/__benchmarks__/monzo.mark.js" + }, + "engines":{ + "node": ">=10.6.0" } } diff --git a/src/__tests__/monzo.spec.ts b/src/__tests__/monzo.spec.ts index 0157d9b..afbe547 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', () => { @@ -88,6 +101,24 @@ describe('Fraction to monzo converter', () => { expect(residual.equals(0)).toBeTruthy(); expect(monzo).toHaveLength(0); }); + + 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', () => { @@ -98,6 +129,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); @@ -143,4 +182,11 @@ describe('Prime limit calculator', () => { const limit = primeLimit(new Fraction(4294967296, 4006077075)); expect(limit).toBe(13); }); + + it('can handle BigInt inputs', () => { + const two = primeLimit(BigInt('1267650600228229401496703205376')); + expect(two).toBe(2); + const limit = primeLimit(BigInt('1561327220802586898249028')); + expect(limit).toBe(19); + }); }); diff --git a/src/monzo.ts b/src/monzo.ts index 7488667..23dda33 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,7 +140,10 @@ 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); return sub(toMonzo(n.n), toMonzo(n.d)); @@ -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. @@ -266,10 +357,13 @@ export function monzoToFraction(monzo: Iterable) { * @returns The largest prime in the factorization of the input. `Infinity` if above the maximum limit. `NaN` if not applicable. */ export function primeLimit( - n: FractionValue, + n: FractionValue | bigint, asOrdinal = false, maxLimit = 7919 ): number { + if (typeof n === 'bigint') { + return bigIntPrimeLimit(n, asOrdinal, maxLimit); + } if (typeof n !== 'number') { n = new Fraction(n); return Math.max( @@ -312,3 +406,41 @@ export function primeLimit( } } } + +function bigIntPrimeLimit( + n: bigint, + asOrdinal: boolean, + maxLimit: number +): number { + if (n < 1n) { + return NaN; + } + if (n === 1n) { + return asOrdinal ? 0 : 1; + } + + // Accumulate increasingly complex factors into the probe + // until it reaches the input value. + + // Bit-magic for 2-limit + let probe = (n ^ (n - 1n)) & n; + if (n === probe) { + return asOrdinal ? 1 : 2; + } + let limitIndex = 1; + + while (true) { + const lastProbe = probe; + probe *= BIG_INT_PRIMES[limitIndex]; + if (n % probe) { + probe = lastProbe; + limitIndex++; + // Using non-big primes here is intentional, the arrays have the same length. + if (limitIndex >= PRIMES.length || PRIMES[limitIndex] > maxLimit) { + return Infinity; + } + } else if (n === probe) { + return asOrdinal ? limitIndex + 1 : PRIMES[limitIndex]; + } + } +} 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"