855 lines
32 KiB
JavaScript
855 lines
32 KiB
JavaScript
"use strict";
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.Decimal = exports.isDecimal = exports.DEFAULT_DIV_PRECISION = void 0;
|
||
/**
|
||
* Maps built-in preset names to their {@link DecimalRoundingOptions}.
|
||
* Used internally by {@link DecimalImpl.round}.
|
||
*
|
||
* @internal
|
||
*/
|
||
const ROUNDING_PRESETS = {
|
||
'ubb-usage-count': { mode: 'significant-figures', value: 15 },
|
||
'v1-api': { mode: 'decimal-places', value: 12 },
|
||
};
|
||
/**
|
||
* The IEEE 754 decimal128 coefficient size (34 digits) — the recommended
|
||
* precision for {@link DecimalImpl.div} when full precision is desired.
|
||
*
|
||
* @remarks
|
||
* Pass this as the `precision` argument to `div()` when you want the
|
||
* maximum available precision. Division requires explicit precision —
|
||
* no invisible defaults in financial code.
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* // Use the full decimal128 precision explicitly
|
||
* a.div(b, DEFAULT_DIV_PRECISION, 'half-even');
|
||
* ```
|
||
*
|
||
* @public
|
||
*/
|
||
exports.DEFAULT_DIV_PRECISION = 34;
|
||
/**
|
||
* Maximum number of digits in plain (non-exponential) notation produced
|
||
* by {@link DecimalImpl.toString}. Values exceeding this threshold are
|
||
* emitted in scientific notation (`1.23E+40`).
|
||
*
|
||
* @internal
|
||
*/
|
||
const PLAIN_NOTATION_DIGIT_LIMIT = 30;
|
||
/**
|
||
* Maximum absolute value for the internal exponent.
|
||
*
|
||
* @remarks
|
||
* This bound also implicitly limits exponent differences used in
|
||
* arithmetic (e.g., scaling by `10^exponentDiff`), preventing
|
||
* astronomically large BigInt allocations that could hang or
|
||
* exhaust the process.
|
||
*
|
||
* The chosen limit is intentionally conservative but still far beyond
|
||
* any magnitude needed for typical financial or billing calculations.
|
||
*
|
||
* @internal
|
||
*/
|
||
const MAX_EXPONENT = 1000000;
|
||
/**
|
||
* Internal implementation of arbitrary-precision decimal arithmetic.
|
||
*
|
||
* @remarks
|
||
* Represents a decimal value as `coefficient × 10^exponent` using
|
||
* native `BigInt` for the coefficient, giving unlimited precision with
|
||
* no rounding on construction. Instances are always
|
||
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze | frozen}
|
||
* and all arithmetic produces new instances.
|
||
*
|
||
* This class is **not** exported directly — consumers interact with
|
||
* the branded {@link Decimal} type and the {@link Decimal | Decimal companion object}.
|
||
*
|
||
* @internal
|
||
*/
|
||
class DecimalImpl {
|
||
/**
|
||
* Construct and normalise a decimal value.
|
||
*
|
||
* @param coefficient - The unscaled integer value.
|
||
* @param exponent - The power-of-ten scale factor.
|
||
*
|
||
* @internal
|
||
*/
|
||
constructor(coefficient, exponent) {
|
||
const [normalizedCoef, normalizedExp] = DecimalImpl.normalize(coefficient, exponent);
|
||
this._coefficient = normalizedCoef;
|
||
this._exponent = normalizedExp;
|
||
Object.freeze(this);
|
||
}
|
||
/**
|
||
* Strip trailing zeros from `coefficient`, incrementing `exponent`
|
||
* for each zero removed. Zero always normalises to `(0n, 0)`.
|
||
*
|
||
* @param coefficient - Raw coefficient before normalisation.
|
||
* @param exponent - Raw exponent before normalisation.
|
||
* @returns A `[coefficient, exponent]` tuple with trailing zeros removed.
|
||
*
|
||
* @internal
|
||
*/
|
||
static normalize(coefficient, exponent) {
|
||
if (coefficient === 0n) {
|
||
return [0n, 0];
|
||
}
|
||
let coef = coefficient;
|
||
let exp = exponent;
|
||
while (coef !== 0n && coef % 10n === 0n) {
|
||
coef /= 10n;
|
||
exp += 1;
|
||
}
|
||
return [coef, exp];
|
||
}
|
||
/**
|
||
* Apply rounding to the result of an integer division.
|
||
*
|
||
* @remarks
|
||
* BigInt division truncates toward zero. This helper inspects the
|
||
* `remainder` to decide whether to adjust the truncated `quotient`
|
||
* by ±1 according to the chosen {@link RoundDirection}.
|
||
*
|
||
* The rounding direction is derived from the signs of `remainder`
|
||
* and `divisor`: when they agree the exact fractional part is
|
||
* positive (the truncation point is below the true value, so +1
|
||
* rounds to nearest); when they disagree the fractional part is
|
||
* negative (−1 rounds to nearest).
|
||
*
|
||
* @param quotient - Truncated integer quotient (`dividend / divisor`).
|
||
* @param remainder - Division remainder (`dividend % divisor`).
|
||
* @param divisor - The divisor used in the division.
|
||
* @param direction - The rounding strategy to apply.
|
||
* @returns The rounded quotient.
|
||
*
|
||
* @internal
|
||
*/
|
||
static roundDivision(quotient, remainder, divisor, direction) {
|
||
if (remainder === 0n) {
|
||
return quotient;
|
||
}
|
||
// 'round-down': truncate toward zero — BigInt division already does this.
|
||
if (direction === 'round-down') {
|
||
return quotient;
|
||
}
|
||
// The sign of remainder/divisor tells us which side of the truncation
|
||
// point the exact value lies on.
|
||
// Same sign → fractional part is positive (exact value > quotient) → +1 adjusts upward.
|
||
// Opposite sign → fractional part is negative (exact value < quotient) → -1 adjusts downward.
|
||
const roundDir = remainder > 0n === divisor > 0n ? 1n : -1n;
|
||
// 'round-up': away from zero whenever there is any remainder.
|
||
if (direction === 'round-up') {
|
||
return quotient + roundDir;
|
||
}
|
||
// 'ceil': toward positive infinity.
|
||
// If the fractional part is positive (roundDir === 1n), round up.
|
||
// If the fractional part is negative (roundDir === -1n), truncation already went toward +∞.
|
||
if (direction === 'ceil') {
|
||
return roundDir === 1n ? quotient + 1n : quotient;
|
||
}
|
||
// 'floor': toward negative infinity.
|
||
// If the fractional part is negative (roundDir === -1n), round down.
|
||
// If the fractional part is positive (roundDir === 1n), truncation already went toward -∞.
|
||
if (direction === 'floor') {
|
||
return roundDir === -1n ? quotient - 1n : quotient;
|
||
}
|
||
// For the half-* modes we need to compare the remainder to exactly half the divisor.
|
||
const absRemainder = remainder < 0n ? -remainder : remainder;
|
||
const absDivisor = divisor < 0n ? -divisor : divisor;
|
||
const doubled = absRemainder * 2n;
|
||
let cmp;
|
||
if (doubled === absDivisor) {
|
||
cmp = 0;
|
||
}
|
||
else if (doubled < absDivisor) {
|
||
cmp = -1;
|
||
}
|
||
else {
|
||
cmp = 1;
|
||
}
|
||
if (cmp < 0) {
|
||
// Less than half — truncation is already the nearest value.
|
||
return quotient;
|
||
}
|
||
if (cmp > 0) {
|
||
// More than half — round to nearest (away from truncation point).
|
||
return quotient + roundDir;
|
||
}
|
||
// Exactly half — tie-breaking depends on the chosen mode.
|
||
if (direction === 'half-up') {
|
||
// Round away from zero.
|
||
return quotient + roundDir;
|
||
}
|
||
if (direction === 'half-down') {
|
||
// Round toward zero — stay at the truncated quotient.
|
||
return quotient;
|
||
}
|
||
// HALF_EVEN: round to nearest even.
|
||
if (quotient % 2n === 0n) {
|
||
// Already even — stay at truncation.
|
||
return quotient;
|
||
}
|
||
else {
|
||
// Odd — adjust to make even.
|
||
return quotient + roundDir;
|
||
}
|
||
}
|
||
// -------------------------------------------------------------------
|
||
// Arithmetic
|
||
// -------------------------------------------------------------------
|
||
/**
|
||
* Return the sum of this value and `other`.
|
||
*
|
||
* @param other - The addend.
|
||
* @returns A new {@link Decimal} equal to `this + other`.
|
||
*
|
||
* @public
|
||
*/
|
||
add(other) {
|
||
const otherImpl = other;
|
||
// Align exponents — use the smaller (more precision) exponent as target.
|
||
if (this._exponent === otherImpl._exponent) {
|
||
return new DecimalImpl(this._coefficient + otherImpl._coefficient, this._exponent);
|
||
}
|
||
if (this._exponent < otherImpl._exponent) {
|
||
const scale = 10n ** BigInt(otherImpl._exponent - this._exponent);
|
||
return new DecimalImpl(this._coefficient + otherImpl._coefficient * scale, this._exponent);
|
||
}
|
||
else {
|
||
const scale = 10n ** BigInt(this._exponent - otherImpl._exponent);
|
||
return new DecimalImpl(this._coefficient * scale + otherImpl._coefficient, otherImpl._exponent);
|
||
}
|
||
}
|
||
/**
|
||
* Return the difference of this value and `other`.
|
||
*
|
||
* @param other - The subtrahend.
|
||
* @returns A new {@link Decimal} equal to `this - other`.
|
||
*
|
||
* @public
|
||
*/
|
||
sub(other) {
|
||
const otherImpl = other;
|
||
if (this._exponent === otherImpl._exponent) {
|
||
return new DecimalImpl(this._coefficient - otherImpl._coefficient, this._exponent);
|
||
}
|
||
if (this._exponent < otherImpl._exponent) {
|
||
const scale = 10n ** BigInt(otherImpl._exponent - this._exponent);
|
||
return new DecimalImpl(this._coefficient - otherImpl._coefficient * scale, this._exponent);
|
||
}
|
||
else {
|
||
const scale = 10n ** BigInt(this._exponent - otherImpl._exponent);
|
||
return new DecimalImpl(this._coefficient * scale - otherImpl._coefficient, otherImpl._exponent);
|
||
}
|
||
}
|
||
/**
|
||
* Return the product of this value and `other`.
|
||
*
|
||
* @param other - The multiplicand.
|
||
* @returns A new {@link Decimal} equal to `this × other`.
|
||
*
|
||
* @public
|
||
*/
|
||
mul(other) {
|
||
const otherImpl = other;
|
||
return new DecimalImpl(this._coefficient * otherImpl._coefficient, this._exponent + otherImpl._exponent);
|
||
}
|
||
/**
|
||
* Return the quotient of this value divided by `other`.
|
||
*
|
||
* @remarks
|
||
* Division scales the dividend to produce `precision` decimal digits
|
||
* in the result, then applies integer division and rounds the
|
||
* remainder according to `direction`.
|
||
*
|
||
* Division requires explicit rounding control — no invisible defaults
|
||
* in financial code. For full precision use {@link DEFAULT_DIV_PRECISION}
|
||
* (34, matching the IEEE 754 decimal128 coefficient size).
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* Decimal.from('1').div(Decimal.from('3'), 5, 'half-up'); // "0.33333"
|
||
* Decimal.from('5').div(Decimal.from('2'), 0, 'half-up'); // "3"
|
||
* Decimal.from('5').div(Decimal.from('2'), 0, 'half-even'); // "2"
|
||
* ```
|
||
*
|
||
* @param other - The divisor. Must not be zero.
|
||
* @param precision - Maximum number of decimal digits in the result.
|
||
* @param direction - How to round when the exact quotient cannot
|
||
* be represented at the requested precision.
|
||
* @returns A new {@link Decimal} equal to `this ÷ other`, rounded to
|
||
* `precision` decimal places.
|
||
* @throws {@link Error} if `other` is zero.
|
||
* @throws {@link Error} if `precision` is negative or non-integer.
|
||
*
|
||
* @public
|
||
*/
|
||
div(other, precision, direction) {
|
||
if (precision < 0 || !Number.isInteger(precision)) {
|
||
throw new Error('precision must be a non-negative integer');
|
||
}
|
||
const otherImpl = other;
|
||
if (otherImpl._coefficient === 0n) {
|
||
throw new Error('Division by zero');
|
||
}
|
||
// result_coefficient = this.coefficient × 10^(thisExp - otherExp + precision) / other.coefficient
|
||
// result_exponent = -precision
|
||
const scale = this._exponent - otherImpl._exponent + precision;
|
||
let quotient;
|
||
let remainder;
|
||
let roundingDivisor;
|
||
if (scale >= 0) {
|
||
const scaledDividend = this._coefficient * 10n ** BigInt(scale);
|
||
quotient = scaledDividend / otherImpl._coefficient;
|
||
remainder = scaledDividend % otherImpl._coefficient;
|
||
roundingDivisor = otherImpl._coefficient;
|
||
}
|
||
else {
|
||
// Negative scale: shift the power onto the divisor side to avoid
|
||
// BigInt exponentiation with a negative exponent (which throws).
|
||
const scaledDivisor = otherImpl._coefficient * 10n ** BigInt(-scale);
|
||
quotient = this._coefficient / scaledDivisor;
|
||
remainder = this._coefficient % scaledDivisor;
|
||
roundingDivisor = scaledDivisor;
|
||
}
|
||
const roundedQuotient = DecimalImpl.roundDivision(quotient, remainder, roundingDivisor, direction);
|
||
return new DecimalImpl(roundedQuotient, -precision);
|
||
}
|
||
// -------------------------------------------------------------------
|
||
// Comparison
|
||
// -------------------------------------------------------------------
|
||
/**
|
||
* Three-way comparison of this value with `other`.
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* const a = Decimal.from('1.5');
|
||
* const b = Decimal.from('2');
|
||
* a.cmp(b); // -1
|
||
* b.cmp(a); // 1
|
||
* a.cmp(a); // 0
|
||
* ```
|
||
*
|
||
* @param other - The value to compare against.
|
||
* @returns `-1` if `this \< other`, `0` if equal, `1` if `this \> other`.
|
||
*
|
||
* @public
|
||
*/
|
||
cmp(other) {
|
||
const otherImpl = other;
|
||
if (this._exponent === otherImpl._exponent) {
|
||
if (this._coefficient < otherImpl._coefficient)
|
||
return -1;
|
||
if (this._coefficient > otherImpl._coefficient)
|
||
return 1;
|
||
return 0;
|
||
}
|
||
if (this._exponent < otherImpl._exponent) {
|
||
// this has smaller exponent — scale other's coefficient to match.
|
||
const scale = 10n ** BigInt(otherImpl._exponent - this._exponent);
|
||
const scaledOther = otherImpl._coefficient * scale;
|
||
if (this._coefficient < scaledOther)
|
||
return -1;
|
||
if (this._coefficient > scaledOther)
|
||
return 1;
|
||
return 0;
|
||
}
|
||
else {
|
||
// other has smaller exponent — scale this's coefficient to match.
|
||
const scale = 10n ** BigInt(this._exponent - otherImpl._exponent);
|
||
const scaledThis = this._coefficient * scale;
|
||
if (scaledThis < otherImpl._coefficient)
|
||
return -1;
|
||
if (scaledThis > otherImpl._coefficient)
|
||
return 1;
|
||
return 0;
|
||
}
|
||
}
|
||
/**
|
||
* Return `true` if this value is numerically equal to `other`.
|
||
*
|
||
* @param other - The value to compare against.
|
||
* @returns `true` if `this === other` in value, `false` otherwise.
|
||
*
|
||
* @public
|
||
*/
|
||
eq(other) {
|
||
return this.cmp(other) === 0;
|
||
}
|
||
/**
|
||
* Return `true` if this value is strictly less than `other`.
|
||
*
|
||
* @param other - The value to compare against.
|
||
* @returns `true` if `this \< other`, `false` otherwise.
|
||
*
|
||
* @public
|
||
*/
|
||
lt(other) {
|
||
return this.cmp(other) === -1;
|
||
}
|
||
/**
|
||
* Return `true` if this value is less than or equal to `other`.
|
||
*
|
||
* @param other - The value to compare against.
|
||
* @returns `true` if `this ≤ other`, `false` otherwise.
|
||
*
|
||
* @public
|
||
*/
|
||
lte(other) {
|
||
return this.cmp(other) <= 0;
|
||
}
|
||
/**
|
||
* Return `true` if this value is strictly greater than `other`.
|
||
*
|
||
* @param other - The value to compare against.
|
||
* @returns `true` if `this \> other`, `false` otherwise.
|
||
*
|
||
* @public
|
||
*/
|
||
gt(other) {
|
||
return this.cmp(other) === 1;
|
||
}
|
||
/**
|
||
* Return `true` if this value is greater than or equal to `other`.
|
||
*
|
||
* @param other - The value to compare against.
|
||
* @returns `true` if `this ≥ other`, `false` otherwise.
|
||
*
|
||
* @public
|
||
*/
|
||
gte(other) {
|
||
return this.cmp(other) >= 0;
|
||
}
|
||
// -------------------------------------------------------------------
|
||
// Predicates
|
||
// -------------------------------------------------------------------
|
||
/**
|
||
* Return `true` if this value is exactly zero.
|
||
*
|
||
* @returns `true` if the value is zero, `false` otherwise.
|
||
*
|
||
* @public
|
||
*/
|
||
isZero() {
|
||
return this._coefficient === 0n;
|
||
}
|
||
/**
|
||
* Return `true` if this value is strictly less than zero.
|
||
*
|
||
* @returns `true` if negative, `false` if zero or positive.
|
||
*
|
||
* @public
|
||
*/
|
||
isNegative() {
|
||
return this._coefficient < 0n;
|
||
}
|
||
/**
|
||
* Return `true` if this value is strictly greater than zero.
|
||
*
|
||
* @returns `true` if positive, `false` if zero or negative.
|
||
*
|
||
* @public
|
||
*/
|
||
isPositive() {
|
||
return this._coefficient > 0n;
|
||
}
|
||
// -------------------------------------------------------------------
|
||
// Unary operations
|
||
// -------------------------------------------------------------------
|
||
/**
|
||
* Return the additive inverse of this value.
|
||
*
|
||
* @returns A new {@link Decimal} equal to `-this`.
|
||
*
|
||
* @public
|
||
*/
|
||
neg() {
|
||
return new DecimalImpl(-this._coefficient, this._exponent);
|
||
}
|
||
/**
|
||
* Return the absolute value.
|
||
*
|
||
* @returns A new {@link Decimal} equal to `|this|`. If this value is
|
||
* already non-negative, returns `this` (no allocation).
|
||
*
|
||
* @public
|
||
*/
|
||
abs() {
|
||
if (this._coefficient < 0n) {
|
||
return new DecimalImpl(-this._coefficient, this._exponent);
|
||
}
|
||
return this;
|
||
}
|
||
// -------------------------------------------------------------------
|
||
// Rounding
|
||
// -------------------------------------------------------------------
|
||
/**
|
||
* Round this value to a specified precision.
|
||
*
|
||
* @remarks
|
||
* **Rounding directions** (IEEE 754-2019 §4.3):
|
||
*
|
||
* | Direction | Behavior |
|
||
* | -------------- | ---------------------------------------------- |
|
||
* | `'ceil'` | 1.1→2, -1.1→-1, 1.0→1 (toward +∞) |
|
||
* | `'floor'` | 1.9→1, -1.1→-2, 1.0→1 (toward -∞) |
|
||
* | `'round-down'` | 1.9→1, -1.9→-1 (toward zero / truncate) |
|
||
* | `'round-up'` | 1.1→2, -1.1→-2 (away from zero) |
|
||
* | `'half-up'` | 0.5→1, 1.5→2, -0.5→-1 (ties away from zero) |
|
||
* | `'half-down'` | 0.5→0, 1.5→1, -0.5→0 (ties toward zero) |
|
||
* | `'half-even'` | 0.5→0, 1.5→2, 2.5→2, 3.5→4 (ties to even) |
|
||
*
|
||
* **Precision** is specified as a {@link DecimalRoundingOptions} object
|
||
* or a preset name from {@link DecimalRoundingPresets}:
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* // Using a preset
|
||
* amount.round('half-even', 'v1-api');
|
||
*
|
||
* // Using explicit options
|
||
* amount.round('half-even', { mode: 'decimal-places', value: 2 });
|
||
* amount.round('half-up', { mode: 'significant-figures', value: 4 });
|
||
* ```
|
||
*
|
||
* @param direction - How to round.
|
||
* @param options - A {@link DecimalRoundingOptions} object or key of {@link DecimalRoundingPresets}.
|
||
* @returns A new {@link Decimal} rounded to the specified precision.
|
||
* @throws {@link Error} if `options.value` is negative or non-integer.
|
||
* @throws {@link Error} if the preset name is not recognized.
|
||
*
|
||
* @public
|
||
*/
|
||
round(direction, options) {
|
||
const resolved = typeof options === 'string'
|
||
? // Declaration merging allows consumers to add keys at compile time, but
|
||
// ROUNDING_PRESETS only knows about built-in keys at runtime. The double
|
||
// cast through `unknown` is intentional: we want an undefined-safe lookup
|
||
// so the runtime guard below can produce a clear error for unrecognised
|
||
// (e.g. declaration-merged) preset names that were not also added to
|
||
// ROUNDING_PRESETS.
|
||
ROUNDING_PRESETS[options]
|
||
: options;
|
||
if (resolved === undefined) {
|
||
throw new Error(`Unknown rounding preset: "${options}"`);
|
||
}
|
||
if (resolved.value < 0 || !Number.isInteger(resolved.value)) {
|
||
throw new Error('DecimalRoundingOptions.value must be a non-negative integer');
|
||
}
|
||
if (resolved.mode === 'decimal-places') {
|
||
// Reuse toFixed logic: round to resolved.value decimal places then re-parse.
|
||
const fixed = this.toFixed(resolved.value, direction);
|
||
return exports.Decimal.from(fixed);
|
||
}
|
||
// significant-figures: round to resolved.value total significant digits.
|
||
if (this._coefficient === 0n) {
|
||
return this;
|
||
}
|
||
const coeffStr = this._coefficient < 0n
|
||
? (-this._coefficient).toString()
|
||
: this._coefficient.toString();
|
||
const currentSigFigs = coeffStr.length;
|
||
if (resolved.value === 0) {
|
||
// 0 significant figures is a degenerate case — return zero.
|
||
return exports.Decimal.zero;
|
||
}
|
||
if (currentSigFigs <= resolved.value) {
|
||
// Already at or below requested precision — no rounding needed.
|
||
return this;
|
||
}
|
||
// We need to reduce the number of significant figures.
|
||
// The number of digits to drop from the coefficient:
|
||
const digitsToTrim = currentSigFigs - resolved.value;
|
||
const divisor = 10n ** BigInt(digitsToTrim);
|
||
const quotient = this._coefficient / divisor;
|
||
const remainder = this._coefficient % divisor;
|
||
const rounded = DecimalImpl.roundDivision(quotient, remainder, divisor, direction);
|
||
// The new exponent shifts to account for trimmed digits.
|
||
return new DecimalImpl(rounded, this._exponent + digitsToTrim);
|
||
}
|
||
// -------------------------------------------------------------------
|
||
// Conversion / serialisation
|
||
// -------------------------------------------------------------------
|
||
/**
|
||
* Return a human-readable string representation.
|
||
*
|
||
* @remarks
|
||
* Plain notation for values whose digit count is at most 30, and
|
||
* scientific notation (`1.23E+40`) for larger values. Trailing zeros
|
||
* are never present because the internal representation is normalised.
|
||
*
|
||
* @public
|
||
*/
|
||
toString() {
|
||
if (this._coefficient === 0n) {
|
||
return '0';
|
||
}
|
||
const coeffStr = this._coefficient.toString();
|
||
const isNeg = coeffStr.startsWith('-');
|
||
const absCoeffStr = isNeg ? coeffStr.slice(1) : coeffStr;
|
||
if (this._exponent < 0) {
|
||
const decimalPlaces = -this._exponent;
|
||
// Guard against unbounded string allocation for extreme negative
|
||
// exponents (e.g. 1e-1000000 would otherwise produce a million-char
|
||
// string of leading zeros). Switch to scientific notation when the
|
||
// number of leading zeros alone exceeds the digit limit. Normal
|
||
// fractional values (e.g. 34-digit division results) pass through.
|
||
const leadingZeroCount = decimalPlaces >= absCoeffStr.length
|
||
? decimalPlaces - absCoeffStr.length
|
||
: 0;
|
||
if (leadingZeroCount > PLAIN_NOTATION_DIGIT_LIMIT) {
|
||
if (absCoeffStr.length === 1) {
|
||
return `${coeffStr}E${String(this._exponent)}`;
|
||
}
|
||
const intPart = absCoeffStr[0] ?? '';
|
||
const fracPart = absCoeffStr.slice(1);
|
||
const adjustedExp = this._exponent + absCoeffStr.length - 1;
|
||
return `${isNeg ? '-' : ''}${intPart}.${fracPart}E${String(adjustedExp)}`;
|
||
}
|
||
if (decimalPlaces >= absCoeffStr.length) {
|
||
const leadingZeros = '0'.repeat(decimalPlaces - absCoeffStr.length);
|
||
return `${isNeg ? '-' : ''}0.${leadingZeros}${absCoeffStr}`;
|
||
}
|
||
else {
|
||
const integerPart = absCoeffStr.slice(0, absCoeffStr.length - decimalPlaces);
|
||
const fractionalPart = absCoeffStr.slice(absCoeffStr.length - decimalPlaces);
|
||
return `${isNeg ? '-' : ''}${integerPart}.${fractionalPart}`;
|
||
}
|
||
}
|
||
const plainLength = absCoeffStr.length + this._exponent;
|
||
if (plainLength <= PLAIN_NOTATION_DIGIT_LIMIT) {
|
||
if (this._exponent === 0) {
|
||
return coeffStr;
|
||
}
|
||
const trailingZeros = '0'.repeat(this._exponent);
|
||
return `${isNeg ? '-' : ''}${absCoeffStr}${trailingZeros}`;
|
||
}
|
||
else {
|
||
if (absCoeffStr.length === 1) {
|
||
return `${coeffStr}E+${String(this._exponent)}`;
|
||
}
|
||
const integerPart = absCoeffStr[0] ?? '';
|
||
const fractionalPart = absCoeffStr.slice(1);
|
||
const adjustedExponent = this._exponent + absCoeffStr.length - 1;
|
||
return `${isNeg ? '-' : ''}${integerPart}.${fractionalPart}E+${String(adjustedExponent)}`;
|
||
}
|
||
}
|
||
/**
|
||
* Return the JSON-serialisable representation.
|
||
*
|
||
* @remarks
|
||
* Returns a plain string matching the Stripe API convention where
|
||
* decimal values are serialised as strings in JSON. Called
|
||
* automatically by `JSON.stringify`.
|
||
*
|
||
* @public
|
||
*/
|
||
toJSON() {
|
||
return this.toString();
|
||
}
|
||
/**
|
||
* Convert to a JavaScript `number`.
|
||
*
|
||
* @remarks
|
||
* This is an explicit, intentionally lossy conversion. Use it only
|
||
* when you need a numeric value for display or interop with APIs
|
||
* that require `number`. Prefer {@link Decimal.toString | toString}
|
||
* or {@link Decimal.toFixed | toFixed} for lossless output.
|
||
*
|
||
* @public
|
||
*/
|
||
toNumber() {
|
||
return Number(this.toString());
|
||
}
|
||
/**
|
||
* Format this value as a fixed-point string with exactly
|
||
* `decimalPlaces` digits after the decimal point.
|
||
*
|
||
* @remarks
|
||
* Values are rounded according to `direction` when the internal
|
||
* precision exceeds the requested number of decimal places.
|
||
* The rounding direction is always required — no invisible defaults
|
||
* in financial code.
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* Decimal.from('1.235').toFixed(2, 'half-up'); // "1.24"
|
||
* Decimal.from('1.225').toFixed(2, 'half-even'); // "1.22"
|
||
* Decimal.from('42').toFixed(3, 'half-up'); // "42.000"
|
||
* ```
|
||
*
|
||
* @param decimalPlaces - Number of digits after the decimal point.
|
||
* Must be a non-negative integer.
|
||
* @param direction - How to round when truncating excess digits.
|
||
* @returns A string with exactly `decimalPlaces` fractional digits.
|
||
* @throws {@link Error} if `decimalPlaces` is negative or non-integer.
|
||
*
|
||
* @public
|
||
*/
|
||
toFixed(decimalPlaces, direction) {
|
||
if (decimalPlaces < 0 || !Number.isInteger(decimalPlaces)) {
|
||
throw new Error('decimalPlaces must be a non-negative integer');
|
||
}
|
||
const formatFixed = (coef) => {
|
||
const coeffStr = coef.toString();
|
||
const isNeg = coeffStr.startsWith('-');
|
||
const absCoeffStr = isNeg ? coeffStr.slice(1) : coeffStr;
|
||
if (decimalPlaces === 0) {
|
||
return coeffStr;
|
||
}
|
||
if (decimalPlaces >= absCoeffStr.length) {
|
||
const leadingZeros = '0'.repeat(decimalPlaces - absCoeffStr.length);
|
||
return `${isNeg ? '-' : ''}0.${leadingZeros}${absCoeffStr}`;
|
||
}
|
||
else {
|
||
const integerPart = absCoeffStr.slice(0, absCoeffStr.length - decimalPlaces);
|
||
const fractionalPart = absCoeffStr.slice(absCoeffStr.length - decimalPlaces);
|
||
return `${isNeg ? '-' : ''}${integerPart}.${fractionalPart}`;
|
||
}
|
||
};
|
||
const targetExponent = -decimalPlaces;
|
||
if (this._exponent === targetExponent) {
|
||
return formatFixed(this._coefficient);
|
||
}
|
||
if (this._exponent < targetExponent) {
|
||
// Need to reduce precision — round the excess digits.
|
||
const scaleDiff = targetExponent - this._exponent;
|
||
const divisor = 10n ** BigInt(scaleDiff);
|
||
const quotient = this._coefficient / divisor;
|
||
const remainder = this._coefficient % divisor;
|
||
const rounded = DecimalImpl.roundDivision(quotient, remainder, divisor, direction);
|
||
return formatFixed(rounded);
|
||
}
|
||
else {
|
||
// Need to increase precision — pad with trailing zeros.
|
||
const scaleDiff = this._exponent - targetExponent;
|
||
const scaled = this._coefficient * 10n ** BigInt(scaleDiff);
|
||
return formatFixed(scaled);
|
||
}
|
||
}
|
||
/**
|
||
* Return a string primitive when the runtime coerces the value.
|
||
*
|
||
* @remarks
|
||
* Deliberately returns a `string` (not a `number`) to discourage
|
||
* silent precision loss through implicit arithmetic coercion.
|
||
* When used in a numeric context (for example, `+myDecimal`), the
|
||
* JavaScript runtime will first call this method and then coerce
|
||
* the resulting string to a `number`, which may lose precision.
|
||
* Callers should prefer the explicit
|
||
* {@link Decimal.toNumber | toNumber} method when an IEEE 754
|
||
* `number` is required.
|
||
*
|
||
* @public
|
||
*/
|
||
valueOf() {
|
||
return this.toString();
|
||
}
|
||
}
|
||
/**
|
||
* Check whether a value is a {@link Decimal} instance.
|
||
*
|
||
* @remarks
|
||
* Use this instead of `instanceof` — the underlying class is not
|
||
* publicly exported, so `instanceof` checks are not available to
|
||
* consumers.
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* if (isDecimal(value)) {
|
||
* value.add(Decimal.from('1')); // value is Decimal
|
||
* }
|
||
* ```
|
||
*
|
||
* @public
|
||
*/
|
||
function isDecimal(value) {
|
||
return value instanceof DecimalImpl;
|
||
}
|
||
exports.isDecimal = isDecimal;
|
||
/**
|
||
* Companion object for creating {@link Decimal} instances.
|
||
*
|
||
* @public
|
||
*/
|
||
exports.Decimal = {
|
||
/**
|
||
* Create a {@link Decimal} from a string, number, or bigint.
|
||
*
|
||
* @remarks
|
||
* - **string**: Parsed as a decimal literal. Accepts an optional sign,
|
||
* integer digits, an optional fractional part, and an optional `e`/`E`
|
||
* exponent. Leading/trailing whitespace is trimmed.
|
||
* - **number**: Must be finite. Converted via `Number.prototype.toString()`
|
||
* then parsed, so `Decimal.from(0.1)` produces `"0.1"` (not the
|
||
* 53-bit binary approximation).
|
||
* - **bigint**: Treated as an integer with exponent 0.
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* Decimal.from('1.23'); // string
|
||
* Decimal.from(42); // number
|
||
* Decimal.from(100n); // bigint
|
||
* Decimal.from('1.5e3'); // scientific notation → 1500
|
||
* ```
|
||
*
|
||
* @param value - The value to convert.
|
||
* @returns A new frozen {@link Decimal} instance.
|
||
* @throws {@link Error} if `value` is a non-finite number, an empty
|
||
* string, or a string that does not match the decimal literal grammar.
|
||
*
|
||
* @public
|
||
*/
|
||
from(value) {
|
||
if (typeof value === 'bigint') {
|
||
return new DecimalImpl(value, 0);
|
||
}
|
||
if (typeof value === 'number') {
|
||
if (!Number.isFinite(value)) {
|
||
throw new Error('Number must be finite');
|
||
}
|
||
return exports.Decimal.from(value.toString());
|
||
}
|
||
// Parse string.
|
||
const trimmed = value.trim();
|
||
if (trimmed === '') {
|
||
throw new Error('Cannot parse empty string as Decimal');
|
||
}
|
||
// Match: optional sign, integer digits, optional fraction, optional exponent.
|
||
const match = /^([+-]?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(trimmed);
|
||
if (!match) {
|
||
throw new Error(`Invalid decimal string: ${value}`);
|
||
}
|
||
const sign = match[1] === '-' ? -1n : 1n;
|
||
const integerPart = match[2] ?? '';
|
||
const fractionalPart = match[3] ?? '';
|
||
const exponentPart = match[4] ? Number(match[4]) : 0;
|
||
if (!Number.isSafeInteger(exponentPart) ||
|
||
exponentPart > MAX_EXPONENT ||
|
||
exponentPart < -MAX_EXPONENT) {
|
||
throw new Error(`Exponent out of range: ${String(match[4])} exceeds safe integer bounds`);
|
||
}
|
||
const coefficientStr = integerPart + fractionalPart;
|
||
const coefficient = sign * BigInt(coefficientStr);
|
||
const exponent = exponentPart - fractionalPart.length;
|
||
if (!Number.isSafeInteger(exponent) ||
|
||
exponent > MAX_EXPONENT ||
|
||
exponent < -MAX_EXPONENT) {
|
||
throw new Error(`Computed exponent out of range: ${String(exponent)} exceeds safe integer bounds`);
|
||
}
|
||
return new DecimalImpl(coefficient, exponent);
|
||
},
|
||
/**
|
||
* The {@link Decimal} value representing zero.
|
||
*
|
||
* @remarks
|
||
* Pre-allocated singleton — prefer `Decimal.zero` over
|
||
* `Decimal.from(0)` to avoid an unnecessary allocation.
|
||
*
|
||
* @public
|
||
*/
|
||
zero: new DecimalImpl(0n, 0),
|
||
};
|
||
//# sourceMappingURL=Decimal.js.map
|