UNPKG

@vechain/sdk-core

Version:

This module is crafted for dApp development and various blockchain operations that seamlessly unfold offline

1,083 lines (1,024 loc) 42.7 kB
import { InvalidDataType, InvalidOperation } from '@vechain/sdk-errors'; import { type VeChainDataModel } from './VeChainDataModel'; import { Txt } from './Txt'; /** * Represents a fixed-point number for precision arithmetic. */ class FixedPointNumber implements VeChainDataModel<FixedPointNumber> { /** * Base of value notation. */ private static readonly BASE = 10n; /** * The default number of decimal places to use for fixed-point math. * * @see * [bignumber.js DECIMAL_PLACES](https://mikemcl.github.io/bignumber.js/#decimal-places) * * @constant {bigint} */ protected static readonly DEFAULT_FRACTIONAL_DECIMALS = 20n; /** * Not a Number. * * @remarks {@link fractionalDigits} and {@link scaledValue} not meaningful. * * @see [Number.NaN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/NaN) * */ public static readonly NaN = new FixedPointNumber(0n, 0n, NaN); /** * The negative Infinity value. * * @remarks {@link fractionalDigits} and {@link scaledValue} not meaningful. * * @see [Number.NEGATIVE_INFINITY](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/NEGATIVE_INFINITY) */ public static readonly NEGATIVE_INFINITY = new FixedPointNumber( 0n, 0n, Number.NEGATIVE_INFINITY ); /** * Represents the one constant. */ public static readonly ONE = FixedPointNumber.of(1n); /** * The positive Infinite value. * * @remarks {@link fractionalDigits} and {@link scaledValue} not meaningful. * * @see [Number.POSITIVE_INFINITY](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/POSITIVE_INFINITY) */ public static readonly POSITIVE_INFINITY = new FixedPointNumber( 0n, 0n, Number.POSITIVE_INFINITY ); /** * Regular expression pattern for matching integers expressed as base 10 strings. */ private static readonly REGEX_INTEGER: RegExp = /^[-+]?\d+$/; /** * Regular expression for matching numeric values expressed as base 10 strings. */ private static readonly REGEX_NUMBER = /(^[-+]?\d+(\.\d+)?)$|(^[-+]?\.\d+)$/; /** * Regular expression pattern for matching natural numbers expressed as base 10 strings. */ private static readonly REGEX_NATURAL: RegExp = /^\d+$/; /** * Represents the zero constant. */ public static readonly ZERO = new FixedPointNumber(0n, 0n, 0); /** * Edge Flag denotes the {@link NaN} or {@link NEGATIVE_INFINITY} or {@link POSITIVE_INFINITY} value. * * @remarks If `ef` is not zero, {@link fractionalDigits} and {@link scaledValue} are not meaningful. */ protected readonly edgeFlag: number; /** * Fractional Digits or decimal places. * * @see [bignumber.js precision](https://mikemcl.github.io/bignumber.js/#sd) */ public readonly fractionalDigits: bigint; /** * Scaled Value = value * 10 ^ {@link fractionalDigits}. */ public readonly scaledValue: bigint; /** * Returns the integer part of this FixedPointNumber value. * * @return {bigint} the integer part of this FixedPointNumber value. * * @throws {InvalidOperation} If the value is not finite. */ get bi(): bigint { if (this.isFinite()) { return ( this.scaledValue / FixedPointNumber.BASE ** this.fractionalDigits ); } throw new InvalidOperation( 'FixedPointNumber.bi', 'not finite value cannot cast to big integer', { this: this.toString() } ); } /** * Returns the array of bytes representing the *Normalization Form Canonical Composition* * [Unicode Equivalence](https://en.wikipedia.org/wiki/Unicode_equivalence) * of this value expressed in decimal base. */ get bytes(): Uint8Array { return Txt.of(this.toString()).bytes; } /** * Return this value approximated as {@link number}. */ get n(): number { if (this.isNaN()) return Number.NaN; if (this.isNegativeInfinite()) return Number.NEGATIVE_INFINITY; if (this.isPositiveInfinite()) return Number.POSITIVE_INFINITY; if (this.isZero()) return 0; return Number(this.scaledValue) * 10 ** -Number(this.fractionalDigits); } /** * Returns the new Fixed-Point Number (FixedPointNumber) instance having * * @param {bigint} fd - Number of Fractional Digits (or decimal places). * @param {bigint} sv - Scaled Value. * @param {number} [ef=0] - Edge Flag. */ protected constructor(fd: bigint, sv: bigint, ef: number = 0) { this.fractionalDigits = fd; this.edgeFlag = ef; this.scaledValue = sv; } /** * Returns a FixedPointNumber whose value is the absolute value, i.e. the magnitude, of the value of this FixedPointNumber. * * @return {FixedPointNumber} the absolute value of this FixedPointNumber. * * @see [bignumber.js absoluteValue](https://mikemcl.github.io/bignumber.js/#abs) */ public abs(): FixedPointNumber { if (this.isNaN()) return FixedPointNumber.NaN; if (this.isNegativeInfinite()) return FixedPointNumber.POSITIVE_INFINITY; return new FixedPointNumber( this.fractionalDigits, this.scaledValue < 0n ? -this.scaledValue : this.scaledValue, this.edgeFlag ); } /** * Compares this instance with `that` FixedPointNumber instance. * * Returns 0 if this is equal to `that` FixedPointNumber, including infinite with equal sign; * * Returns -1, if this is -Infinite or less than `that` FixedPointNumber;, * * Returns 1 if this is +Infinite or greater than `that` FixedPointNumber. * * @param {FixedPointNumber} that - The instance to compare with this instance. * @return {number} Returns -1, 0, or 1 if this instance is less than, equal to, or greater * than the specified instance, respectively. * @throw InvalidOperation If this or `that` FixedPointNumber is {@link NaN}. * * @see [bignumber.js comparedTo](https://mikemcl.github.io/bignumber.js/#cmp) */ public compareTo(that: FixedPointNumber): number { if (this.isNaN() || that.isNaN()) throw new InvalidOperation( 'FixedPointNumber.compareTo', 'compare between NaN', { this: `${this}`, that: `${that}` } ); if (this.isNegativeInfinite()) return that.isNegativeInfinite() ? 0 : -1; if (this.isPositiveInfinite()) return that.isPositiveInfinite() ? 0 : 1; if (that.isNegativeInfinite()) return 1; if (that.isPositiveInfinite()) return -1; const fd = this.maxFractionalDigits(that, this.fractionalDigits); // Max common fractional decimals. const delta = this.dp(fd).scaledValue - that.dp(fd).scaledValue; return delta < 0n ? -1 : delta === 0n ? 0 : 1; } /** * Compares this instance with `that` FixedPointNumber instance. * * **Returns `null` if either instance is NaN;** * * Returns 0 if this is equal to `that` FixedPointNumber, including infinite with equal sign; * * Returns -1, if this is -Infinite or less than `that` FixedPointNumber;, * * Returns 1 if this is +Infinite or greater than `that` FixedPointNumber. * * @param {FixedPointNumber} that - The instance to compare with this instance. * @return {null | number} A null if either instance is NaN; * -1, 0, or 1 if this instance is less than, equal to, or greater * than the specified instance, respectively. * * @remarks This method uses internally {@link compareTo} wrapping the {@link InvalidOperation} exception * when comparing between {@link NaN} values to behave according the * [[bignumber.js comparedTo](https://mikemcl.github.io/bignumber.js/#cmp)] rules. */ public comparedTo(that: FixedPointNumber): null | number { try { return this.compareTo(that); } catch { return null; } } /** * Returns a FixedPointNumber whose value is the value of this FixedPointNumber divided by `that` FixedPointNumber. * * Limit cases * * 0 / 0 = NaN * * NaN / ±n = NaN * * ±Infinity / ±Infinity = NaN * * +n / NaN = NaN * * +n / ±Infinity = 0 * * -n / 0 = -Infinity * * +n / 0 = +Infinity * * @param {FixedPointNumber} that - The fixed-point number to divide by. * @return {FixedPointNumber} The result of the division. * * @remarks The precision is the greater of the precision of the two operands. * * @see [bignumber.js dividedBy](https://mikemcl.github.io/bignumber.js/#div) */ public div(that: FixedPointNumber): FixedPointNumber { if (this.isNaN() || that.isNaN()) return FixedPointNumber.NaN; if (this.isNegativeInfinite()) { if (that.isInfinite()) return FixedPointNumber.NaN; if (that.isPositive()) return FixedPointNumber.NEGATIVE_INFINITY; return FixedPointNumber.POSITIVE_INFINITY; } if (this.isPositiveInfinite()) { if (that.isInfinite()) return FixedPointNumber.NaN; if (that.isPositive()) return FixedPointNumber.POSITIVE_INFINITY; return FixedPointNumber.NEGATIVE_INFINITY; } if (that.isInfinite()) return FixedPointNumber.ZERO; if (that.isZero()) { if (this.isZero()) return FixedPointNumber.NaN; if (this.isNegative()) return FixedPointNumber.NEGATIVE_INFINITY; return FixedPointNumber.POSITIVE_INFINITY; } const fd = this.maxFractionalDigits(that, this.fractionalDigits); // Max common fractional decimals. return new FixedPointNumber( fd, FixedPointNumber.div( fd, this.dp(fd).scaledValue, that.dp(fd).scaledValue ) ).dp(this.fractionalDigits); // Minimize fractional decimals without precision loss. } /** * Divides the given dividend by the given divisor, adjusted by a factor based on fd. * * @param {bigint} fd - The factor determining the power of 10 to apply to the dividend. * @param {bigint} dividend - The number to be divided. * @param {bigint} divisor - The number by which to divide the dividend. * * @return {bigint} - The result of the division, adjusted by the given factor fd. */ private static div(fd: bigint, dividend: bigint, divisor: bigint): bigint { return (FixedPointNumber.BASE ** fd * dividend) / divisor; } /** * Adjust the precision of the floating-point number by the specified * number of decimal places. * * @param decimalPlaces The number of decimal places to adjust to, * it must be a positive value. * @return {FixedPointNumber} A new FixedPointNumber instance with the adjusted precision. * @throws InvalidDataType if `decimalPlaces` is negative. */ public dp(decimalPlaces: bigint | number): FixedPointNumber { const dp = BigInt(decimalPlaces); if (dp >= 0) { let fd = this.fractionalDigits; let sv = this.scaledValue; if (dp > fd) { // Scale up. sv *= FixedPointNumber.BASE ** (dp - fd); fd = dp; } else { // Scale down. while (fd > dp && sv % FixedPointNumber.BASE === 0n) { fd--; sv /= FixedPointNumber.BASE; } } return new FixedPointNumber(fd, sv, this.edgeFlag); } throw new InvalidDataType( 'FixedPointNumber.scale', 'negative `dp` arg', { dp: `${dp}` } ); } /** * Returns `true `if the value of thisFPN is equal to the value of `that` FixedPointNumber, otherwise returns `false`. * * As with JavaScript, `NaN` does not equal `NaN`. * * @param {FixedPointNumber} that - The FixedPointNumber to compare against. * @return {boolean} `true` if the FixedPointNumber numbers are equal, otherwise `false`. * * @remarks This method uses {@link comparedTo} internally. * * @see [bigbumber.js isEqualTo](https://mikemcl.github.io/bignumber.js/#eq) */ public eq(that: FixedPointNumber): boolean { return this.comparedTo(that) === 0; } /** * Returns `true` if the value of this FixedPointNumber is greater than `that` FixedPointNumber`, otherwise returns `false`. * * @param {FixedPointNumber} that The FixedPointNumber to compare against. * @return {boolean} `true` if this FixedPointNumber is greater than `that` FixedPointNumber, otherwise `false`. * * @remarks This method uses {@link comparedTo} internally. * * @see [bignummber.js isGreaterThan](https://mikemcl.github.io/bignumber.js/#gt) */ public gt(that: FixedPointNumber): boolean { const cmp = this.comparedTo(that); return cmp !== null && cmp > 0; } /** * Returns `true` if the value of this FixedPointNumber is greater or equal than `that` FixedPointNumber`, otherwise returns `false`. * * @param {FixedPointNumber} that - The FixedPointNumber to compare against. * @return {boolean} `true` if this FixedPointNumber is greater or equal than `that` FixedPointNumber, otherwise `false`. * * @remarks This method uses {@link comparedTo} internally. * * @see [bignumber.js isGreaterThanOrEqualTo](https://mikemcl.github.io/bignumber.js/#gte) */ public gte(that: FixedPointNumber): boolean { const cmp = this.comparedTo(that); return cmp !== null && cmp >= 0; } /** * Returns a fixed-point number whose value is the integer part of dividing the value of this fixed-point number * by `that` fixed point number. * * Limit cases * * 0 / 0 = NaN * * NaN / ±n = NaN * * ±Infinity / ±Infinity = NaN * * +n / NaN = NaN * * +n / ±Infinite = 0 * * -n / 0 = -Infinite * * +n / 0 = +Infinite * * @param {FixedPointNumber} that - The fixed-point number to divide by. * @return {FixedPointNumber} The result of the division. * * @remarks The precision is the greater of the precision of the two operands. * * @see [bignumber.js dividedToIntegerBy](https://mikemcl.github.io/bignumber.js/#divInt) */ public idiv(that: FixedPointNumber): FixedPointNumber { if (this.isNaN() || that.isNaN()) return FixedPointNumber.NaN; if (this.isNegativeInfinite()) { if (that.isInfinite()) return FixedPointNumber.NaN; if (that.isPositive()) return FixedPointNumber.NEGATIVE_INFINITY; return FixedPointNumber.POSITIVE_INFINITY; } if (this.isPositiveInfinite()) { if (that.isInfinite()) return FixedPointNumber.NaN; if (that.isPositive()) return FixedPointNumber.POSITIVE_INFINITY; return FixedPointNumber.NEGATIVE_INFINITY; } if (that.isInfinite()) return FixedPointNumber.ZERO; if (that.isZero()) { if (this.isZero()) return FixedPointNumber.NaN; if (this.isNegative()) return FixedPointNumber.NEGATIVE_INFINITY; return FixedPointNumber.POSITIVE_INFINITY; } const fd = this.maxFractionalDigits(that, this.fractionalDigits); // Max common fractional decimals. return new FixedPointNumber( fd, FixedPointNumber.idiv( fd, this.dp(fd).scaledValue, that.dp(fd).scaledValue ) ).dp(this.fractionalDigits); // Minimize fractional decimals without precision loss. } /** * Performs integer division on two big integers and scales the result by a factor of 10 raised to the power of fd. * * @param {bigint} fd - The power to which 10 is raised to scale the result. * @param {bigint} dividend - The number to be divided. * @param {bigint} divisor - The number by which dividend is divided. * @return {bigint} - The scaled result of the integer division. */ private static idiv(fd: bigint, dividend: bigint, divisor: bigint): bigint { return (dividend / divisor) * FixedPointNumber.BASE ** fd; } /** * Returns `true `if the value of thisFPN is equal to the value of `that` FixedPointNumber, otherwise returns `false`. * * As with JavaScript, `NaN` does not equal `NaN`. * * @param {FixedPointNumber} that - The FixedPointNumber to compare against. * @return {boolean} `true` if the FixedPointNumber numbers are equal, otherwise `false`. * * @remarks This method uses {@link eq} internally. */ public isEqual(that: FixedPointNumber): boolean { return this.eq(that); } /** * Returns `true` if the value of this FixedPointNumber is a finite number, otherwise returns `false`. * * The only possible non-finite values of a FixedPointNumber are {@link NaN}, {@link NEGATIVE_INFINITY} and {@link POSITIVE_INFINITY}. * * @return `true` if the value of this FixedPointNumber is a finite number, otherwise returns `false`. * * @see [bignumber.js isFinite](https://mikemcl.github.io/bignumber.js/#isF) */ public isFinite(): boolean { return this.edgeFlag === 0; } /** * Return `true` if the value of this FixedPointNumber is {@link NEGATIVE_INFINITY} or {@link POSITIVE_INFINITY}, * otherwise returns false. * * @return true` if the value of this FixedPointNumber is {@link NEGATIVE_INFINITY} or {@link POSITIVE_INFINITY}, */ public isInfinite(): boolean { return this.isNegativeInfinite() || this.isPositiveInfinite(); } /** * Returns `true` if the value of this FixedPointNumber is an integer, * otherwise returns `false`. * * @return `true` if the value of this FixedPointNumber is an integer. * * @see [bignumber.js isInteger](https://mikemcl.github.io/bignumber.js/#isInt) */ public isInteger(): boolean { if (this.isFinite()) { return ( this.scaledValue % FixedPointNumber.BASE ** this.fractionalDigits === 0n ); } return false; } /** * Checks if a given string expression is an integer in base 10 notation, * considering `-` for negative and `+` optional for positive values. * * @param {string} exp - The string expression to be tested. * * @return {boolean} `true` if the expression is an integer, * `false` otherwise. */ public static isIntegerExpression(exp: string): boolean { return this.REGEX_INTEGER.test(exp); } /** * Returns `true` if the value of this FixedPointNumber is `NaN`, otherwise returns `false`. * * @return `true` if the value of this FixedPointNumber is `NaN`, otherwise returns `false`. * * @see [bignumber.js isNaN](https://mikemcl.github.io/bignumber.js/#isNaN) */ public isNaN(): boolean { return Number.isNaN(this.edgeFlag); } /** * Checks if a given string expression is a natural (unsigned positive integer) * number in base 10 notation. * * @param {string} exp - The string expression to be tested. * * @return {boolean} `true` if the expression is a natural number, * `false` otherwise. */ public static isNaturalExpression(exp: string): boolean { return this.REGEX_NATURAL.test(exp); } /** * Returns `true` if the sign of this FixedPointNumber is negative, otherwise returns `false`. * * @return `true` if the sign of this FixedPointNumber is negative, otherwise returns `false`. * * @see [bignumber.js isNegative](https://mikemcl.github.io/bignumber.js/#isNeg) */ public isNegative(): boolean { return ( (this.isFinite() && this.scaledValue < 0n) || this.isNegativeInfinite() ); } /** * Returns `true` if this FixedPointNumber value is {@link NEGATIVE_INFINITY}, otherwise returns `false`. */ public isNegativeInfinite(): boolean { return this.edgeFlag === Number.NEGATIVE_INFINITY; } /** * Checks if a given string expression is a number in base 10 notation, * considering `-` for negative and `+` optional for positive values. * * The method returns `true` for the following cases. * - Whole numbers: * - Positive whole numbers, optionally signed: 1, +2, 3, ... * - Negative whole numbers: -1, -2, -3, ... * - Decimal numbers: * - Positive decimal numbers, optionally signed: 1.0, +2.5, 3.14, ... * - Negative decimal numbers: -1.0, -2.5, -3.14, ... * - Decimal numbers without whole part: * - Positive decimal numbers, optionally signed: .1, +.5, .75, ... * - Negative decimal numbers: -.1, -.5, -.75, ... * * @param exp - The string expression to be checked. * * @return `true` is `exp` represents a number, otherwise `false`. */ public static isNumberExpression(exp: string): boolean { return FixedPointNumber.REGEX_NUMBER.test(exp); } /** * Returns `true` if the sign of this FixedPointNumber is positive, otherwise returns `false`. * * @return `true` if the sign of this FixedPointNumber is positive, otherwise returns `false`. * * @see [bignumber.js isPositive](https://mikemcl.github.io/bignumber.js/#isPos) */ public isPositive(): boolean { return ( (this.isFinite() && this.scaledValue >= 0n) || this.isPositiveInfinite() ); } /** * Returns `true` if this FixedPointNumber value is {@link POSITIVE_INFINITY}, otherwise returns `false`. * * @return `true` if this FixedPointNumber value is {@link POSITIVE_INFINITY}, otherwise returns `false`. */ public isPositiveInfinite(): boolean { return this.edgeFlag === Number.POSITIVE_INFINITY; } /** * Returns `true` if the value of this FixedPointNumber is zero or minus zero, otherwise returns `false`. * * @return `true` if the value of this FixedPointNumber is zero or minus zero, otherwise returns `false`. * * [see bignumber.js isZero](https://mikemcl.github.io/bignumber.js/#isZ) */ public isZero(): boolean { return this.isFinite() && this.scaledValue === 0n; } /** * Returns `true` if the value of this FixedPointNumber is less than the value of `that` FixedPointNumber, otherwise returns `false`. * * @param {FixedPointNumber} that - The FixedPointNumber to compare against. * * @return {boolean} `true` if the value of this FixedPointNumber is less than the value of `that` FixedPointNumber, otherwise returns `false`. * * @remarks This method uses {@link comparedTo} internally. * * @see [bignumber.js isLessThan](https://mikemcl.github.io/bignumber.js/#lt) */ public lt(that: FixedPointNumber): boolean { const cmp = this.comparedTo(that); return cmp !== null && cmp < 0; } /** * Returns `true` if the value of this FixedPointNumber is less than or equal to the value of `that` FixedPointNumber, * otherwise returns `false`. * * @param {FixedPointNumber} that - The FixedPointNumber to compare against. * @return {boolean} `true` if the value of this FixedPointNumber is less than or equal to the value of `that` FixedPointNumber, * otherwise returns `false`. * * @remarks This method uses {@link comparedTo} internally. * * @see [bignumber.js isLessThanOrEqualTo](https://mikemcl.github.io/bignumber.js/#lte) */ public lte(that: FixedPointNumber): boolean { const cmp = this.comparedTo(that); return cmp !== null && cmp <= 0; } /** * Return the maximum between the fixed decimal value of this object and `that` one. * If the maximum fixed digits value is less than `minFixedDigits`, return `minFixedDigits`. * * @param {FixedPointNumber} that to evaluate if `that` has the maximum fixed digits value. * @param {bigint} minFixedDigits Min value of returned value. * * @return the greater fixed digits value among `this`, `that` and `minFixedDigits`. */ private maxFractionalDigits( that: FixedPointNumber, minFixedDigits: bigint ): bigint { const fd = this.fractionalDigits < that.fractionalDigits ? that.fractionalDigits : this.fractionalDigits; return fd > minFixedDigits ? fd : minFixedDigits; } /** * Returns a FixedPointNumber whose value is the value of this FixedPointNumber minus `that` FixedPointNumber. * * Limit cases * * NaN - ±n = NaN * * ±n - NaN = NaN * * -Infinity - -Infinity = NaN * * -Infinity - +n = -Infinity * * +Infinity - +Infinity = NaN * * +Infinity - +n = +Infinity * * @param {FixedPointNumber} that - The fixed-point number to subtract. * @return {FixedPointNumber} The result of the subtraction. The return value is always exact and unrounded. * * @remarks The precision is the greater of the precision of the two operands. * * @see [bignumber.js minus](https://mikemcl.github.io/bignumber.js/#minus) */ public minus(that: FixedPointNumber): FixedPointNumber { if (this.isNaN() || that.isNaN()) return FixedPointNumber.NaN; if (this.isNegativeInfinite()) return that.isNegativeInfinite() ? FixedPointNumber.NaN : FixedPointNumber.NEGATIVE_INFINITY; if (this.isPositiveInfinite()) return that.isPositiveInfinite() ? FixedPointNumber.NaN : FixedPointNumber.POSITIVE_INFINITY; const fd = this.maxFractionalDigits(that, this.fractionalDigits); // Max common fractional decimals. return new FixedPointNumber( fd, this.dp(fd).scaledValue - that.dp(fd).scaledValue ).dp(this.fractionalDigits); // Minimize fractional decimals without precision loss. } /** * Returns a FixedPointNumber whose value is the value of this FixedPointNumber modulo `that` FixedPointNumber, * i.e. the integer remainder of dividing this FixedPointNumber by `that`. * * Limit cases * * NaN % ±n = NaN * * ±n % NaN = NaN * * ±Infinity % n = NaN * * n % ±Infinity = NaN * * @param that {FixedPointNumber} - The fixed-point number to divide by. * @return {FixedPointNumber} the integer remainder of dividing this FixedPointNumber by `that`. * * @remarks The precision is the greater of the precision of the two operands. * * @see [bignumber.js modulo](https://mikemcl.github.io/bignumber.js/#mod) */ public modulo(that: FixedPointNumber): FixedPointNumber { if (this.isNaN() || that.isNaN()) return FixedPointNumber.NaN; if (this.isInfinite() || that.isInfinite()) return FixedPointNumber.NaN; if (that.isZero()) return FixedPointNumber.NaN; const fd = this.maxFractionalDigits(that, this.fractionalDigits); // Max common fractional decimals. let modulo = this.abs().dp(fd).scaledValue; const divisor = that.abs().dp(fd).scaledValue; while (modulo >= divisor) { modulo -= divisor; } return new FixedPointNumber(fd, modulo).dp(this.fractionalDigits); // Minimize fractional decimals without precision loss. } /** * Multiplies two big integer values and divides by a factor of ten raised to a specified power. * * @param {bigint} multiplicand - The first number to be multiplied. * @param {bigint} multiplicator - The second number to be multiplied. * @param {bigint} fd - The power of ten by which the product is to be divided. * * @return {bigint} The result of the multiplication divided by ten raised to the specified power. */ private static mul( multiplicand: bigint, multiplicator: bigint, fd: bigint ): bigint { return (multiplicand * multiplicator) / FixedPointNumber.BASE ** fd; } /** * Returns a new instance of FixedPointNumber whose value is the value of this FixedPointNumber value * negated, i.e. multiplied by -1. * * @see [bignumber.js negated](https://mikemcl.github.io/bignumber.js/#neg) */ public negated(): FixedPointNumber { if (this.isNegativeInfinite()) return FixedPointNumber.POSITIVE_INFINITY; if (this.isPositiveInfinite()) return FixedPointNumber.NEGATIVE_INFINITY; return new FixedPointNumber( this.fractionalDigits, -this.scaledValue, this.edgeFlag ); } /** * Constructs a new instance of FixedPointNumber (Fixed Point Number) parsing the * `exp` numeric expression in base 10 and representing the value with the * precision of `decimalPlaces` fractional decimal digits. * * @param {bigint|number|string} exp - The value to represent. * It can be a bigint, number, or string representation of the number. * @param {bigint} [decimalPlaces=this.DEFAULT_FRACTIONAL_DECIMALS] - The * number of fractional decimal digits to be used to represent the value. * * @return {FixedPointNumber} A new instance of FixedPointNumber with the given parameters. * * @throws {InvalidDataType} If `exp` is not a numeric expression. */ public static of( exp: bigint | number | string | FixedPointNumber, decimalPlaces: bigint = this.DEFAULT_FRACTIONAL_DECIMALS ): FixedPointNumber { try { if (exp instanceof FixedPointNumber) { return new FixedPointNumber( exp.fractionalDigits, exp.scaledValue, exp.edgeFlag ); } if (Number.isNaN(exp)) return new FixedPointNumber(decimalPlaces, 0n, Number.NaN); if (exp === Number.NEGATIVE_INFINITY) return new FixedPointNumber( decimalPlaces, -1n, Number.NEGATIVE_INFINITY ); if (exp === Number.POSITIVE_INFINITY) return new FixedPointNumber( decimalPlaces, 1n, Number.POSITIVE_INFINITY ); return new FixedPointNumber( decimalPlaces, this.txtToSV(exp.toString(), decimalPlaces) ); } catch (e) { throw new InvalidDataType( 'FixedPointNumber.of', 'not a number', { exp }, e ); } } /** * Returns a FixedPointNumber whose value is the value of this FixedPointNumber plus `that` FixedPointNumber. * * Limit cases * * NaN + ±n = NaN * * ±n + NaN = NaN * * -Infinity + -Infinity = -Infinity * * -Infinity + +Infinity = NaN * * +Infinity + -Infinity = NaN * * +Infinity + +Infinity = +Infinity * * @param {FixedPointNumber} that - The fixed-point number to add to the current number. * @return {FixedPointNumber} The result of the addition. The return value is always exact and unrounded. * * @remarks The precision is the greater of the precision of the two operands. * * @see [bignumber.js plus](https://mikemcl.github.io/bignumber.js/#plus) */ public plus(that: FixedPointNumber): FixedPointNumber { if (this.isNaN() || that.isNaN()) return FixedPointNumber.NaN; if (this.isNegativeInfinite()) return that.isPositiveInfinite() ? FixedPointNumber.NaN : FixedPointNumber.NEGATIVE_INFINITY; if (this.isPositiveInfinite()) return that.isNegativeInfinite() ? FixedPointNumber.NaN : FixedPointNumber.POSITIVE_INFINITY; const fd = this.maxFractionalDigits(that, this.fractionalDigits); // Max common fractional decimals. return new FixedPointNumber( fd, this.dp(fd).scaledValue + that.dp(fd).scaledValue ).dp(this.fractionalDigits); // Minimize fractional decimals without precision loss. } /** * Returns a FixedPointNumber whose value is the value of this FixedPointNumber raised to the power of `that` FixedPointNumber. * * This method implements the * [Exponentiation by Squaring](https://en.wikipedia.org/wiki/Exponentiation_by_squaring) * algorithm. * * Limit cases * * NaN ^ e = NaN * * b ^ NaN = NaN * * b ^ -Infinite = 0 * * b ^ 0 = 1 * * b ^ +Infinite = +Infinite * * ±Infinite ^ -e = 0 * * ±Infinite ^ +e = +Infinite * * @param {FixedPointNumber} that - The exponent as a fixed-point number. * truncated to its integer component because **Exponentiation by Squaring** is not valid for rational exponents. * @return {FixedPointNumber} - The result of raising this fixed-point number to the power of the given exponent. * * @see [bignumber.js exponentiatedBy](https://mikemcl.github.io/bignumber.js/#pow) */ public pow(that: FixedPointNumber): FixedPointNumber { // Limit cases if (this.isNaN() || that.isNaN()) return FixedPointNumber.NaN; if (this.isInfinite()) return that.isZero() ? FixedPointNumber.ONE : that.isNegative() ? FixedPointNumber.ZERO : FixedPointNumber.POSITIVE_INFINITY; if (that.isNegativeInfinite()) return FixedPointNumber.ZERO; if (that.isPositiveInfinite()) { return FixedPointNumber.POSITIVE_INFINITY; } if (that.isZero()) return FixedPointNumber.ONE; // Exponentiation by squaring works for natural exponent value. let exponent = that.abs().bi; let base = FixedPointNumber.of(this); let result = FixedPointNumber.ONE; while (exponent > 0n) { // If the exponent is odd, multiply the result by the current base. if (exponent % 2n === 1n) { result = result.times(base); } // Square the base and halve the exponent. base = base.times(base); exponent = exponent / 2n; } // If exponent is negative, convert the problem to positive exponent. return that.isNegative() ? FixedPointNumber.ONE.div(result) : result; } /** * Computes the square root of a given positive bigint value using a fixed-point iteration method. * * @param {bigint} value - The positive bigint value for which the square root is to be calculated. * @param {bigint} fd - The iteration factor determinant. * @return {bigint} The calculated square root of the input bigint value. * * @throws {RangeError} If the input value is negative. */ private static sqr(value: bigint, fd: bigint): bigint { if (value < 0n) { throw new RangeError(`Value must be positive`); } const sf = fd * FixedPointNumber.BASE; // Scale Factor. let iteration = 0; let actualResult = value; let storedResult = 0n; while (actualResult !== storedResult && iteration < sf) { storedResult = actualResult; actualResult = (actualResult + FixedPointNumber.div(fd, value, actualResult)) / 2n; iteration++; } return actualResult; } /** * Returns a FixedPointNumber whose value is the square root of the value of this FixedPointNumber * * Limit cases * * NaN = NaN * * +Infinite = +Infinite * * -n = NaN * * @return {FixedPointNumber} The square root of the number. * * @see [bignumber.js sqrt](https://mikemcl.github.io/bignumber.js/#sqrt) */ public sqrt(): FixedPointNumber { if (this.isNaN()) return FixedPointNumber.NaN; if (this.isNegativeInfinite()) return FixedPointNumber.NaN; if (this.isPositiveInfinite()) return FixedPointNumber.POSITIVE_INFINITY; try { return new FixedPointNumber( this.fractionalDigits, FixedPointNumber.sqr(this.scaledValue, this.fractionalDigits) ); } catch { return FixedPointNumber.NaN; } } /** * Returns a FixedPointNumber whose value is the value of this FixedPointNumber multiplied by `that` FixedPointNumber. * * Limits cases * * NaN * n = NaN * * n * NaN = NaN * * -Infinite * -n = +Infinite * * -Infinite * +n = -Infinite * * +Infinite * -n = -Infinite * * +Infinite * +n = +Infinite * * @param {FixedPointNumber} that - The fixed-point number to multiply with this number. * @return {FixedPointNumber} a FixedPointNumber whose value is the value of this FixedPointNumber multiplied by `that` FixedPointNumber. * * @remarks The precision is the greater of the precision of the two operands. * * @see [bignumber.js multipliedBy](https://mikemcl.github.io/bignumber.js/#times) */ public times(that: FixedPointNumber): FixedPointNumber { if (this.isNaN() || that.isNaN()) return FixedPointNumber.NaN; if (this.isNegativeInfinite()) return that.isNegative() ? FixedPointNumber.POSITIVE_INFINITY : FixedPointNumber.NEGATIVE_INFINITY; if (this.isPositiveInfinite()) return that.isNegative() ? FixedPointNumber.NEGATIVE_INFINITY : FixedPointNumber.POSITIVE_INFINITY; const fd = this.fractionalDigits > that.fractionalDigits ? this.fractionalDigits : that.fractionalDigits; // Max common fractional decimals. return new FixedPointNumber( fd, FixedPointNumber.mul( this.dp(fd).scaledValue, that.dp(fd).scaledValue, fd ) ).dp(this.fractionalDigits); // Minimize fractional decimals without precision loss. } /** * Converts the fixed-point number to its string representation. * * @param {string} [decimalSeparator='.'] - The character to use as the decimal separator in the string representation. Default is '.'. * @return {string} A string representation of the fixed-point number. */ public toString(decimalSeparator = '.'): string { if (this.edgeFlag === 0) { const sign = this.scaledValue < 0n ? '-' : ''; const digits = this.scaledValue < 0n ? (-this.scaledValue).toString() : this.scaledValue.toString(); const padded = digits.padStart(Number(this.fractionalDigits), '0'); const decimals = this.fractionalDigits > 0 ? padded.slice(Number(-this.fractionalDigits)) : ''; const integers = padded.slice(0, padded.length - decimals.length); const integersShow = integers.length < 1 ? '0' : integers; const decimalsShow = FixedPointNumber.trimEnd(decimals); return ( sign + integersShow + (decimalsShow.length > 0 ? decimalSeparator + decimalsShow : '') ); } return this.edgeFlag.toString(); } /** * Trims the specified trailing substring from the end of the input string recursively. * * @param {string} str - The input string to be trimmed. * @param {string} [sub='0'] - The substring to be removed from the end of the input string. Defaults to '0' if not provided. * @return {string} The trimmed string with the specified trailing substring removed. */ private static trimEnd(str: string, sub: string = '0'): string { // Check if the input string ends with the trailing substring if (str.endsWith(sub)) { // Remove the trailing substring recursively. return FixedPointNumber.trimEnd( str.substring(0, str.length - sub.length), sub ); } return str; } /** * Converts a string expression of a number into a scaled value. * * @param {string} exp - The string expression of the number to be converted. * @param {bigint} fd - The scale factor to be used for conversion. * @param {string} [decimalSeparator='.'] - The character used as the decimal separator in the string expression. * @return {bigint} - The converted scaled value as a bigint. */ private static txtToSV( exp: string, fd: bigint, decimalSeparator = '.' ): bigint { const fc = exp.charAt(0); // First Character. let sign = 1n; if (fc === '-') { sign = -1n; exp = exp.substring(1); } else if (fc === '+') { exp = exp.substring(1); } const sf = FixedPointNumber.BASE ** fd; // Scale Factor. const di = exp.lastIndexOf(decimalSeparator); // Decimal Index. if (di < 0) { return sign * sf * BigInt(exp); // Signed Integer. } const ie = exp.substring(0, di); // Integer Expression. const fe = exp.substring(di + 1); // Fractional Expression. return ( sign * sf * BigInt(ie) + // Integer part (sign * (sf * BigInt(fe))) / BigInt(10 ** fe.length) // Fractional part. ); } } export { FixedPointNumber };