Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement conversion between BigInt and monzo #10

Merged
merged 1 commit into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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