UNPKG

@tempus-labs/utils

Version:

Tempus utility helpers

292 lines 12.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Decimal = exports.decimal = exports.DEFAULT_DECIMAL_PRECISION = void 0; /** * Matches the most common ERC20 18-decimals precision, such as wETH */ exports.DEFAULT_DECIMAL_PRECISION = 18; const ZERO = BigInt(0); /** * Creates a new `Decimal` Fixed-Point Decimal type * The default precision is 18 decimals. * @warning Any EXCESS digits will ALWAYS be truncated, not rounded! * @param value Number-like value to convert to Decimal * @param decimals Decimals precision after the fraction, excess is truncated */ function decimal(value, decimals = exports.DEFAULT_DECIMAL_PRECISION) { return new Decimal(value, decimals); } exports.decimal = decimal; /** * @abstract A Fixed-Point Decimal type with a strongly defined `decimals` precision * compatible with ERC20.decimals() concept. */ class Decimal { /** * Creates a new `Decimal` Fixed-Point Decimal type with fixed decimals precision * @warning Any EXCESS digits will ALWAYS be truncated, not rounded! * @param value Any Number-like value. * BigInt passes through without any scaling. * Decimal types are upscaled/downscaled to this decimals precision * @param decimals Fixed count of fractional decimal digits, eg 18 or 6. * Excess digits are truncated. */ constructor(value, decimals) { this.decimals = decimals; this.int = Decimal.toScaledBigInt(value, decimals); Object.freeze(this); } /** @returns Scaled BigInt from this Decimal */ toBigInt() { return this.int; } /** @returns Numberish converted to this Decimal precision bigint */ toScaledBigInt(x) { return Decimal.toScaledBigInt(x, this.decimals); } /** @returns Number(x) converted to Decimal this precision */ toDecimal(x) { return new Decimal(x, this.decimals); } /** @returns Decimal(this) converted to `decimals` precision */ toPrecision(decimals) { return new Decimal(this, decimals); } /** @returns Decimal toString with full fractional part */ toString() { return this._toString(this.decimals); } /*** @returns Decimal toString with fractional part truncated to maxDecimals */ toTruncated(maxDecimals = 0) { return this._toString(maxDecimals); } /*** @returns Decimal toString with fractional part truncated to maxDecimals */ toRounded(maxDecimals) { return this._toString(maxDecimals, true); } /** * @param padZeroes If set, pads the front of the hex string to specific width * @returns Scaled Bigint converted to an Ethers suitable HEX string */ toHexString(padZeroes) { const hex = this.int.toString(16); return '0x' + (padZeroes ? hex.padStart(padZeroes, '0') : hex); } /*** @returns JSON representation of this Decimal as { type: "Decimal", value: "1.000000" } */ toJSON(key) { return { type: "Decimal", value: this.toString() }; } /** * Converts this Decimal into a string and parses it as a number * Some precision loss will occur for big decimals * @returns This Decimal FixedPoint converted to a number. */ toNumber() { return Number(this.toString()); } /** * @brief To compare if two Decimals or Numbers are equal. * For chai asserts use deep equality * Ex: expect(a).to.eql(b); -- chai deep equality check * Ex: expect(a.equals(b)).to.be.true; -- directly call equals * Ex: expect(decimal(1.0).equals(1.0)).to.be.true; */ equals(other) { if (other instanceof Decimal) return this.decimals === other.decimals && this.int === other.int; return this.int === this.toScaledBigInt(other); } /** * @brief Alias of this.equals() */ eq(other) { return this.equals(other); } /** * @brief To enable quick conversion from Decimal to a number * Ex: let x:string = +myDecimal; * Ex: expect(+myDecimal).to.equal(100); */ valueOf() { return this.toNumber(); } /** 1.0 expressed as a scaled bigint of this decimals precision */ one() { return Decimal._one(this.decimals); } static _one(decimals) { let one = Decimal.ONE_CACHE[decimals]; if (!one) { one = BigInt("1".padEnd(decimals + 1, "0")); Decimal.ONE_CACHE[decimals] = one; } return one; } /** @return decimal(this) + decimal(x) */ add(x) { return this.toDecimal(this.int + this.toScaledBigInt(x)); } /** @return decimal(this) - decimal(x) */ sub(x) { return this.toDecimal(this.int - this.toScaledBigInt(x)); } /** @return decimal(this) * decimal(x) */ mul(x) { // mulf = (a * b) / ONE return this.toDecimal((this.int * this.toScaledBigInt(x)) / this.one()); } /** @return decimal(this) / decimal(x) */ div(x) { // divf = (a * ONE) / b return this.toDecimal((this.int * this.one()) / this.toScaledBigInt(x)); } /** @return Absolute value of this decimal */ abs() { return new Decimal((this.int >= 0 ? this.int : -this.int), this.decimals); } /** @return TRUE if Decimal(this) > Decimal(x) */ gt(x) { return this.int > this.toScaledBigInt(x); } /** @return TRUE if Decimal(this) < Decimal(x) */ lt(x) { return this.int < this.toScaledBigInt(x); } /** @return TRUE if Decimal(this) >= Decimal(x) */ gte(x) { return this.int >= this.toScaledBigInt(x); } /** @return TRUE if Decimal(this) <= Decimal(x) */ lte(x) { return this.int <= this.toScaledBigInt(x); } isZero() { return this.int === ZERO; } /** * Main utility for converting numeric values into the internal * scaled fixed point integer representation. * The numeric `value` is scaled by pow(10, decimals), eg. 5.0 * 10^6 = bigint(5_000_000) * @param value Any kind of numberish value * @param decimals Fixed Point decimals precision, eg 18 or 6 * @returns BigInt scaled to decimals precision, * ex: toScaledBigInt(1.0, 6) => bigint(1_000_000) */ static toScaledBigInt(value, decimals) { if (typeof (value) === "bigint") { return value; // accept BigInt without any validation, this is necessary to enable raw interop } if (value._isBigNumber) { // ethers.js compatibility, treat ethers.BigNumber as a BigInt return BigInt(value.toString()); } // for Decimal types we can perform optimized upscaling/downscaling fastpaths if (value instanceof Decimal) { if (value.decimals === decimals) { return value.int; // this is a no-op case } else if (value.decimals > decimals) { // incoming needs to be truncated const downscaleDigits = value.decimals - decimals; return value.int / this._one(downscaleDigits); } else { // incoming needs to be upscaled const upscaleDigits = decimals - value.decimals; return value.int * this._one(upscaleDigits); } } // get the string representation of the Numberish value const val = value.toString(); // figure out if there is a fractional part to it and get the Whole part const decimalIdx = val.indexOf('.'); const whole = val.slice(0, decimalIdx === -1 ? val.length : decimalIdx); if (decimals === 0) { // pure integer case, TRUNCATE any decimals return BigInt(whole); } if (decimalIdx === -1) { // input was integer eg "1234" return BigInt(val.padEnd(val.length + decimals, '0')); } // input was a decimal eg "123.456" (pad trail) or "1.23456789" (truncate fract) // extract the fractional part of the decimal string up to `decimals` count of // characters (truncating excess), or up to end of the string (pad trail) const fract = val.slice(decimalIdx + 1, Math.min(decimalIdx + 1 + decimals, val.length)); // if it's not long enough, create a trail of zeroes to satisfy decimals precision const trail = decimals > fract.length ? decimals - fract.length : 0; return BigInt(whole + fract.padEnd(fract.length + trail, '0')); } _toString(maxDecimals, round = false) { if (this.decimals === 0) { return this.int.toString(); } // get the BigInt digits and check if it's negative const neg = this.int < 0; const abs = (neg ? this.int * BigInt(-1) : this.int).toString(); // split the BigInt digits into whole and fractional parts const gotWhole = abs.length > this.decimals; // is BigInt >= Decimal(1.000000) const whole = gotWhole ? abs.slice(0, abs.length - this.decimals) : '0'; // if -1, then auto set the decimals value maxDecimals = maxDecimals === -1 ? this.decimals : maxDecimals; if (maxDecimals <= 0) { // truncate fraction return Decimal.toDecimalString(neg, whole); } const f = gotWhole ? abs.slice(abs.length - this.decimals) : abs.padStart(this.decimals, '0'); if (round) { const roundedFract = Decimal.roundFraction(f, maxDecimals); return Decimal.toDecimalString(neg, whole, roundedFract); } else if (maxDecimals !== this.decimals) { // truncate the trailing fraction const truncationIdx = Math.min(maxDecimals, f.length); const truncatedFract = f.slice(0, truncationIdx); return Decimal.toDecimalString(neg, whole, truncatedFract); } else { return Decimal.toDecimalString(neg, whole, f); } } /** @return Decimal string from sign, whole part and fraction part */ static toDecimalString(neg, whole, fract) { const signPart = neg ? '-' : ''; const wholePart = whole ? whole : '0'; const decPoint = fract ? '.' : ''; const fractPart = fract ? fract : ''; return signPart + wholePart + decPoint + fractPart; } /** * Rounds the trailing fraction at the boundary of `maxDecimals` * ex: roundFraction('004555', 3) -> '005' * ex: roundFraction('004000', 3) -> '004' */ static roundFraction(f, maxDecimals) { const truncationIdx = Math.min(maxDecimals, f.length); if (f.length === truncationIdx) { // nothing to round, just trim it return Decimal.trimFraction(f); } // convert the fract part into a Number and round it at the truncation point const toRound = f.slice(0, truncationIdx) + '.' + f.slice(truncationIdx); const rounded = Math.round(Number(toRound)).toString(); // make sure we pad back to correct length const fractPart = rounded.padStart(truncationIdx, '0'); return Decimal.trimFraction(fractPart); } /** * Trims a fraction string by removing trailing zeroes * ex: '00000' -> '' * ex: '04000' -> '.04' */ static trimFraction(fraction) { let end = fraction.length; while (end > 0 && fraction.charAt(end - 1) === '0') --end; return end === 0 ? '' : fraction.slice(0, end); } } exports.Decimal = Decimal; /** @dev Decimal implementation version number */ Decimal.version = 2; Decimal.ONE_CACHE = { 6: BigInt("1000000"), 18: BigInt("1000000000000000000"), }; //# sourceMappingURL=Decimal.js.map