Files
2026-06-13 17:36:44 -07:00

855 lines
32 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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