exactnumber
Version:
Arbitrary-precision decimals. Enables making math calculations with rational numbers, without precision loss.
1,359 lines (1,354 loc) • 50.1 kB
JavaScript
/*!
* exactnumber v2.0.1 (https://www.npmjs.com/package/exactnumber)
* (c) Dani Biro
* @license MIT
*/
// a random, hard to guess number
var RoundingMode;
(function (RoundingMode) {
/** Rounds to nearest number, with ties rounded towards +Infinity. Similar to Math.round(). */
RoundingMode[RoundingMode["NEAREST_TO_POSITIVE"] = 201008] = "NEAREST_TO_POSITIVE";
/** Rounds to nearest number, with ties rounded towards -Infinity. */
RoundingMode[RoundingMode["NEAREST_TO_NEGATIVE"] = 201009] = "NEAREST_TO_NEGATIVE";
/** Rounds to nearest number, with ties rounded towards the nearest even number. */
RoundingMode[RoundingMode["NEAREST_TO_EVEN"] = 201010] = "NEAREST_TO_EVEN";
/** Rounds to nearest number, with ties rounded towards zero. */
RoundingMode[RoundingMode["NEAREST_TO_ZERO"] = 201011] = "NEAREST_TO_ZERO";
/** Rounds to nearest number, with ties rounded away from zero. */
RoundingMode[RoundingMode["NEAREST_AWAY_FROM_ZERO"] = 201012] = "NEAREST_AWAY_FROM_ZERO";
/** Rounds towards +Infinity. Similar to Math.ceil(). */
RoundingMode[RoundingMode["TO_POSITIVE"] = 201001] = "TO_POSITIVE";
/** Rounds towards -Infinity. Similar to Math.floor(). */
RoundingMode[RoundingMode["TO_NEGATIVE"] = 201002] = "TO_NEGATIVE";
/** Rounds towards zero. Similar to Math.trunc(). */
RoundingMode[RoundingMode["TO_ZERO"] = 201003] = "TO_ZERO";
/** Rounds away from zero */
RoundingMode[RoundingMode["AWAY_FROM_ZERO"] = 201004] = "AWAY_FROM_ZERO";
})(RoundingMode || (RoundingMode = {}));
var ModType;
(function (ModType) {
ModType["TRUNCATED"] = "T";
ModType["FLOORED"] = "F";
ModType["EUCLIDEAN"] = "E";
})(ModType || (ModType = {}));
/* eslint-disable @typescript-eslint/naming-convention */
/** Trims trailing zeros from numbers in fixed-point format (1.23000 -> 1.23) */
const trimTrailingZerosFromFixed = (num) => {
const pointPos = num.indexOf('.');
if (pointPos === -1)
return num;
let firstZeroAt = num.length;
while (firstZeroAt > pointPos && num.charAt(firstZeroAt - 1) === '0')
firstZeroAt--;
const newLength = pointPos === firstZeroAt - 1 ? pointPos : firstZeroAt;
if (newLength === 0)
return '0';
return num.slice(0, newLength);
};
const bigIntToStr = (num, inputDecimals, outputDecimals, trimZeros) => {
let str = num.toString();
if (inputDecimals === 0 && outputDecimals === 0)
return str;
const isNegative = str.startsWith('-');
if (isNegative) {
str = str.slice(1);
}
if (inputDecimals >= str.length) {
str = '0'.repeat(inputDecimals - str.length + 1) + str;
}
if (inputDecimals > 0) {
const wholePart = str.slice(0, -inputDecimals);
const fracPart = str.slice(-inputDecimals);
const outFracPart = outputDecimals <= inputDecimals
? fracPart.slice(0, outputDecimals)
: `${fracPart}${'0'.repeat(outputDecimals - inputDecimals)}`;
if (outFracPart.length !== 0) {
str = `${wholePart}.${outFracPart}`;
if (trimZeros) {
str = trimTrailingZerosFromFixed(str);
}
}
else {
str = wholePart;
}
}
else if (outputDecimals > 0 && !trimZeros) {
str = `${str}.${'0'.repeat(outputDecimals)}`;
}
return isNegative ? `-${str}` : str;
};
// BigInt literals (1n) are not supported by all parsers
// also, the BigInt() constructor is still too slow to call in a loop
const _0N = BigInt(0);
const _1N = BigInt(1);
const _2N = BigInt(2);
BigInt(3);
BigInt(4);
const _5N = BigInt(5);
const _10N = BigInt(10);
const _24N = BigInt(24);
class FixedNumber {
parseConstructorParameter(x) {
if (x instanceof FixedNumber) {
return { number: x.number, decimalPos: x.decimalPos };
}
if (x instanceof Fraction) {
if (!x.isInteger()) {
throw new Error('Cannot create FixedNumber from non-integer Fraction');
}
return { number: x.trunc().number, decimalPos: 0 };
}
if (typeof x === 'number') {
if (!Number.isSafeInteger(x)) {
throw new Error('The specified number cannot be exactly represented as an integer. Please provide a string instead.');
}
return { number: BigInt(x), decimalPos: 0 };
}
if (typeof x === 'string') {
x = x.trim();
if (x.length === 0)
throw new Error('Empty string is not allowed');
const m = x.match(/^(-?[0-9]*)(?:\.([0-9]*))?(?:[eE]([+-]?[0-9]+))?$/);
if (!m) {
throw new Error(`Cannot parse number "${x}"`);
}
let decimalPos = 0; // at the right end
let str = m[1] ?? '0';
if (m[2] !== undefined) {
str += m[2];
decimalPos += m[2].length;
}
if (m[3] !== undefined) {
const exp = Number(m[3]);
if (exp > 0) {
str += '0'.repeat(exp);
}
else {
decimalPos -= exp;
}
}
return { number: BigInt(str), decimalPos };
}
throw new Error('Unsupported parameter!');
}
constructor(x, decimalPos = 0) {
this.type = 'fixed';
// fast path
if (typeof x === 'bigint') {
this.number = x;
this.decimalPos = decimalPos;
}
else {
const input = this.parseConstructorParameter(x);
this.number = input.number;
this.decimalPos = input.decimalPos;
}
}
scaleNumber(number, decimalPos) {
const maxPos = Math.max(this.decimalPos, decimalPos);
const a = maxPos === this.decimalPos ? this.number : this.number * _10N ** BigInt(maxPos - this.decimalPos);
const b = maxPos === decimalPos ? number : number * _10N ** BigInt(maxPos - decimalPos);
return { a, b, decimalPos: maxPos };
}
add(x) {
const operand = parseParameter(x);
if (operand instanceof Fraction) {
return operand.add(this);
}
const fixedOperand = operand;
const { a, b, decimalPos: newPos } = this.scaleNumber(fixedOperand.number, fixedOperand.decimalPos);
const res = new FixedNumber(a + b, newPos);
return res;
}
sub(x) {
const operand = parseParameter(x);
return this.add(operand.neg());
}
mul(x) {
const operand = parseParameter(x);
if (operand instanceof Fraction) {
return operand.mul(this);
}
const fixedOperand = operand;
const product = this.number * fixedOperand.number;
const res = new FixedNumber(product, this.decimalPos + fixedOperand.decimalPos);
return res;
}
pow(x) {
const operand = parseParameter(x);
const exp = operand.toNumber();
if (!Number.isSafeInteger(exp)) {
throw new Error('Unsupported parameter');
}
const absExp = Math.abs(exp);
const res = new FixedNumber(this.number ** BigInt(absExp), this.decimalPos * absExp);
return exp < 0 ? res.inv() : res;
}
powm(_exp, _mod, modType) {
let exp = parseParameter(_exp).toNumber();
if (!Number.isSafeInteger(exp)) {
throw new Error('Unsupported parameter');
}
const mod = parseParameter(_mod);
let base = this;
let res = new FixedNumber(_1N);
while (exp !== 0) {
if (exp % 2 !== 0) {
res = res.mul(base).mod(mod, modType);
}
base = base.pow(_2N).mod(mod, modType);
exp = Math.floor(exp / 2);
}
return res;
}
div(x) {
const frac = this.convertToFraction();
return frac.div(x);
}
divToInt(x) {
const operand = parseParameter(x);
if (operand instanceof Fraction) {
return this.convertToFraction().divToInt(operand);
}
const fixedOperand = operand;
const { a, b } = this.scaleNumber(fixedOperand.number, fixedOperand.decimalPos);
const res = new FixedNumber(a / b);
return res;
}
mod(x, type = ModType.TRUNCATED) {
const operand = parseParameter(x);
if (operand instanceof Fraction) {
return this.convertToFraction().mod(operand);
}
const fixedOperand = operand;
const { a, b, decimalPos } = this.scaleNumber(fixedOperand.number, fixedOperand.decimalPos);
const mod = a % b;
const res = new FixedNumber(mod, decimalPos);
if (type === ModType.TRUNCATED) {
return res;
}
if (type === ModType.FLOORED) {
return Number(a < _0N) ^ Number(b < _0N) ? res.add(b) : res;
}
if (type === ModType.EUCLIDEAN) {
return mod < _0N ? res.add(b < _0N ? -b : b) : res;
}
throw new Error('Invalid ModType');
}
abs() {
const res = new FixedNumber(this.number < _0N ? -this.number : this.number, this.decimalPos);
return res;
}
neg() {
return this.mul(-_1N);
}
inv() {
return this.convertToFraction().inv();
}
floor(decimals) {
if (this.decimalPos === 0)
return this;
return this.round(decimals, RoundingMode.TO_NEGATIVE);
}
ceil(decimals) {
if (this.decimalPos === 0)
return this;
return this.round(decimals, RoundingMode.TO_POSITIVE);
}
trunc(decimals) {
if (this.decimalPos === 0)
return this;
return this.round(decimals, RoundingMode.TO_ZERO);
}
isTieStr(lastDigitsStr) {
if (lastDigitsStr[0] !== '5')
return false;
for (let i = 1; i < lastDigitsStr.length; i++) {
if (lastDigitsStr[i] !== '0') {
return false;
}
}
return true;
}
_round(decimals, roundingMode) {
const shift = this.decimalPos - decimals;
if (shift <= 0) {
return this;
}
const exp = _10N ** BigInt(shift);
const outDigits = this.number / exp;
if (roundingMode === RoundingMode.TO_ZERO) {
return new FixedNumber(outDigits, decimals);
}
const extraDigits = this.number % exp;
if (extraDigits === _0N) {
return new FixedNumber(outDigits, decimals);
}
if (roundingMode === RoundingMode.AWAY_FROM_ZERO) {
const res = this.number < _0N ? outDigits - _1N : outDigits + _1N;
return new FixedNumber(res, decimals);
}
if (roundingMode === RoundingMode.TO_POSITIVE) {
const res = this.number < _0N ? outDigits : outDigits + _1N;
return new FixedNumber(res, decimals);
}
if (roundingMode === RoundingMode.TO_NEGATIVE) {
const res = this.number >= _0N ? outDigits : outDigits - _1N;
return new FixedNumber(res, decimals);
}
if (![
undefined,
RoundingMode.NEAREST_TO_ZERO,
RoundingMode.NEAREST_AWAY_FROM_ZERO,
RoundingMode.NEAREST_TO_POSITIVE,
RoundingMode.NEAREST_TO_NEGATIVE,
RoundingMode.NEAREST_TO_EVEN,
].includes(roundingMode)) {
throw new Error('Invalid rounding mode. Use the predefined values from the RoundingMode enum.');
}
let extraDigitsStr = (extraDigits < _0N ? -extraDigits : extraDigits).toString();
// '00123' extra part will appear in extraDigitsStr as '123'
// -> in this case we can exclude the tie case by setting the extra part to zero
if (extraDigitsStr.length < shift) {
extraDigitsStr = '0';
}
if (this.isTieStr(extraDigitsStr)) {
if (roundingMode === RoundingMode.NEAREST_TO_ZERO) {
return new FixedNumber(outDigits, decimals);
}
if (roundingMode === RoundingMode.NEAREST_AWAY_FROM_ZERO) {
const res = this.number < _0N ? outDigits - _1N : outDigits + _1N;
return new FixedNumber(res, decimals);
}
if (roundingMode === undefined || roundingMode === RoundingMode.NEAREST_TO_POSITIVE) {
const res = this.number < _0N ? outDigits : outDigits + _1N;
return new FixedNumber(res, decimals);
}
if (roundingMode === RoundingMode.NEAREST_TO_NEGATIVE) {
const res = this.number >= _0N ? outDigits : outDigits - _1N;
return new FixedNumber(res, decimals);
}
if (roundingMode === RoundingMode.NEAREST_TO_EVEN) {
if (outDigits % _2N === _0N) {
return new FixedNumber(outDigits, decimals);
}
const res = outDigits < _0N ? outDigits - _1N : outDigits + _1N;
return new FixedNumber(res, decimals);
}
}
if (Number(extraDigitsStr[0]) < 5) {
return new FixedNumber(outDigits, decimals);
}
const res = this.number < _0N ? outDigits - _1N : outDigits + _1N;
return new FixedNumber(res, decimals);
}
round(decimals, roundingMode) {
decimals = decimals === undefined ? 0 : decimals;
if (!Number.isSafeInteger(decimals) || decimals < 0) {
throw new Error('Invalid value for decimals');
}
return this._round(decimals, roundingMode).normalize();
}
limitDecimals(maxDecimals, roundingMode) {
return this.round(maxDecimals, roundingMode);
}
_incExponent(amount) {
if (amount === 0)
return this;
let newNumber = this.number;
let newDecimalPos = this.decimalPos;
if (amount < 0) {
newDecimalPos -= amount;
}
else {
// amount >= 0
const maxChange = Math.min(amount, this.decimalPos);
newDecimalPos -= maxChange;
const rem = amount - maxChange;
if (rem > 0) {
newNumber *= _10N ** BigInt(rem);
}
}
return new FixedNumber(newNumber, newDecimalPos);
}
countDigits() {
if (this.number === _0N)
return 1;
let digits = 0;
let x = this.number < _0N ? -this.number : this.number;
while (x > _0N) {
x /= _10N;
digits++;
}
return digits;
}
// move the number to the +-[0.1, 1) interval
toSubZeroNum() {
const digits = this.countDigits();
const subZeroNum = new FixedNumber(this.number, digits);
const exponentDiff = digits - this.decimalPos;
return { subZeroNum, exponentDiff };
}
roundToDigits(digits, roundingMode) {
if (!Number.isSafeInteger(digits) || digits < 1) {
throw new Error('Invalid value for digits');
}
const { subZeroNum, exponentDiff } = this.toSubZeroNum();
let roundedNumber = subZeroNum.round(digits, roundingMode);
roundedNumber = roundedNumber._incExponent(exponentDiff);
return roundedNumber;
}
intPart() {
return this.trunc();
}
fracPart() {
return this.sub(this.trunc());
}
sign() {
return this.number < _0N ? -1 : 1;
}
bitwiseAnd(x) {
x = ExactNumber(x);
if (!this.isInteger() || this.isNegative() || !x.isInteger() || x.isNegative()) {
throw new Error('Only positive integers are supported');
}
if (x instanceof Fraction) {
x = x.trunc();
}
const pow = _2N ** _24N;
let an = this.normalize().number;
let bn = x.trunc().normalize().number;
let res = _0N;
let shift = _1N;
while (an > _0N && bn > _0N) {
const modA = BigInt.asUintN(24, an);
const modB = BigInt.asUintN(24, bn);
res += BigInt(Number(modA) & Number(modB)) * shift;
shift *= pow;
an /= pow;
bn /= pow;
}
return new FixedNumber(res);
}
bitwiseOr(x) {
x = ExactNumber(x);
if (!this.isInteger() || this.isNegative() || !x.isInteger() || x.isNegative()) {
throw new Error('Only positive integers are supported');
}
if (x instanceof Fraction) {
x = x.trunc();
}
const pow = _2N ** _24N;
let an = this.normalize().number;
let bn = x.trunc().normalize().number;
let res = _0N;
let shift = _1N;
while (an > _0N || bn > _0N) {
const modA = BigInt.asUintN(24, an);
const modB = BigInt.asUintN(24, bn);
res += BigInt(Number(modA) | Number(modB)) * shift;
shift *= pow;
an /= pow;
bn /= pow;
}
return new FixedNumber(res);
}
bitwiseXor(x) {
x = ExactNumber(x);
if (!this.isInteger() || this.isNegative() || !x.isInteger() || x.isNegative()) {
throw new Error('Only positive integers are supported');
}
if (x instanceof Fraction) {
x = x.trunc();
}
const pow = _2N ** _24N;
let an = this.normalize().number;
let bn = x.trunc().normalize().number;
let res = _0N;
let shift = _1N;
while (an > _0N || bn > _0N) {
const modA = BigInt.asUintN(24, an);
const modB = BigInt.asUintN(24, bn);
res += BigInt(Number(modA) ^ Number(modB)) * shift;
shift *= pow;
an /= pow;
bn /= pow;
}
return new FixedNumber(res);
}
shiftLeft(bitCount) {
if (!this.isInteger() || this.isNegative()) {
throw new Error('Only positive integers are supported');
}
if (!Number.isSafeInteger(bitCount) || bitCount < 0) {
throw new Error('Invalid value for bitCount');
}
const pow = _2N ** BigInt(bitCount);
return this.mul(pow);
}
shiftRight(bitCount) {
if (!this.isInteger() || this.isNegative()) {
throw new Error('Only positive integers are supported');
}
if (!Number.isSafeInteger(bitCount) || bitCount < 0) {
throw new Error('Invalid value for bitCount');
}
const pow = _2N ** BigInt(bitCount);
return new FixedNumber(this.normalize().number / pow);
}
cmp(x) {
const operand = parseParameter(x);
if (operand instanceof Fraction) {
return -operand.cmp(this);
}
const fixedOperand = operand;
const { a, b } = this.scaleNumber(fixedOperand.number, fixedOperand.decimalPos);
if (a === b)
return 0;
return a > b ? 1 : -1;
}
eq(x) {
return this.cmp(x) === 0;
}
lt(x) {
return this.cmp(x) === -1;
}
lte(x) {
return this.cmp(x) <= 0;
}
gt(x) {
return this.cmp(x) === 1;
}
gte(x) {
return this.cmp(x) >= 0;
}
clamp(min, max) {
const minNum = ExactNumber(min);
const maxNum = ExactNumber(max);
if (minNum.gt(maxNum))
throw new Error('Min parameter has to be smaller than max');
if (this.lt(minNum))
return minNum;
if (this.gt(maxNum))
return maxNum;
return this;
}
isZero() {
return this.number === _0N;
}
isOne() {
if (this.decimalPos === 0) {
return this.number === _1N;
}
const exp = _10N ** BigInt(this.decimalPos);
const q = this.number / exp;
return q === _1N && q * exp === this.number;
}
isInteger() {
if (this.decimalPos === 0)
return true;
return this.number % _10N ** BigInt(this.decimalPos) === _0N;
}
isNegative() {
return this.sign() === -1;
}
serialize() {
return [this.number, this.decimalPos];
}
getFractionParts(normalize = true) {
return this.convertToFraction().getFractionParts(normalize);
}
normalize() {
if (this.decimalPos === 0)
return this;
let pos = this.decimalPos;
let n = this.number;
while (pos > 0 && n % _10N === _0N) {
pos--;
n /= _10N;
}
return new FixedNumber(n, pos);
}
convertToFraction() {
if (this.decimalPos === 0) {
return new Fraction(this.number, _1N);
}
const denominator = _10N ** BigInt(this.decimalPos);
return new Fraction(this.number, denominator);
}
toNumber() {
return Number(this.toPrecision(20));
}
toFixed(decimals, roundingMode = RoundingMode.TO_ZERO, trimZeros = false) {
if (!Number.isSafeInteger(decimals) || decimals < 0)
throw new Error('Invalid parameter');
const rounded = this._round(decimals, roundingMode);
return bigIntToStr(rounded.number, rounded.decimalPos, decimals, trimZeros);
}
toExponential(digits, roundingMode = RoundingMode.TO_ZERO, trimZeros = false) {
if (!Number.isSafeInteger(digits) || digits < 0)
throw new Error('Invalid parameter');
const rounded = this.roundToDigits(digits + 1, roundingMode).normalize();
const isNegative = rounded.isNegative();
const absNumber = rounded.abs();
const str = absNumber.number.toString();
const slicedString = str.length <= digits ? `${str}${'0'.repeat(digits - str.length + 1)}` : str.slice(0, digits + 1);
let strWithPoint = slicedString;
if (slicedString.length > 1) {
strWithPoint = `${slicedString.slice(0, 1)}.${slicedString.slice(1)}`;
if (trimZeros) {
strWithPoint = trimTrailingZerosFromFixed(strWithPoint);
}
}
const fractionalDigitsBefore = absNumber.decimalPos;
const fractionalDigitsAfter = str.length - 1;
const exponent = fractionalDigitsAfter - fractionalDigitsBefore;
const res = `${isNegative ? '-' : ''}${strWithPoint}e${exponent >= 0 ? '+' : ''}${exponent}`;
return res;
}
toBase(radix, maxDigits) {
if (!Number.isSafeInteger(radix) || radix < 2 || radix > 16)
throw new Error('Invalid radix');
if (maxDigits !== undefined && (!Number.isSafeInteger(maxDigits) || maxDigits < 0)) {
throw new Error('Invalid parameter');
}
const num = this.normalize();
if (num.decimalPos === 0)
return num.number.toString(radix);
const loopEnd = maxDigits === undefined ? Number.MAX_SAFE_INTEGER : maxDigits;
let intPart = num.intPart();
let fracPart = num.sub(intPart);
const isNegative = num.isNegative();
if (isNegative) {
intPart = intPart.neg();
fracPart = fracPart.neg();
}
const match = new Map();
let digits = [];
while (!fracPart.isZero()) {
const mul = fracPart.mul(radix);
const mulStr = mul.toString();
const cycleStart = match.get(mulStr);
if (cycleStart !== undefined) {
digits = [...digits.slice(0, cycleStart - 1), '(', ...digits.slice(cycleStart - 1), ')'];
break;
}
if (digits.length === loopEnd) {
break;
}
const q = Math.abs(mul.intPart().toNumber());
digits.push(q.toString(radix));
fracPart = mul.fracPart();
match.set(mulStr, digits.length);
}
const digitsStr = digits.join('');
const res = `${isNegative ? '-' : ''}${intPart.number.toString(radix)}${digits.length ? '.' : ''}${digitsStr}`;
return res;
}
toFraction() {
return this.convertToFraction().toFraction();
}
toString(radix, maxDigits) {
if (radix === undefined || radix === 10) {
const num = maxDigits !== undefined ? this.trunc(maxDigits) : this;
return bigIntToStr(num.number, num.decimalPos, num.decimalPos, true);
}
return this.toBase(radix, maxDigits);
}
toPrecision(digits, roundingMode = RoundingMode.TO_ZERO, trimZeros = false) {
if (!Number.isSafeInteger(digits) || digits < 1)
throw new Error('Invalid parameter');
const rounded = this.roundToDigits(digits, roundingMode);
const { subZeroNum, exponentDiff } = rounded.toSubZeroNum();
const isNegative = subZeroNum.isNegative();
let subZeroStr = bigIntToStr(subZeroNum.number, subZeroNum.decimalPos, subZeroNum.decimalPos, false);
subZeroStr = subZeroStr.slice(isNegative ? 3 : 2); // '-0.' or '0.'
// cut extra digits
subZeroStr = subZeroStr.slice(0, Math.max(digits, exponentDiff));
const whole = subZeroStr.slice(0, Math.max(0, exponentDiff));
const frac = subZeroStr.slice(Math.max(0, exponentDiff));
const suffixLength = Math.max(0, digits - whole.length - frac.length);
const prefix = '0'.repeat(exponentDiff < 0 ? -exponentDiff : 0);
let res = whole || '0';
if (frac.length + prefix.length + suffixLength > 0) {
const suffix = '0'.repeat(suffixLength);
res += `.${prefix}${frac}${suffix}`;
if (trimZeros) {
res = trimTrailingZerosFromFixed(res);
}
}
return isNegative ? `-${res}` : res;
}
valueOf() {
throw new Error('Unsafe conversion to Number type! Use toNumber() instead.');
}
}
class Fraction {
parseRepeatingDecimal(x) {
if (!x.includes('(')) {
return new FixedNumber(x).convertToFraction();
}
x = x.trim();
const m = x.match(/^(-?[0-9]*)\.([0-9]+)?\(([0-9]+)\)(?:[eE]([+-]?[0-9]+))?$/);
if (!m) {
throw new Error(`Cannot parse string "${x}"`);
}
const wholePart = m[1] === '-' ? '-0' : m[1];
const beforeCycle = m[2] ?? '';
const cycle = m[3];
const exponent = m[4];
const numerator = BigInt(wholePart + beforeCycle + cycle) - BigInt(wholePart + beforeCycle);
const denominator = BigInt('9'.repeat(cycle.length) + '0'.repeat(beforeCycle.length));
const fraction = new Fraction(numerator, denominator);
if (exponent !== undefined) {
const isNegativeExp = exponent.startsWith('-');
const exp = _10N ** BigInt(isNegativeExp ? exponent.slice(1) : exponent);
if (isNegativeExp) {
return fraction.div(exp).normalize();
}
return fraction.mul(exp).normalize();
}
return fraction.simplify();
}
parseParameter(x) {
if (x instanceof Fraction) {
return x;
}
if (x instanceof FixedNumber) {
return x.convertToFraction();
}
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 Fraction(BigInt(x), _1N);
}
if (typeof x === 'bigint') {
return new Fraction(x, _1N);
}
if (typeof x === 'string') {
const parts = x.split('/');
if (parts.length > 2)
throw new Error(`Cannot parse string '${x}'`);
const numerator = this.parseRepeatingDecimal(parts[0]);
const denominator = parts[1] ? this.parseRepeatingDecimal(parts[1]) : new Fraction(_1N, _1N);
const res = numerator.div(denominator);
const fraction = res.convertToFraction();
return fraction;
}
throw new Error('Unsupported parameter!');
}
constructor(x, y) {
this.type = 'fraction';
// fast path
if (typeof x === 'bigint' && typeof y === 'bigint') {
this.numerator = x;
this.denominator = y;
}
else {
const xFraction = this.parseParameter(x);
const yFraction = this.parseParameter(y);
const res = xFraction.div(yFraction);
const frac = res instanceof FixedNumber ? res.convertToFraction() : res;
this.numerator = frac.numerator;
this.denominator = frac.denominator;
}
if (this.denominator === _0N) {
throw new Error('Division by zero');
}
}
add(x) {
const { numerator, denominator } = this.parseParameter(x);
if (this.denominator === denominator) {
return new Fraction(this.numerator + numerator, this.denominator);
}
// if (false) {
// const commonDenominator = this.lcm(this.denominator, denominator);
// const lMultiplier = commonDenominator / this.denominator;
// const rMultiplier = commonDenominator / denominator;
// return new Fraction(this.numerator * lMultiplier + numerator * rMultiplier, commonDenominator);
// }
return new Fraction(this.numerator * denominator + numerator * this.denominator, denominator * this.denominator);
}
sub(x) {
const { numerator, denominator } = this.parseParameter(x);
return this.add(new Fraction(-numerator, denominator));
}
mul(x) {
const { numerator, denominator } = this.parseParameter(x);
const res = new Fraction(this.numerator * numerator, this.denominator * denominator);
return res;
}
div(x) {
const { numerator, denominator } = this.parseParameter(x);
return this.mul(new Fraction(denominator, numerator));
}
divToInt(x) {
const num = this.div(x);
return num.trunc();
}
mod(r, type = ModType.TRUNCATED) {
// n1 / d1 = n2 / d2 * q + r
// d2 * n1 = n2 * d1 * q + d1 * d2 * r
// (d2 * n1 % n2 * d1) / (d1 * d2)
const rFrac = this.parseParameter(r);
const a = (rFrac.denominator * this.numerator) % (rFrac.numerator * this.denominator);
const b = this.denominator * rFrac.denominator;
const res = new Fraction(a, b);
if (type === ModType.TRUNCATED) {
return res;
}
if (type === ModType.FLOORED) {
return Number(this.isNegative()) ^ Number(rFrac.isNegative()) ? res.add(rFrac) : res;
}
if (type === ModType.EUCLIDEAN) {
return res.isNegative() ? res.add(rFrac.isNegative() ? rFrac.neg() : rFrac) : res;
}
throw new Error('Invalid ModType');
}
pow(x) {
const param = this.parseParameter(x);
if (!param.isInteger()) {
throw new Error('Unsupported parameter');
}
const exp = param.numerator / param.denominator;
const absExp = exp < _0N ? -exp : exp;
const res = new Fraction(this.numerator ** absExp, this.denominator ** absExp);
return exp < _0N ? res.inv() : res;
}
powm(_exp, _mod, modType) {
const exp = this.parseParameter(_exp);
if (!exp.isInteger()) {
throw new Error('Unsupported parameter');
}
let expInt = exp.toNumber();
const mod = this.parseParameter(_mod);
let base = this;
let res = new Fraction(_1N, _1N);
while (expInt !== 0) {
if (expInt % 2 !== 0) {
res = res.mul(base).mod(mod, modType);
}
base = base.pow(_2N).mod(mod, modType);
expInt = Math.floor(expInt / 2);
}
return res;
}
inv() {
const res = new Fraction(this.denominator, this.numerator);
return res;
}
floor(decimals) {
if (this.denominator === _1N)
return new FixedNumber(this.numerator);
return this.round(decimals, RoundingMode.TO_NEGATIVE);
}
ceil(decimals) {
if (this.denominator === _1N)
return new FixedNumber(this.numerator);
return this.round(decimals, RoundingMode.TO_POSITIVE);
}
trunc(decimals) {
if (this.denominator === _1N)
return new FixedNumber(this.numerator);
return this.round(decimals, RoundingMode.TO_ZERO);
}
round(decimals, roundingMode) {
decimals = decimals === undefined ? 0 : decimals;
if (!Number.isSafeInteger(decimals) || decimals < 0) {
throw new Error('Invalid value for decimals');
}
const fixedPart = this.toFixedNumber(decimals + 1);
// tie case must be adjusted
const remainder = this.sub(fixedPart);
if (remainder.isZero()) {
// nothing is lost
return fixedPart.round(decimals, roundingMode);
}
// 0.105 might got cutted to 0.1, which might round incorrectly
// solution: add one digit to the end
let correctedFixedNum = new FixedNumber(`${fixedPart.toFixed(decimals + 1)}1`);
// 0 loses negative sign, so it needs to be corrected
if (fixedPart.isNegative() && !correctedFixedNum.isNegative()) {
correctedFixedNum = correctedFixedNum.neg();
}
const res = correctedFixedNum.round(decimals, roundingMode);
return res;
}
roundToDigits(digits, roundingMode) {
if (!Number.isSafeInteger(digits) || digits < 1) {
throw new Error('Invalid value for digits');
}
if (this.isZero())
return new FixedNumber(_0N);
let x = this.abs();
// move the number to the [0.1, 1) interval
let divisions = 0;
while (x.gte(_1N)) {
x = x.div(_10N);
divisions++;
}
const zeroPointOne = new Fraction(_1N, _10N);
while (x.lt(zeroPointOne)) {
x = x.mul(_10N);
divisions--;
}
let roundedNumber = x.round(digits, roundingMode);
roundedNumber = roundedNumber._incExponent(divisions);
return this.isNegative() ? roundedNumber.neg() : roundedNumber;
}
limitDecimals(maxDecimals, roundingMode) {
if (this.denominator === _1N) {
return new FixedNumber(this.numerator, 0);
}
const { cycleLen, cycleStart } = this.getDecimalFormat(maxDecimals);
if (cycleLen !== null && cycleStart + cycleLen <= maxDecimals) {
return this;
}
const roundedNumber = this.round(maxDecimals, roundingMode);
return roundedNumber;
}
gcd(numerator, denominator) {
let a = numerator < _0N ? -numerator : numerator;
let b = denominator < _0N ? -denominator : denominator;
if (b > a) {
const temp = a;
a = b;
b = temp;
}
while (true) {
if (b === _0N)
return a;
a %= b;
if (a === _0N)
return b;
b %= a;
}
}
// private lcm(a: bigint, b: bigint): bigint {
// return (a * b) / this.gcd(a, b);
// }
simplify() {
let { numerator, denominator } = this;
const gcd = this.gcd(numerator, denominator);
if (gcd > _1N) {
numerator /= gcd;
denominator /= gcd;
}
if (denominator < _0N) {
numerator = -numerator;
denominator = -denominator;
}
return new Fraction(numerator, denominator);
}
normalize() {
const { numerator, denominator } = this.simplify();
if (denominator === _1N) {
return new FixedNumber(numerator, 0);
}
const frac = new Fraction(numerator, denominator);
// check if conversion to FixedNumber is possible
const { cycleLen, cycleStart } = frac.getDecimalFormat(0);
if (cycleLen !== 0) {
return frac;
}
return frac.round(cycleStart, RoundingMode.TO_ZERO);
}
getFractionParts(normalize = true) {
const num = normalize ? this.simplify() : this;
return {
numerator: new FixedNumber(num.numerator),
denominator: new FixedNumber(num.denominator),
};
}
sign() {
const numeratorSign = this.numerator < _0N ? -1 : 1;
const denominatorSign = this.denominator < _0N ? -1 : 1;
return (numeratorSign * denominatorSign);
}
abs() {
const res = new Fraction(this.numerator < _0N ? -this.numerator : this.numerator, this.denominator < _0N ? -this.denominator : this.denominator);
return res;
}
neg() {
return this.mul(-_1N);
}
intPart() {
return this.trunc();
}
fracPart() {
return this.sub(this.trunc());
}
cmp(x) {
const rVal = this.parseParameter(x);
const hasCommonDenominator = this.denominator === rVal.denominator;
const a = hasCommonDenominator ? this.numerator : this.numerator * rVal.denominator;
const b = hasCommonDenominator ? rVal.numerator : rVal.numerator * this.denominator;
if (a === b)
return 0;
return a > b ? 1 : -1;
}
eq(x) {
return this.cmp(x) === 0;
}
lt(x) {
return this.cmp(x) === -1;
}
lte(x) {
return this.cmp(x) <= 0;
}
gt(x) {
return this.cmp(x) === 1;
}
gte(x) {
return this.cmp(x) >= 0;
}
clamp(min, max) {
const minNum = ExactNumber(min);
const maxNum = ExactNumber(max);
if (minNum.gt(maxNum))
throw new Error('Min parameter has to be smaller than max');
if (this.lt(minNum))
return minNum;
if (this.gt(maxNum))
return maxNum;
return this;
}
isZero() {
return this.numerator === _0N;
}
isOne() {
return this.numerator === this.denominator;
}
isInteger() {
return this.numerator % this.denominator === _0N;
}
isNegative() {
return this.sign() === -1;
}
serialize() {
return [this.numerator, this.denominator];
}
toNumber() {
return Number(this.toPrecision(20));
}
convertToFraction() {
return this;
}
getNumberForBitwiseOp() {
if (!this.isInteger() || this.isNegative()) {
throw new Error('Only positive integers are supported');
}
return this.intPart();
}
bitwiseAnd(x) {
return this.getNumberForBitwiseOp().bitwiseAnd(x);
}
bitwiseOr(x) {
return this.getNumberForBitwiseOp().bitwiseOr(x);
}
bitwiseXor(x) {
return this.getNumberForBitwiseOp().bitwiseXor(x);
}
shiftLeft(bitCount) {
return this.getNumberForBitwiseOp().shiftLeft(bitCount);
}
shiftRight(bitCount) {
return this.getNumberForBitwiseOp().shiftRight(bitCount);
}
getDecimalFormat(maxDigits) {
maxDigits = maxDigits ?? Number.MAX_SAFE_INTEGER;
let d = this.denominator < _0N ? -this.denominator : this.denominator;
let twoExp = 0;
while (d % _2N === _0N) {
d /= _2N;
twoExp++;
}
let fiveExp = 0;
while (d % _5N === _0N) {
d /= _5N;
fiveExp++;
}
const cycleStart = Math.max(twoExp, fiveExp);
if (d === _1N) {
return { cycleLen: 0, cycleStart };
}
const end = Math.max(1, maxDigits - cycleStart);
let rem = _10N % d;
let cycleLen = 1;
// 10^l ≡ 1 (mod d)
while (rem !== _1N) {
if (cycleLen === end) {
// abort calculation
return { cycleLen: null, cycleStart };
}
rem = (rem * _10N) % d;
cycleLen++;
}
return { cycleLen, cycleStart };
}
toFixed(decimals, roundingMode = RoundingMode.TO_ZERO, trimZeros = false) {
if (!Number.isSafeInteger(decimals) || decimals < 0)
throw new Error('Invalid parameter');
return this.round(decimals, roundingMode).toFixed(decimals, RoundingMode.TO_ZERO, trimZeros);
}
toRepeatingParts(maxDigits) {
if (this.isZero()) {
return ['0', '', ''];
}
const { cycleLen, cycleStart } = this.simplify().getDecimalFormat(maxDigits);
// if aborted calculation or terminating decimal
if (cycleLen === null || cycleLen === 0) {
const outputDigits = maxDigits ?? cycleStart;
const str = this.toFixed(outputDigits);
const parts = trimTrailingZerosFromFixed(str).split('.');
return [parts[0], parts[1] ?? '', ''];
}
const digits = cycleStart + cycleLen;
const str = this.toFixed(digits);
const parts = str.split('.');
return [parts[0], parts[1].slice(0, cycleStart), parts[1].slice(cycleStart)];
}
toRepeatingDigits(maxDigits) {
const parts = this.toRepeatingParts(maxDigits);
let res = parts[0];
if (parts[1] || parts[2]) {
res += `.${parts[1]}`;
}
if (parts[2]) {
res += `(${parts[2]})`;
}
return res;
}
toExponential(digits, roundingMode = RoundingMode.TO_ZERO, trimZeros = false) {
if (!Number.isSafeInteger(digits) || digits < 0)
throw new Error('Invalid parameters');
const fixedNum = this.toFixedNumber(digits);
return fixedNum.toExponential(digits, roundingMode, trimZeros);
}
toFraction() {
const { numerator, denominator } = this.getFractionParts(true);
return `${numerator.toString()}/${denominator.toString()}`;
}
toFixedNumber(digits) {
if (this.numerator === _0N)
return new FixedNumber(0, 0);
if (this.denominator === _1N)
return new FixedNumber(this.numerator, 0);
let requiredDigits = digits;
let absNumerator = this.numerator < 0 ? -this.numerator : this.numerator;
while (absNumerator < this.denominator) {
absNumerator *= _10N;
requiredDigits++;
}
const factor = _10N ** BigInt(requiredDigits);
const numerator = this.numerator * factor;
const div = numerator / this.denominator;
const fixedNum = new FixedNumber(div, requiredDigits);
return fixedNum;
}
toBase(radix, maxDigits) {
if (!Number.isSafeInteger(radix) || radix < 2 || radix > 16)
throw new Error('Invalid radix');
if (maxDigits !== undefined && (!Number.isSafeInteger(maxDigits) || maxDigits < 0)) {
throw new Error('Invalid parameter');
}
if (radix === 10) {
return maxDigits === undefined
? this.toRepeatingDigits(maxDigits)
: trimTrailingZerosFromFixed(this.toFixed(maxDigits));
}
const num = this.normalize();
const loopEnd = maxDigits === undefined ? Number.MAX_SAFE_INTEGER : maxDigits + 1;
let intPart = num.intPart();
let fracPart = num.sub(intPart);
const isNegative = num.isNegative();
if (isNegative) {
intPart = intPart.neg();
fracPart = fracPart.neg();
}
const match = new Map();
let digits = [];
while (!fracPart.isZero()) {
if (digits.length === loopEnd)
break;
const mul = fracPart.mul(radix);
const mulStr = mul.normalize().toFraction();
const cycleStart = match.get(mulStr);
if (cycleStart !== undefined) {
digits = [...digits.slice(0, cycleStart - 1), '(', ...digits.slice(cycleStart - 1), ')'];
break;
}
const q = Math.abs(mul.intPart().toNumber());
digits.push(q.toString(radix));
fracPart = mul.fracPart();
match.set(mulStr, digits.length);
}
if (digits.length === loopEnd) {
digits.pop();
}
const digitsStr = digits.join('');
const res = `${isNegative ? '-' : ''}${intPart.toString(radix)}${digits.length ? '.' : ''}${digitsStr}`;
return res;
}
toString(radix, maxDigits) {
if (radix === undefined || radix === 10) {
return this.toRepeatingDigits(maxDigits);
}
return this.toBase(radix, maxDigits);
}
toPrecision(digits, roundingMode = RoundingMode.TO_ZERO, trimZeros = false) {
if (!Number.isSafeInteger(digits) || digits < 1)
throw new Error('Invalid parameter');
return this.roundToDigits(digits, roundingMode).toPrecision(digits, RoundingMode.TO_ZERO, trimZeros);
}
valueOf() {
throw new Error('Unsafe conversion to Number type! Use toNumber() instead.');
}
}
function parseParameter(x) {
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');
}
const ExactNumber = (((x, y) => {
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 = ((...params) => {
if (params.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 = ((...params) => {
if (params.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, radix) => {
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 = ((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 = 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 = (((x) => x instanceof FixedNumber || x instanceof Fraction));
ExactNumber.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 = ((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);
});
// ExactNumb