UNPKG

exactnumber

Version:

Arbitrary-precision decimals. Enables making math calculations with rational numbers, without precision loss.

1,359 lines (1,354 loc) 50.1 kB
/*! * 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