UNPKG

@sovryn-zero/lib-base

Version:
457 lines (364 loc) 12.1 kB
import assert from "assert"; import { BigNumber } from "@ethersproject/bignumber"; const getDigits = (numDigits: number) => TEN.pow(numDigits); const MAX_UINT_256 = "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; const PRECISION = 18; const ONE = BigNumber.from(1); const TEN = BigNumber.from(10); const DIGITS = getDigits(PRECISION); const stringRepresentationFormat = /^[0-9]*(\.[0-9]*)?(e[-+]?[0-9]+)?$/; const trailingZeros = /0*$/; const magnitudes = ["", "K", "M", "B", "T"]; const roundedMul = (x: BigNumber, y: BigNumber) => x.mul(y).add(Decimal.HALF.hex).div(DIGITS); /** * Types that can be converted into a Decimal. * * @public */ export type Decimalish = Decimal | number | string; /** * Fixed-point decimal bignumber with 18 digits of precision. * * @remarks * Used by Zero libraries to precisely represent native currency (e.g. Ether), ZUSD and ZERO * amounts, as well as derived metrics like collateral ratios. * * @public */ export class Decimal { static readonly INFINITY = Decimal.fromBigNumberString(MAX_UINT_256); static readonly ZERO = Decimal.from(0); static readonly HALF = Decimal.from(0.5); static readonly ONE = Decimal.from(1); private readonly _bigNumber: BigNumber; /** @internal */ get hex(): string { return this._bigNumber.toHexString(); } /** @internal */ get bigNumber(): string { return this._bigNumber.toString(); } private constructor(bigNumber: BigNumber) { if (bigNumber.isNegative()) { throw new Error("negatives not supported by Decimal"); } this._bigNumber = bigNumber; } static fromBigNumberString(bigNumberString: string): Decimal { return new Decimal(BigNumber.from(bigNumberString)); } private static _fromString(representation: string): Decimal { if (!representation || !representation.match(stringRepresentationFormat)) { throw new Error(`bad decimal format: "${representation}"`); } if (representation.includes("e")) { // eslint-disable-next-line prefer-const let [coefficient, exponent] = representation.split("e"); if (exponent.startsWith("-")) { return new Decimal( Decimal._fromString(coefficient)._bigNumber.div( TEN.pow(BigNumber.from(exponent.substr(1))) ) ); } if (exponent.startsWith("+")) { exponent = exponent.substr(1); } return new Decimal( Decimal._fromString(coefficient)._bigNumber.mul(TEN.pow(BigNumber.from(exponent))) ); } if (!representation.includes(".")) { return new Decimal(BigNumber.from(representation).mul(DIGITS)); } // eslint-disable-next-line prefer-const let [characteristic, mantissa] = representation.split("."); if (mantissa.length < PRECISION) { mantissa += "0".repeat(PRECISION - mantissa.length); } else { mantissa = mantissa.substr(0, PRECISION); } return new Decimal( BigNumber.from(characteristic || 0) .mul(DIGITS) .add(mantissa) ); } static from(decimalish: Decimalish): Decimal { switch (typeof decimalish) { case "object": if (decimalish instanceof Decimal) { return decimalish; } else { throw new Error("invalid Decimalish value"); } case "string": return Decimal._fromString(decimalish); case "number": return Decimal._fromString(decimalish.toString()); default: throw new Error("invalid Decimalish value"); } } private _toStringWithAutomaticPrecision() { const characteristic = this._bigNumber.div(DIGITS); const mantissa = this._bigNumber.mod(DIGITS); if (mantissa.isZero()) { return characteristic.toString(); } else { const paddedMantissa = mantissa.toString().padStart(PRECISION, "0"); const trimmedMantissa = paddedMantissa.replace(trailingZeros, ""); return characteristic.toString() + "." + trimmedMantissa; } } private _roundUp(precision: number) { const halfDigit = getDigits(PRECISION - 1 - precision).mul(5); return this._bigNumber.add(halfDigit); } private _toStringWithPrecision(precision: number) { if (precision < 0) { throw new Error("precision must not be negative"); } const value = precision < PRECISION ? this._roundUp(precision) : this._bigNumber; const characteristic = value.div(DIGITS); const mantissa = value.mod(DIGITS); if (precision === 0) { return characteristic.toString(); } else { const paddedMantissa = mantissa.toString().padStart(PRECISION, "0"); const trimmedMantissa = paddedMantissa.substr(0, precision); return characteristic.toString() + "." + trimmedMantissa; } } toString(precision?: number): string { if (this.infinite) { return "∞"; } else if (precision !== undefined) { return this._toStringWithPrecision(precision); } else { return this._toStringWithAutomaticPrecision(); } } prettify(precision = 2): string { const [characteristic, mantissa] = parseFloat(this.toString(precision)).toString().split("."); const prettyCharacteristic = characteristic.replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,"); return mantissa !== undefined ? prettyCharacteristic + "." + mantissa : prettyCharacteristic; } shorten(): string { const characteristicLength = this.toString(0).length; const magnitude = Math.min(Math.floor((characteristicLength - 1) / 3), magnitudes.length - 1); const precision = Math.max(3 * (magnitude + 1) - characteristicLength, 0); const normalized = this.div(new Decimal(getDigits(PRECISION + 3 * magnitude))); return normalized.prettify(precision) + magnitudes[magnitude]; } add(addend: Decimalish): Decimal { return new Decimal(this._bigNumber.add(Decimal.from(addend)._bigNumber)); } sub(subtrahend: Decimalish): Decimal { return new Decimal(this._bigNumber.sub(Decimal.from(subtrahend)._bigNumber)); } mul(multiplier: Decimalish): Decimal { return new Decimal(this._bigNumber.mul(Decimal.from(multiplier)._bigNumber).div(DIGITS)); } div(divider: Decimalish): Decimal { divider = Decimal.from(divider); if (divider.isZero) { return Decimal.INFINITY; } return new Decimal(this._bigNumber.mul(DIGITS).div(divider._bigNumber)); } /** @internal */ _divCeil(divider: Decimalish): Decimal { divider = Decimal.from(divider); if (divider.isZero) { return Decimal.INFINITY; } return new Decimal( this._bigNumber.mul(DIGITS).add(divider._bigNumber.sub(ONE)).div(divider._bigNumber) ); } mulDiv(multiplier: Decimalish, divider: Decimalish): Decimal { multiplier = Decimal.from(multiplier); divider = Decimal.from(divider); if (divider.isZero) { return Decimal.INFINITY; } return new Decimal(this._bigNumber.mul(multiplier._bigNumber).div(divider._bigNumber)); } pow(exponent: number): Decimal { assert(Number.isInteger(exponent)); assert(0 <= exponent && exponent <= 0xffffffff); // Ensure we're safe to use bitwise ops if (exponent === 0) { return Decimal.ONE; } if (exponent === 1) { return this; } let x = this._bigNumber; let y = DIGITS; for (; exponent > 1; exponent >>>= 1) { if (exponent & 1) { y = roundedMul(x, y); } x = roundedMul(x, x); } return new Decimal(roundedMul(x, y)); } get isZero(): boolean { return this._bigNumber.isZero(); } get zero(): this | undefined { if (this.isZero) { return this; } } get nonZero(): this | undefined { if (!this.isZero) { return this; } } get infinite(): this | undefined { if (this.eq(Decimal.INFINITY)) { return this; } } get finite(): this | undefined { if (!this.eq(Decimal.INFINITY)) { return this; } } /** @internal */ get absoluteValue(): this { return this; } lt(that: Decimalish): boolean { return this._bigNumber.lt(Decimal.from(that)._bigNumber); } eq(that: Decimalish): boolean { return this._bigNumber.eq(Decimal.from(that)._bigNumber); } gt(that: Decimalish): boolean { return this._bigNumber.gt(Decimal.from(that)._bigNumber); } gte(that: Decimalish): boolean { return this._bigNumber.gte(Decimal.from(that)._bigNumber); } lte(that: Decimalish): boolean { return this._bigNumber.lte(Decimal.from(that)._bigNumber); } static min(a: Decimalish, b: Decimalish): Decimal { a = Decimal.from(a); b = Decimal.from(b); return a.lt(b) ? a : b; } static max(a: Decimalish, b: Decimalish): Decimal { a = Decimal.from(a); b = Decimal.from(b); return a.gt(b) ? a : b; } } type DifferenceRepresentation = { sign: "" | "+" | "-"; absoluteValue: Decimal }; /** @alpha */ export class Difference { private _number?: DifferenceRepresentation; private constructor(number?: DifferenceRepresentation) { this._number = number; } static between(d1: Decimalish | undefined, d2: Decimalish | undefined): Difference { if (d1 === undefined || d2 === undefined) { return new Difference(undefined); } d1 = Decimal.from(d1); d2 = Decimal.from(d2); if (d1.infinite && d2.infinite) { return new Difference(undefined); } else if (d1.infinite) { return new Difference({ sign: "+", absoluteValue: d1 }); } else if (d2.infinite) { return new Difference({ sign: "-", absoluteValue: d2 }); } else if (d1.gt(d2)) { return new Difference({ sign: "+", absoluteValue: Decimal.from(d1).sub(d2) }); } else if (d2.gt(d1)) { return new Difference({ sign: "-", absoluteValue: Decimal.from(d2).sub(d1) }); } else { return new Difference({ sign: "", absoluteValue: Decimal.ZERO }); } } toString(precision?: number): string { if (!this._number) { return "N/A"; } return this._number.sign + this._number.absoluteValue.toString(precision); } prettify(precision?: number): string { if (!this._number) { return this.toString(); } return this._number.sign + this._number.absoluteValue.prettify(precision); } mul(multiplier: Decimalish): Difference { return new Difference( this._number && { sign: this._number.sign, absoluteValue: this._number.absoluteValue.mul(multiplier) } ); } get nonZero(): this | undefined { return this._number?.absoluteValue.nonZero && this; } get positive(): this | undefined { return this._number?.sign === "+" ? this : undefined; } get negative(): this | undefined { return this._number?.sign === "-" ? this : undefined; } get absoluteValue(): Decimal | undefined { return this._number?.absoluteValue; } get infinite(): this | undefined { return this._number?.absoluteValue.infinite && this; } get finite(): this | undefined { return this._number?.absoluteValue.finite && this; } } /** @alpha */ export class Percent< T extends { infinite?: T | undefined; absoluteValue?: A | undefined; mul?(hundred: 100): T; toString(precision?: number): string; }, A extends { gte(n: string): boolean; } > { private _percent: T; public constructor(ratio: T) { this._percent = ratio.infinite || (ratio.mul && ratio.mul(100)) || ratio; } nonZeroish(precision: number): this | undefined { const zeroish = `0.${"0".repeat(precision)}5`; if (this._percent.absoluteValue?.gte(zeroish)) { return this; } } toString(precision: number): string { return ( this._percent.toString(precision) + (this._percent.absoluteValue && !this._percent.infinite ? "%" : "") ); } prettify(): string { if (this._percent.absoluteValue?.gte("1000")) { return this.toString(0); } else if (this._percent.absoluteValue?.gte("10")) { return this.toString(1); } else { return this.toString(2); } } }