Skip to content

Commit

Permalink
Merge pull request #10 from xenharmonic-devs/bigint-monzos-2
Browse files Browse the repository at this point in the history
Implement conversion between BigInt and monzo
  • Loading branch information
frostburn authored Nov 30, 2023
2 parents 1226ae1 + 2cdc38f commit 68f9d9a
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 5 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
46 changes: 46 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 @@ -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', () => {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
});
});
140 changes: 136 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,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));
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 All @@ -266,10 +357,13 @@ export function monzoToFraction(monzo: Iterable<number>) {
* @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(
Expand Down Expand Up @@ -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];
}
}
}
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 68f9d9a

Please sign in to comment.