UNPKG

0xweb

Version:

Contract package manager and other web3 tools

562 lines (504 loc) 17.7 kB
import { $bigint } from './$bigint'; import { $require } from './$require'; type TNumeric = BigFloat | string | number | bigint; // impl. https://github.com/MikeMcl/bignumber.js enum EBigFloatType { FLOAT, POSITIVE_INFINITY, NEGATIVE_INFINITY, NAN, } export class BigFloat { constructor(public value: bigint, public mantissa: bigint = 1n) { } toNumber () { return Number(this.value)/Number(this.mantissa); } toString () { let isNegative = this.value < 0n; let int = this.value / this.mantissa; let str = `${int}`; let [toExpMin, toExpMax] = CONFIG.EXPONENTIAL_AT; let fractional = $bigint.abs(this.value % this.mantissa); if (fractional > 0n) { let digitsMantissa = $num.getDigits(this.mantissa) - 1; let digitsFractional = $num.getDigits(fractional) - 1; let fractionalStr = $bigint.abs(fractional).toString().padStart(digitsMantissa, '0').replace(/0+$/, ''); str += `.${fractionalStr}`; if (int === 0n && digitsFractional - digitsMantissa <= toExpMin) { // to exponential notation; let diff = digitsMantissa - digitsFractional; fractionalStr = $bigint.abs(fractional).toString().replace(/0+$/, ''); let arr = [ fractionalStr[0] ]; if (fractionalStr.length > 1) { arr.push(fractionalStr.substring(1)) } str = `${arr.join('.')}e-${diff}`; } } let intDigits = $num.getDigits(int); if (intDigits > toExpMax) { // to exponential notation; let [ intPart, fractionalPart ] = str.split('.'); let intPartN, fractionalPartN; if (isNegative) { intPartN = `-${intPart[1]}` fractionalPartN = `${intPart.substring(2)}${fractionalPart ?? ''}`; } else { intPartN = intPart[0]; fractionalPartN = `${intPart.substring(1)}${fractionalPart ?? ''}`; } fractionalPartN = fractionalPartN.replace(/0+$/, ''); str = `${intPartN}.${fractionalPartN}e+${intDigits - 1}`; } if (isNegative && int === 0n && fractional > 0n) { str = `-${str}`; } return str; } toBigInt () { let int = this.value / this.mantissa; return int; } toJSON () { return this.toString(); } valueOf () { return this.toString(); } toFormat (locales?: string | string[], options?: Intl.NumberFormatOptions) { let str = this.toString(); // NumberFormat supports also fractional bignumber as string, e.g.: // new Intl.NumberFormat().format('123123123123123123123123123123123123.231555555') return new Intl.NumberFormat(locales, options).format(str as any); } // a-b minus (mix: TNumeric) { return math.minus(this, from(mix)); } sub (mix: TNumeric) { return math.minus(this, from(mix)); } // a+b plus (mix: TNumeric) { return math.plus(this, from(mix)); } add (mix: TNumeric) { return math.plus(this, from(mix)); } // a*b multipliedBy (mix: TNumeric) { return math.mul(this, from(mix)); } // a*b times (mix: TNumeric) { return math.mul(this, from(mix)); } // a*b mul (mix: TNumeric) { return math.mul(this, from(mix)); } // a/b dividedBy (mix: TNumeric) { return math.div(this, from(mix)); } // a/b div (mix: TNumeric) { return math.div(this, from(mix)); } isLessThan(mix: TNumeric) { return math.lt(this, from(mix)); } lt(mix: TNumeric) { return math.lt(this, from(mix)); } isLessThanOrEqual(mix: TNumeric) { return math.lte(this, from(mix)); } lte(mix: TNumeric) { return math.lte(this, from(mix)); } isGreaterThan(mix: TNumeric) { return math.gt(this, from(mix)); } gt(mix: TNumeric) { return math.gt(this, from(mix)); } isGreaterThanOrEqualTo(mix: TNumeric) { return math.gte(this, from(mix)); } gte(mix: TNumeric) { return math.gte(this, from(mix)); } modulo(mix: TNumeric) { return math.mod(this, from(mix)); } mod(mix: TNumeric) { return math.mod(this, from(mix)); } //Supports negative and fractional numbers pow (exponent: number | bigint) { return math.pow(this, exponent) } squareRoot () { return math.rootNth(this, 2n); } sqrt () { return math.rootNth(this, 2n); } root (nth: bigint) { return math.rootNth(this, nth); } absoluteValue () { return math.abs(this); } abs () { return math.abs(this); } /** * Math.ceil = ROUNDING_MODE.ROUND_UP * Math.floor = ROUNDING_MODE.ROUND_DOWN * Math.round = ROUNDING_MODE.ROUND_HALF_CEIL */ dp (dp: number, rm: ROUNDING_MODE = ROUNDING_MODE.ROUND_DOWN) { return math.dp(this, dp, rm); } decimalPlaces (dp: number, rm: ROUNDING_MODE = ROUNDING_MODE.ROUND_DOWN) { return math.dp(this, dp, rm); } compact () { let { value, mantissa } = this; let isNegative = value <= 0n; if (isNegative) { value = -value; } while (value >= 10n && mantissa >= 10n) { if (value % 10n === 0n && mantissa % 10n === 0n) { value /= 10n; mantissa /= 10n; continue; } break; } if (isNegative) { value = -value; } return new BigFloat(value, mantissa); } static from (mix: TNumeric) { return from(mix); } } function from (mix: TNumeric): BigFloat { if (mix instanceof BigFloat) { return mix; } if (typeof mix === 'string') { mix = mix.trim(); let eps = 1n; if (/e/i.test(mix)) { let match = mix.match(/e(?<eps>[-+]?(?<value>\d+))/i); eps = 10n ** BigInt(match.groups.value); mix = mix.replace(match[0], ''); if (match.groups.eps[0] === '-') { eps = -eps; } } let result: BigFloat; if (mix.includes('.')) { let [ integer, fractional ] = mix.split('.'); let nr = BigInt(`${integer}${fractional}`); let mantissa = 10n**BigInt(fractional.length); result = new BigFloat(nr, mantissa); } else { // hex or any int result = BigFloat.from(BigInt(mix)) } if (eps !== 1n) { if (eps > 0n) { result = result.mul(eps).compact(); } else { result = result.div(-eps).compact(); } } return result; } if (typeof mix === 'bigint') { return new BigFloat(mix); } if (typeof mix === 'number') { if (isFinite(mix) === false) { throw new Error(`Cannot calculate with Infinity`); } if (isNaN(mix)) { throw new Error(`Cannot calculate with NaN`); } let mantissa = $num.getMantissa(mix); let valueNr = Math.floor(mix * Number(mantissa)); return new BigFloat( BigInt(valueNr), mantissa); } throw new Error(`Invalid numeric value ${mix}`); } namespace $num { export function getMantissa(value: number): bigint { let nr = value; let mantissa = 1n; while (nr - Math.trunc(nr) !== 0) { nr *= 10; mantissa *= 10n; } return mantissa; } export function getDigits (value: bigint) { let count = 0; if (value < 0n) { value = -value; } while (value > 0n) { value /= 10n; count++; } return count; } } export enum ROUNDING_MODE { // Rounds away from zero ROUND_UP = 0, // Rounds towards zero ROUND_DOWN = 1, // Rounds towards Infinity ROUND_CEIL = 2, // Rounds towards -Infinity ROUND_FLOOR = 3, // Rounds towards nearest neighbor. If equidistant, rounds away from zero ROUND_HALF_UP = 4, // Rounds towards nearest neighbor. If equidistant, rounds towards zero ROUND_HALF_DOWN = 5, // Rounds towards nearest neighbor. If equidistant, rounds towards even neighbor ROUND_HALF_EVEN = 6, // Rounds towards nearest neighbor. If equidistant, rounds towards Infinity // Equivalent to Math.round ROUND_HALF_CEIL = 7, // Rounds towards nearest neighbor. If equidistant, rounds towards -Infinity ROUND_HALF_FLOOR = 8, } namespace math { export function abs (a: BigFloat) { return new BigFloat(a.value < 0n ? -a.value : a.value, a.mantissa); } export function dp (a: BigFloat, dp: number, rm: ROUNDING_MODE = ROUNDING_MODE.ROUND_DOWN) { let precision = $num.getDigits(a.mantissa) - 1; if (precision <= dp) { return new BigFloat(a.value, a.mantissa); } let truncMantissa = 10n**BigInt(precision - dp); let truncValue = a.value % truncMantissa; // rounded down let value = a.value / truncMantissa; let roundingBit = getRoundingBit(value, truncValue, truncMantissa, rm); return new BigFloat(value + roundingBit, 10n ** BigInt(dp)); } export function mul (a: BigFloat, b: BigFloat): BigFloat { let value = a.value * b.value; let mantissa = a.mantissa * b.mantissa; return new BigFloat(value, mantissa); } export function div (a: BigFloat, b: BigFloat): BigFloat { if (a.value === 0n) { return from(0); } if ($bigint.abs(a.value) < $bigint.abs(b.value)) { let aDigits = $num.getDigits(a.value); let bDigits = $num.getDigits(b.value); let increasedMantissa = 10n ** BigInt(bDigits - aDigits); a = new BigFloat(a.value * increasedMantissa, a.mantissa * increasedMantissa); } let precisionExp = Math.max(CONFIG.DECIMAL_PLACES + 6, 18) + Math.max($num.getDigits(a.mantissa), $num.getDigits(b.mantissa)) - 1; let precision = 10n**BigInt(precisionExp); let aValue = a.value * precision; let aMantissa = a.mantissa * precision; let value = aValue / b.value; let mantissa = aMantissa / b.mantissa; let fractional = $bigint.abs((aValue * 10n / b.value) % value); let fractionalMantissa = 10n; let roundingBit = getRoundingBit(value, fractional, fractionalMantissa, ROUNDING_MODE.ROUND_DOWN); value += roundingBit; return new BigFloat(value, mantissa); } export function mod (a: BigFloat, b: BigFloat): BigFloat { let [ aCommon, bCommon ] = toSameMantissa(a, b); let value = aCommon.value % bCommon.value; return new BigFloat(value, aCommon.mantissa); } export function pow(basis: BigFloat, exponent: number | bigint): BigFloat { if (typeof exponent === 'number' && Math.floor(exponent) === exponent) { exponent = BigInt(exponent); } if (typeof exponent === 'bigint') { if (exponent >= 0n) { return new BigFloat(basis.value ** exponent, basis.mantissa ** exponent); } return from(1).div(pow(basis, -exponent)); } // convert float to x^(m/n) let m = exponent; let n = 1n; while (Math.floor(m) !== m) { m *= 10; n *= 10n; } return rootNth(pow(basis, BigInt(m)), n); } export function rootNth(a: BigFloat, k = 2n) { let precision = CONFIG.DECIMAL_PLACES * 2 + 4; let mantissa = a.mantissa * 10n**BigInt(precision); let rootValue = calcRootNthBigInt(a.value * mantissa, k); return new BigFloat(rootValue, a.mantissa * 10n**BigInt(precision / 2)).compact(); } function calcRootNthBigInt(value: bigint, k = 2n) { if (value < 0n) { throw Error(`Root of ${value} is not allowed`); } let o = 0n; let x = value; let limit = 5000; while (x ** k !== k && x !== o && --limit) { o = x; x = ((k - 1n) * x + value / x ** (k - 1n)) / k; } return x; } export function plus (a: BigFloat, b: BigFloat): BigFloat { let [ aCommon, bCommon ] = toSameMantissa(a, b); return new BigFloat(aCommon.value + bCommon.value, aCommon.mantissa); } export function minus (a: BigFloat, b: BigFloat): BigFloat { let [ aCommon, bCommon ] = toSameMantissa(a, b); return new BigFloat(aCommon.value - bCommon.value, aCommon.mantissa); } export function lt (a: BigFloat, b: BigFloat): boolean { let [ aCommon, bCommon ] = toSameMantissa(a, b); return aCommon.value < bCommon.value; } export function lte (a: BigFloat, b: BigFloat): boolean { let [ aCommon, bCommon ] = toSameMantissa(a, b); return aCommon.value <= bCommon.value; } export function gt (a: BigFloat, b: BigFloat): boolean { let [ aCommon, bCommon ] = toSameMantissa(a, b); return aCommon.value > bCommon.value; } export function gte (a: BigFloat, b: BigFloat): boolean { let [ aCommon, bCommon ] = toSameMantissa(a, b); return aCommon.value >= bCommon.value; } export function toSameMantissa (a: BigFloat, b: BigFloat): [ BigFloat, BigFloat ] { if (a.mantissa === b.mantissa) { return [ a, b ] } let min = a.mantissa < b.mantissa ? a : b; let max = min === a ? b : a; let increasePrecision = 1n; let common = min.mantissa; while (common < max.mantissa) { common *= 10n; increasePrecision *= 10n; } $require.eq(common, max.mantissa); let minNew = new BigFloat(min.value * increasePrecision, common); let maxNew = new BigFloat(max.value, common); return min === a ? [ minNew, maxNew ] : [ maxNew, minNew ]; } function getRoundingBit (value: bigint, truncatedValue: bigint, truncMantissa: bigint, rm?: ROUNDING_MODE): 1n | -1n | 0n { if (truncatedValue === 0n ) { return 0n; } //|| truncatedValue * 10n / truncMantissa === 0n if (rm == null) { rm = CONFIG.ROUNDING_MODE; } if (rm === ROUNDING_MODE.ROUND_DOWN) { // 1.1 -> 1 | -1.1 -> -1 return 0n; } if (rm === ROUNDING_MODE.ROUND_UP) { // 1.1 -> 2 | -1.1 -> -2 return truncatedValue > 0n ? 1n : -1n; } if (rm === ROUNDING_MODE.ROUND_CEIL) { // 1.1 -> 2 | -1.1 -> -1 return truncatedValue > 0n? 1n : 0n; } if (rm === ROUNDING_MODE.ROUND_FLOOR) { // 1.1 -> 1 | -1.1 -> -2 return truncatedValue > 0n ? 0n : -1n; } let half = 5n * truncMantissa / 10n if (rm === ROUNDING_MODE.ROUND_HALF_UP) { // Rounds towards nearest neighbor. If equidistant, rounds away from zero if (truncatedValue > 0n) { // 1.3 -> 0 | 1.6 -> 1 return truncatedValue >= half ? 1n : 0n; } // -2.6 -> -1 | -2.5 -> 0 return -truncatedValue < half ? 0n : -1n; } if (rm === ROUNDING_MODE.ROUND_HALF_DOWN) { // Rounds towards nearest neighbor. If equidistant, rounds towards zero if (truncatedValue > 0n) { // 1.3 -> 0 | 1.6 -> 1 return truncatedValue > half ? 1n : 0n; } // -2.6 -> 1 | -2.5 -> 0 return -truncatedValue <= half ? 0n : -1n; } if (rm === ROUNDING_MODE.ROUND_HALF_EVEN) { if (value % 2n === 0n) { return getRoundingBit(value, truncatedValue, truncMantissa, ROUNDING_MODE.ROUND_HALF_DOWN); } else { return getRoundingBit(value, truncatedValue, truncMantissa, ROUNDING_MODE.ROUND_HALF_UP); } } if (rm === ROUNDING_MODE.ROUND_HALF_CEIL) { // Rounds towards nearest neighbor. If equidistant, rounds towards Infinity // ~~ Math.round if (truncatedValue > 0n) { // 1.3 -> 0 | 1.6 -> 1 return truncatedValue >= half ? 1n : 0n; } // -2.5 -> -1 | -2.3 -> -0 return -truncatedValue <= half ? 0n : -1n; } if (rm === ROUNDING_MODE.ROUND_HALF_FLOOR) { // Rounds towards nearest neighbor. If equidistant, rounds towards -Infinity if (truncatedValue > 0n) { // 1.3 -> 0 | 1.6 -> 1 return truncatedValue > half ? 1n : 0n; } // -2.5 -> -1 | -2.3 -> -0 return -truncatedValue < half ? 0n : -1n; } return 0n; } } const CONFIG = { DECIMAL_PLACES: 18, ROUNDING_MODE: ROUNDING_MODE.ROUND_HALF_UP, EXPONENTIAL_AT: [-24, 24], RANGE: 1E9 } export namespace $bigfloat { export function from (mix: TNumeric) { return BigFloat.from(mix); } export function config (cfg: { DECIMAL_PLACES?: number ROUNDING_MODE?: number EXPONENTIAL_AT?: [number, number] RANGE?: number }) { for (let key in cfg) { CONFIG[key] = cfg[key]; } } }