fixed-precision
Version:
A fixed-precision decimal arithmetic library for JavaScript/TypeScript
591 lines (590 loc) • 21.8 kB
JavaScript
// 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
};