Skip to content

Commit

Permalink
Implement conversion between BigInt and monzo
Browse files Browse the repository at this point in the history
ref #9
  • Loading branch information
frostburn committed Nov 29, 2023
1 parent f0e1247 commit 70716d0
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 5 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,8 @@
},
"dependencies": {
"fraction.js": "^4.2.0"
},
"engines":{
"node": ">=10.6.0"
}
}
39 changes: 39 additions & 0 deletions src/__tests__/monzo.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {describe, it, expect} from 'vitest';
import {Fraction} from '../fraction';
import {
monzoToBigInt,
monzoToFraction,
primeLimit,
toMonzo,
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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);
Expand Down
99 changes: 95 additions & 4 deletions src/monzo.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -258,6 +329,26 @@ export function monzoToFraction(monzo: Iterable<number>) {
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<number>) {
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.
Expand Down
5 changes: 5 additions & 0 deletions src/primes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
6 changes: 5 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 70716d0

Please sign in to comment.