Initial project import
This commit is contained in:
855
node_modules/stripe/cjs/Decimal.js
generated
vendored
Normal file
855
node_modules/stripe/cjs/Decimal.js
generated
vendored
Normal file
@@ -0,0 +1,855 @@
|
||||
"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
|
||||
Reference in New Issue
Block a user