UNPKG

fixed-precision

Version:

A fixed-precision decimal arithmetic library for JavaScript/TypeScript

591 lines (590 loc) 21.8 kB
// src/FixedPrecision.ts var FixedPrecision = class _FixedPrecision { value = 0n; /** * Formatting settings. * By default, uses 8 decimal places and ROUND_HALF_UP rounding (4). */ static format = { places: 8, roundingMode: 4 }; // Pre-calculate the scale factor static SCALENUMBER = 10 ** _FixedPrecision.format.places; static SCALE = BigInt(10 ** _FixedPrecision.format.places); /** * Configures the FixedPrecision library. * @param config - FixedPrecision configuration */ static configure(config) { if (config.places !== void 0) { if (!Number.isInteger(config.places) || config.places < 0 || config.places > 20) { throw new Error("Decimal places must be an integer between 0 and 20"); } _FixedPrecision.format.places = config.places; _FixedPrecision.SCALE = BigInt(10 ** config.places); _FixedPrecision.SCALENUMBER = 10 ** config.places; } if (config.roundingMode !== void 0) { if (![0, 1, 2, 3, 4, 5, 6, 7, 8].includes(config.roundingMode)) { throw new Error( "Invalid rounding mode. Must be 0, 1, 2, 3, 4, 5, 6, 7 or 8" ); } _FixedPrecision.format.roundingMode = config.roundingMode; } } constructor(val) { switch (typeof val) { case "bigint": this.value = val * _FixedPrecision.SCALE; break; case "number": { this.value = _FixedPrecision.fromNumber(val); break; } case "string": this.value = _FixedPrecision.fromString(val); break; default: this.value = val.value; } } static POW10 = Array.from( { length: 21 }, (_, i) => 10n ** BigInt(i) ); static pow10Big = (n) => n >= 0 && n < _FixedPrecision.POW10.length ? _FixedPrecision.POW10[n] : 10n ** BigInt(n); /** * Helper method to create a FixedPrecision instance from a raw, already scaled bigint. * @param rawValue - The raw bigint value. * @returns A new FixedPrecision instance with the internal value set to rawValue. */ static fromRaw(rawValue) { const instance = new _FixedPrecision(0n); instance.value = rawValue; return instance; } // –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– // Conversion helpers // –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– // Converts a string (with up to 8 decimal places) to bigint. static fromString(str) { const dotIndex = str.indexOf(".", 1); const P = _FixedPrecision.format.places; const lens = str.length; if (dotIndex === -1) { if (P <= 16) { const scale3 = _FixedPrecision.SCALENUMBER; if (lens + P < 16) { const num3 = Number(str); return BigInt(num3 * scale3); } if (lens < 16) { const num3 = Number(str); if (Number.isFinite(num3)) { if (Math.abs(num3) <= Number.MAX_SAFE_INTEGER / scale3) { return BigInt(num3 * scale3); } const nP = 16 - lens; if (nP >= P) { return BigInt(num3 * scale3); } return BigInt(num3 * Math.pow(10, nP)) * _FixedPrecision.pow10Big(P - nP); } } const num2 = BigInt(str); return num2 * _FixedPrecision.SCALE; } const scale2 = _FixedPrecision.SCALE; if (lens < 16) { const num2 = Number(str); return BigInt(num2) * scale2; } const num = BigInt(str); return num * scale2; } if (dotIndex + P < 16) { const num = Number(str); const scale2 = _FixedPrecision.SCALENUMBER; const nP = 16 - dotIndex; const nScaled2 = num < 0 ? -1 / scale2 : 1 / scale2; if (nP >= P) { return BigInt(Math.trunc(num * scale2 + nScaled2)); } const Num = num * Math.pow(10, nP); const newNum = Math.trunc(Num); const NewFrac = Math.trunc(newNum - Num); if (!NewFrac) { return BigInt(newNum) * _FixedPrecision.pow10Big(P - nP); } return BigInt(newNum) * _FixedPrecision.pow10Big(P - nP) + BigInt(NewFrac); } const intStr = str.slice(0, dotIndex); const facStr = str.slice(dotIndex + 1, dotIndex + 1 + P); const faclen = facStr.length; if (dotIndex < 16) { const int2 = Number(intStr); const scale2 = _FixedPrecision.SCALENUMBER; if (Math.abs(int2) <= Number.MAX_SAFE_INTEGER / scale2) { const nNum = int2 * scale2; if (P <= 16) { const frac4 = Number(facStr); const newLen4 = P >= faclen ? P - faclen : P; const nScaled4 = BigInt(frac4 * Math.pow(10, newLen4)); return nNum < 0 ? BigInt(nNum) - nScaled4 : BigInt(nNum) + nScaled4; } const frac3 = BigInt(facStr); const newLen3 = P >= faclen ? P - faclen : P; const nScaled3 = frac3 * _FixedPrecision.pow10Big(newLen3); return nNum < 0 ? BigInt(nNum) - nScaled3 : BigInt(nNum) + nScaled3; } if (P <= 16) { const frac3 = Number(facStr); const nP = 16 - dotIndex; if (nP >= P) { return BigInt(int2 * scale2 + frac3); } const Num = int2 * Math.pow(10, nP); const nScaled3 = BigInt(frac3 * Math.pow(10, P - nP)); return int2 < 0 ? BigInt(Num) * _FixedPrecision.pow10Big(P - nP) - nScaled3 : BigInt(Num) * _FixedPrecision.pow10Big(P - nP) + nScaled3; } const frac2 = BigInt(facStr); if (!frac2) { return BigInt(int2 * scale2); } const newLen2 = P >= faclen ? P - faclen : P; const nScaled2 = frac2 * _FixedPrecision.pow10Big(newLen2); return int2 < 0 ? BigInt(int2 * scale2) - nScaled2 : BigInt(int2 * scale2) + nScaled2; } const int = BigInt(intStr); const scale = _FixedPrecision.SCALE; if (P < 16) { const frac2 = Number(facStr); if (!frac2) { return int * scale; } const newLen2 = P >= faclen ? P - faclen : P; const nScaled2 = BigInt(frac2 * Math.pow(10, newLen2)); return int < 0n ? int * scale - nScaled2 : int * scale + nScaled2; } const frac = BigInt(facStr); if (!frac) { return int * scale; } const newLen = P >= faclen ? P - faclen : P; const nScaled = frac * _FixedPrecision.pow10Big(newLen); return int < 0n ? int * scale - nScaled : int * scale + nScaled; } // Converts a number to bigint. static fromNumber(value) { if (isNaN(value) || !isFinite(value)) { throw new Error("Invalid number: value must be a finite number."); } const scaled = value * _FixedPrecision.SCALENUMBER; if (Math.abs(scaled) > Number.MAX_SAFE_INTEGER) { if (Number.isInteger(value)) { return BigInt(value) * _FixedPrecision.SCALE; } else { const num = Math.trunc(value); const nNum = Math.abs(num - value); const nScaled = BigInt(Math.trunc(nNum * _FixedPrecision.SCALENUMBER)); return BigInt(num) * _FixedPrecision.SCALE + nScaled; } } return BigInt(Math.trunc(scaled)); } // Converts an internal scaled BigInt to a number. static toNumber(value) { return Number(value) / _FixedPrecision.SCALENUMBER; } // Converts a raw bigint to string (in normal decimal notation). static toString(value) { const abs = value < 0n ? 0 : 1; const s = value.toString(); const intPart = abs ? s.slice(0, -_FixedPrecision.format.places) || "0" : s.slice(1, -_FixedPrecision.format.places) || "0"; let fracPart = s.slice(-_FixedPrecision.format.places); if (fracPart.length < _FixedPrecision.format.places) { fracPart = fracPart.padStart(_FixedPrecision.format.places, "0"); } return abs ? fracPart ? `${intPart}.${fracPart}` : intPart : fracPart ? `-${intPart}.${fracPart}` : `-${intPart}`; } // Instance conversion methods toNumber() { return _FixedPrecision.toNumber(this.value); } toString() { return _FixedPrecision.toString(this.value); } // –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– // Arithmetic methods // –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– /** Returns a FixedPrecision whose value is the absolute value of this FixedPrecision. */ abs() { return _FixedPrecision.fromRaw(this.value < 0n ? -this.value : this.value); } /** Compares the values. * Returns -1 if this < other, 0 if equal, and 1 if this > other. */ cmp(other) { if (this.value < other.value) return -1; if (this.value > other.value) return 1; return 0; } /** Returns true if this FixedPrecision equals other. */ eq(other) { return this.value === other.value; } /** Returns true if this FixedPrecision is greater than other. */ gt(other) { return this.value > other.value; } /** Returns true if this FixedPrecision is greater than or equal to other. */ gte(other) { return this.value >= other.value; } /** Returns true if this FixedPrecision is less than other. */ lt(other) { return this.value < other.value; } /** Returns true if this FixedPrecision is less than or equal to other. */ lte(other) { return this.value <= other.value; } isZero() { return this.value === 0n; } isPositive() { return this.value > 0n; } isNegative() { return this.value < 0n; } /** Returns a FixedPrecision whose value is this FixedPrecision plus n. */ add(other) { return _FixedPrecision.fromRaw(this.value + other.value); } /** Alias for add. */ plus(other) { return this.add(other); } /** Returns a FixedPrecision whose value is this FixedPrecision minus n. */ sub(other) { return _FixedPrecision.fromRaw(this.value - other.value); } /** Alias for sub. */ minus(other) { return this.sub(other); } /** Returns a FixedPrecision whose value is this FixedPrecision times n. */ mul(other) { return _FixedPrecision.fromRaw( this.value * other.value / _FixedPrecision.SCALE ); } product(other) { return new _FixedPrecision(this.value * other.value); } /** Returns a FixedPrecision whose value is this FixedPrecision divided by n. */ div(other) { return _FixedPrecision.fromRaw( this.value * _FixedPrecision.SCALE / other.value ); } fraction(other) { return _FixedPrecision.fromRaw(this.value / other.value); } /** Returns a FixedPrecision representing the integer remainder of dividing this by n. */ mod(other) { return _FixedPrecision.fromRaw( this.value * _FixedPrecision.SCALE % other.value ); } leftover(other) { return _FixedPrecision.fromRaw(this.value % other.value); } /** Returns a FixedPrecision whose value is the negation of this FixedPrecision. */ neg() { return _FixedPrecision.fromRaw(-this.value); } /** * Returns a FixedPrecision whose value is this FixedPrecision raised to the power exp. * (Only integer exponents are supported.) */ pow(exp) { if (!Number.isInteger(exp)) throw new Error("Exponent must be an integer"); if (exp === 0) return _FixedPrecision.fromRaw(_FixedPrecision.SCALE); if (this.isZero()) { if (exp < 0) throw new Error("0 ** negative is undefined"); return _FixedPrecision.fromRaw(0n); } let e = Math.abs(exp); let base = this.value; let acc = _FixedPrecision.SCALE; while (e > 0) { if (e & 1) acc = acc * base / _FixedPrecision.SCALE; base = base * base / _FixedPrecision.SCALE; e >>= 1; } if (exp < 0) { const inv = _FixedPrecision.SCALE * _FixedPrecision.SCALE / acc; return _FixedPrecision.fromRaw(inv); } return _FixedPrecision.fromRaw(acc); } /** * Returns a FixedPrecision representing π (pi) with the current precision */ static PI() { return new _FixedPrecision(Math.PI); } /** * Returns a FixedPrecision representing e (Euler's number) with current precision */ static e() { return new _FixedPrecision(Math.E); } /** * Returns a FixedPrecision representing φ (golden ratio) with current precision */ static phi() { const phiValue = (1 + Math.sqrt(5)) / 2; return new _FixedPrecision(phiValue); } /** * Returns a FixedPrecision representing √2 with current precision */ static sqrt2() { const sqrt2Value = Math.sqrt(2); return new _FixedPrecision(sqrt2Value); } /** * Returns a FixedPrecision whose value is the square root of this FixedPrecision. * (For simplicity, we use Math.sqrt on the number value.) */ sqrt() { if (this.lt(new _FixedPrecision(0n))) { throw new Error("Square root of negative number"); } if (this.eq(new _FixedPrecision(0n))) { return new _FixedPrecision(0n); } const initialGuess = this.div(new _FixedPrecision(2n)); return this.sqrtGo(initialGuess, _FixedPrecision.format.places); } /** * Newton–Raphson iteration for square root. * @param guess Current approximation. * @param iter Remaining iterations. * @returns Improved square root approximation. */ sqrtGo(guess, iter) { if (iter === 0) { return guess; } const next = guess.add(this.div(guess)).div(new _FixedPrecision(2n)); if (guess.eq(next)) { return next; } return this.sqrtGo(next, iter - 1); } /** * Returns a JSON representation of this FixedPrecision (its string value). */ toJSON() { return this.toString(); } /** * Returns a new FixedPrecision representing the ceiling of this value. * For positive numbers, rounds up; for negatives, rounds toward zero. */ ceil() { return this.round(0, 2); } /** * Returns a new FixedPrecision representing the floor of this value. * For positive numbers, rounds down; for negatives, rounds away from zero. */ floor() { return this.round(0, 3); } /** * Returns a new FixedPrecision representing the value truncated toward zero. */ trunc() { return this.round(0, 1); } /** * Returns a new FixedPrecision with its value shifted by n decimal places. * A positive n shifts to the left (multiplication), negative to the right (division). * * The operation is exact; if a negative shift does not divide evenly, an error is thrown. * * @param n - The number of places to shift. * @returns A new FixedPrecision instance with the shifted value. */ shiftedBy(n) { const shiftFactor = 10n ** BigInt(Math.abs(n)); let newRaw; if (n >= 0) { newRaw = this.value * shiftFactor; } else { if (this.value % shiftFactor !== 0n) { throw new Error("Inexact shift"); } newRaw = this.value / shiftFactor; } return _FixedPrecision.fromRaw(newRaw); } /** * Returns a new FixedPrecision with a pseudo-random value ≥ 0 and < 1. * The result will have the specified number of decimal places. * * @param decimalPlaces - Number of decimal places (default: FixedPrecision.format.places). * @returns A new FixedPrecision representing a random value. */ static random(decimalPlaces = _FixedPrecision.format.places) { const max = 10 ** decimalPlaces; const randInt = Math.floor(Math.random() * max); const fracStr = randInt.toString().padStart(decimalPlaces, "0"); const valueStr = "0." + fracStr; return new _FixedPrecision(valueStr); } // –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– // Formatting methods // –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– /** * Rounds the internal value according to the given scaling factor and rounding mode. * * @param roundingFactor - The rounding factor (power of 10). * @param rm - Rounding mode to use * @returns The quotient after rounding (without the discarded digits) */ roundToScale(roundingFactor, rm) { const quotient = this.value / roundingFactor; const remainder = this.value % roundingFactor; const absRem = remainder < 0n ? -remainder : remainder; switch (rm) { case 0: return remainder === 0n ? quotient : this.value > 0n ? quotient + 1n : quotient - 1n; case 1: return quotient; case 2: return this.value > 0n && remainder !== 0n ? quotient + 1n : quotient; case 3: return this.value < 0n && remainder !== 0n ? quotient - 1n : quotient; case 4: return 2n * absRem >= roundingFactor ? this.value > 0n ? quotient + 1n : quotient - 1n : quotient; case 5: return 2n * absRem > roundingFactor ? this.value > 0n ? quotient + 1n : quotient - 1n : quotient; case 6: if (2n * absRem === roundingFactor) { return quotient % 2n === 0n ? quotient : this.value > 0n ? quotient + 1n : quotient - 1n; } return 2n * absRem > roundingFactor ? this.value > 0n ? quotient + 1n : quotient - 1n : quotient; case 7: if (2n * absRem === roundingFactor) { return this.value > 0n ? quotient + 1n : quotient; } return 2n * absRem > roundingFactor ? this.value > 0n ? quotient + 1n : quotient : quotient; case 8: if (2n * absRem === roundingFactor) { return this.value < 0n ? quotient - 1n : quotient; } return 2n * absRem > roundingFactor ? this.value > 0n ? quotient + 1n : quotient - 1n : quotient; default: throw new Error(`Rounding mode ${rm} is not supported.`); } } /** * Returns a new FixedPrecision with the value rounded to the specified number of decimal places. * * @param dp - Desired number of decimal places (default: FixedPrecision.format.places) * @param rm - Rounding mode (default: FixedPrecision.format.roundingMode) * @returns A new FixedPrecision instance with the rounded value. */ round(dp = _FixedPrecision.format.places, rm = _FixedPrecision.format.roundingMode) { if (dp < 0 || dp > _FixedPrecision.format.places) { throw new Error( `Decimal places (dp) must be between 0 and ${_FixedPrecision.format.places}` ); } const diff = _FixedPrecision.format.places - dp; const factor = 10n ** BigInt(diff); const rounded = this.roundToScale(factor, rm); const newValue = rounded * factor; return _FixedPrecision.fromRaw(newValue); } /** * Adjusts the number scale, rounding to the new number of decimal places. * Returns a new FixedPrecision with the adjusted value. * * Example: * new FixedPrecision("1.23456789").scale(2) // represents 1.23 */ scale(newScale, rm = _FixedPrecision.format.roundingMode) { if (newScale < 0 || newScale > _FixedPrecision.format.places) { throw new Error( `newScale must be between 0 and ${_FixedPrecision.format.places}` ); } const diff = _FixedPrecision.format.places - newScale; const factor = 10n ** BigInt(diff); const rounded = this.roundToScale(factor, rm); const newValue = rounded * factor; return _FixedPrecision.fromRaw(newValue); } toExponential(dp = _FixedPrecision.format.places, rm) { const rounded = this.round(dp, rm); const [int = "", frac = ""] = rounded.toString().split("."); const exp = int.length > 1 ? int.length - 1 : int === "0" ? -frac?.search(/[1-9]/) - 1 : 0; const shifted = rounded.div( new _FixedPrecision(10n ** BigInt(Math.abs(exp))) ); return `${shifted.toFixed(dp)}e${exp}`; } toPrecision(sd, rm) { if (sd >= 1e6) { throw new Error("Invalid precision"); } return this.round(sd - (Math.floor(Math.log10(this.toNumber())) + 1), rm).toString().replace(/0+$/, ""); } /** * Returns a string representing the FixedPrecision in normal notation * to a fixed number of decimal places. */ toFixed(places = 0, rm = _FixedPrecision.format.roundingMode) { const decPlaces = places !== void 0 ? places : _FixedPrecision.format.places; if (decPlaces < 0 || decPlaces > _FixedPrecision.format.places) { throw new Error( `places must be between 0 and ${_FixedPrecision.format.places}` ); } const diff = _FixedPrecision.format.places - decPlaces; const roundingFactor = 10n ** BigInt(diff); const scaled = this.roundToScale(roundingFactor, rm); const divisor = 10n ** BigInt(decPlaces); const intPart = scaled / divisor; const fracPart = (scaled % divisor).toString().padStart(decPlaces, "0"); return decPlaces > 0 ? `${intPart.toString()}.${fracPart}` : intPart.toString(); } valueOf() { return this.toString(); } /** * Returns the type of this object as a string ('FixedPrecision') */ typeof() { return "FixedPrecision"; } raw() { return this.value; } }; var fixedconfig = { configure: FixedPrecision.configure.bind(FixedPrecision) }; export { FixedPrecision as default, fixedconfig };