exactnumber
Version:
Arbitrary-precision decimals. Enables making math calculations with rational numbers, without precision loss.
212 lines (174 loc) • 6.2 kB
text/typescript
import { Fraction } from './Fraction';
import { FixedNumber } from './FixedNumber';
import type { ExactNumberType } from './types';
import { _0N, _1N } from './util';
export function parseParameter(x: number | bigint | string | ExactNumberType): ExactNumberType {
if (x instanceof FixedNumber || x instanceof Fraction) {
return x;
}
if (typeof x === 'bigint') {
return new FixedNumber(x);
}
if (typeof x === 'number') {
if (!Number.isSafeInteger(x)) {
throw new Error('Floating point values as numbers are unsafe. Please provide them as a string.');
}
return new FixedNumber(x);
}
if (typeof x === 'string') {
if (x.includes('/') || x.includes('(')) {
return new Fraction(x, _1N);
}
return new FixedNumber(x);
}
throw new Error('Unsupported parameter type');
}
type ExactNumberInterface = {
(x: number | bigint | string | ExactNumberType, y?: number | bigint | string | ExactNumberType): ExactNumberType;
min: (...params: (number | bigint | string | ExactNumberType)[]) => ExactNumberType;
max: (...params: (number | bigint | string | ExactNumberType)[]) => ExactNumberType;
fromBase: (num: string, radix: number) => ExactNumberType;
isExactNumber: (x: any) => boolean;
range: (
start: number | bigint | string | ExactNumberType,
end: number | bigint | string | ExactNumberType,
increment?: number | bigint | string | ExactNumberType,
) => Generator<ExactNumberType, void, unknown>;
gcd: (
a: number | bigint | string | ExactNumberType,
b: number | bigint | string | ExactNumberType,
) => ExactNumberType;
lcm: (
a: number | bigint | string | ExactNumberType,
b: number | bigint | string | ExactNumberType,
) => ExactNumberType;
};
export const ExactNumber = <ExactNumberInterface>(
((x: number | bigint | string | ExactNumberType, y?: number | bigint | string | ExactNumberType) => {
if (x === undefined) {
throw new Error('First parameter cannot be undefined');
}
const xVal = parseParameter(x);
if (y === undefined) {
return xVal;
}
const yVal = parseParameter(y);
return new Fraction(xVal, _1N).div(new Fraction(yVal, _1N));
})
);
ExactNumber.min = <ExactNumberInterface['min']>((...params) => {
if ((params as any).length === 0) {
throw new Error('Got empty array');
}
let minVal = ExactNumber(params[0]);
for (let i = 1; i < params.length; i++) {
const x = ExactNumber(params[i]);
if (x.lt(minVal)) {
minVal = x;
}
}
return minVal;
});
ExactNumber.max = <ExactNumberInterface['max']>((...params) => {
if ((params as any).length === 0) {
throw new Error('Got empty array');
}
let maxVal = ExactNumber(params[0]);
for (let i = 1; i < params.length; i++) {
const x = ExactNumber(params[i]);
if (x.gt(maxVal)) {
maxVal = x;
}
}
return maxVal;
});
const parseDigitsInBase = (str: string, radix: number) => {
let res = _0N;
for (let i = 0; i < str.length; i++) {
const c = str.charAt(i);
const digit = parseInt(c, radix);
if (Number.isNaN(digit)) {
throw new Error(`Invalid digit "${c}"`);
}
res *= BigInt(radix);
res += BigInt(digit);
}
return res;
};
ExactNumber.fromBase = <ExactNumberInterface['fromBase']>((num, radix) => {
if (typeof num !== 'string') {
throw new Error('First parameter must be string');
}
if (!Number.isSafeInteger(radix) || radix < 2 || radix > 16) {
throw new Error('Invalid radix');
}
if (radix === 10) {
return ExactNumber(num);
}
num = num.trim();
if (num.length === 0) throw new Error('Empty string is not allowed');
const isNegative = num.startsWith('-');
if (isNegative) {
num = num.slice(1);
}
const m = num.match(/^([0-9a-f]*)(?:\.([0-9a-f]*)(?:\(([0-9a-f]+)\))?)?$/i);
if (!m) {
throw new Error(`Cannot parse number "${num}"`);
}
const wholePartStr = m[1] ?? '';
const nonRepeatingPartStr = m[2] ?? '';
const repeatingPartStr = m[3] ?? '';
if (repeatingPartStr.length > 0) {
const numerator =
parseDigitsInBase(`${wholePartStr}${nonRepeatingPartStr}${repeatingPartStr}`, radix) -
parseDigitsInBase(`${wholePartStr}${nonRepeatingPartStr}`, radix);
const denominator = parseDigitsInBase(
(radix - 1).toString(radix).repeat(repeatingPartStr.length) + '0'.repeat(nonRepeatingPartStr.length),
radix,
);
const res = new Fraction(numerator, denominator).normalize();
return isNegative ? res.neg() : res;
}
const whole = parseDigitsInBase(wholePartStr, radix);
const nonRepeating = parseDigitsInBase(nonRepeatingPartStr, radix);
const fracPath = new Fraction(nonRepeating, BigInt(radix) ** BigInt(nonRepeatingPartStr.length));
const res = new Fraction(whole, _1N).add(fracPath).normalize();
return isNegative ? res.neg() : res;
});
/** Used to iterate over exact rational numbers.
* E.g. Iterating from -2 to 3 with 0.5 increments:
* for(const x of ExactNumber.range(-2, 3, '0.5')) {} */
// eslint-disable-next-line func-names
ExactNumber.range = <ExactNumberInterface['range']>function* (_start, _end, _increment) {
const end = ExactNumber(_end);
const increment = ExactNumber(_increment ?? 1);
let i = ExactNumber(_start);
while (i.lt(end)) {
yield i;
i = i.add(increment);
}
};
ExactNumber.isExactNumber = <ExactNumberInterface['isExactNumber']>(
((x) => x instanceof FixedNumber || x instanceof Fraction)
);
ExactNumber.gcd = <ExactNumberInterface['gcd']>((a, b) => {
const aNum = ExactNumber(a).abs();
const bNum = ExactNumber(b).abs();
let maxNum = bNum.gt(aNum) ? bNum : aNum;
let minNum = maxNum.eq(aNum) ? bNum : aNum;
while (true) {
if (minNum.isZero()) return maxNum;
maxNum = maxNum.mod(minNum);
if (maxNum.isZero()) return minNum;
minNum = minNum.mod(maxNum);
}
});
ExactNumber.lcm = <ExactNumberInterface['lcm']>((a, b) => {
const aNum = ExactNumber(a).abs();
const bNum = ExactNumber(b).abs();
const product = aNum.mul(bNum);
if (product.isZero()) throw new Error('LCM of zero is undefined');
const gcd = ExactNumber.gcd(aNum, bNum);
return product.div(gcd);
});
// ExactNumber.modpow