UNPKG

@hastom/fixed-point

Version:

Light lib for fixed point math made around native bigint

433 lines (432 loc) 16.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FixedPoint = exports.Decimals = exports.Rounding = void 0; const math_1 = require("./math"); var Rounding; (function (Rounding) { Rounding[Rounding["ROUND_UP"] = 0] = "ROUND_UP"; Rounding[Rounding["ROUND_DOWN"] = 1] = "ROUND_DOWN"; Rounding[Rounding["ROUND_CEIL"] = 2] = "ROUND_CEIL"; Rounding[Rounding["ROUND_FLOOR"] = 3] = "ROUND_FLOOR"; Rounding[Rounding["ROUND_HALF_UP"] = 4] = "ROUND_HALF_UP"; Rounding[Rounding["ROUND_HALF_DOWN"] = 5] = "ROUND_HALF_DOWN"; Rounding[Rounding["ROUND_HALF_EVEN"] = 6] = "ROUND_HALF_EVEN"; Rounding[Rounding["ROUND_HALF_CEIL"] = 7] = "ROUND_HALF_CEIL"; Rounding[Rounding["ROUND_HALF_FLOOR"] = 8] = "ROUND_HALF_FLOOR"; })(Rounding || (exports.Rounding = Rounding = {})); var Decimals; (function (Decimals) { Decimals["left"] = "left"; Decimals["right"] = "right"; Decimals["min"] = "min"; Decimals["max"] = "max"; Decimals["add"] = "add"; Decimals["sub"] = "sub"; })(Decimals || (exports.Decimals = Decimals = {})); const pickPrecision = (aPrecision, bPrecision, precisionResolution) => { if (typeof precisionResolution !== 'string') { return BigInt(precisionResolution); } switch (precisionResolution) { case Decimals.left: return aPrecision; case Decimals.right: return bPrecision; case Decimals.min: return (0, math_1.min2)(aPrecision, bPrecision); case Decimals.max: return (0, math_1.max2)(aPrecision, bPrecision); case Decimals.add: return aPrecision + bPrecision; case Decimals.sub: return (0, math_1.max2)(aPrecision, bPrecision) - (0, math_1.min2)(aPrecision, bPrecision); } }; const roundDiv = (numerator, denominator, rounding) => { if (denominator < 0n) { numerator = -numerator; denominator = -denominator; } const isNegative = numerator < 0n; const absNumerator = isNegative ? -numerator : numerator; const quotient = absNumerator / denominator; const remainder = absNumerator % denominator; if (remainder === 0n) { return isNegative ? -quotient : quotient; } const twiceRemainder = remainder * 2n; let roundUp = false; switch (rounding) { case Rounding.ROUND_UP: roundUp = true; break; case Rounding.ROUND_DOWN: roundUp = false; break; case Rounding.ROUND_CEIL: roundUp = !isNegative; break; case Rounding.ROUND_FLOOR: roundUp = isNegative; break; case Rounding.ROUND_HALF_UP: roundUp = twiceRemainder >= denominator; break; case Rounding.ROUND_HALF_DOWN: roundUp = twiceRemainder > denominator; break; case Rounding.ROUND_HALF_EVEN: roundUp = twiceRemainder > denominator || (twiceRemainder === denominator && quotient % 2n === 1n); break; case Rounding.ROUND_HALF_CEIL: roundUp = twiceRemainder > denominator || (twiceRemainder === denominator && !isNegative); break; case Rounding.ROUND_HALF_FLOOR: roundUp = twiceRemainder > denominator || (twiceRemainder === denominator && isNegative); break; } const rounded = roundUp ? quotient + 1n : quotient; return isNegative ? -rounded : rounded; }; const toPrecisionRounded = (base, to, from, rounding) => { if (to >= from) { return (0, math_1.toPrecision)(base, to, from); } if (rounding === undefined || rounding === Rounding.ROUND_DOWN) { return base / (0, math_1.pow10)(from - to); } return roundDiv(base, (0, math_1.pow10)(from - to), rounding); }; class FixedPoint { static min(arg0, ...args) { let min = arg0; for (const arg of args) { if (arg.lt(min)) { min = arg; } } return min; } static max(arg0, ...args) { let max = arg0; for (const arg of args) { if (arg.gt(max)) { max = arg; } } return max; } constructor(base, precision) { this.base = base; this.precision = precision; } add(arg, resultPrecision) { if (resultPrecision === undefined && this.precision === arg.precision) { return new FixedPoint(this.base + arg.base, this.precision); } const aPrecision = this.precision; const bPrecision = arg.precision; const calcPrecision = (0, math_1.max2)(aPrecision, bPrecision); const targetPrecision = pickPrecision(aPrecision, bPrecision, resultPrecision ?? Decimals.left); const aBase = (0, math_1.toPrecision)(this.base, calcPrecision, aPrecision); const bBase = (0, math_1.toPrecision)(arg.base, calcPrecision, bPrecision); return new FixedPoint((0, math_1.toPrecision)(aBase + bBase, targetPrecision, calcPrecision), targetPrecision); } sub(arg, resultPrecision) { if (resultPrecision === undefined && this.precision === arg.precision) { return new FixedPoint(this.base - arg.base, this.precision); } const aPrecision = this.precision; const bPrecision = arg.precision; const calcPrecision = (0, math_1.max2)(aPrecision, bPrecision); const targetPrecision = pickPrecision(aPrecision, bPrecision, resultPrecision ?? Decimals.left); const aBase = (0, math_1.toPrecision)(this.base, calcPrecision, aPrecision); const bBase = (0, math_1.toPrecision)(arg.base, calcPrecision, bPrecision); return new FixedPoint((0, math_1.toPrecision)(aBase - bBase, targetPrecision, calcPrecision), targetPrecision); } mul(arg, resultPrecision, rounding) { if (rounding === undefined && resultPrecision === undefined && this.precision === arg.precision) { return new FixedPoint((this.base * arg.base) / (0, math_1.pow10)(this.precision), this.precision); } return this.mulGeneral(arg, resultPrecision, rounding); } mulGeneral(arg, resultPrecision, rounding) { const aPrecision = this.precision; const bPrecision = arg.precision; const calcPrecision = aPrecision + bPrecision; const targetPrecision = pickPrecision(aPrecision, bPrecision, resultPrecision ?? Decimals.max); const rawBase = this.base * arg.base; return new FixedPoint(toPrecisionRounded(rawBase, targetPrecision, calcPrecision, rounding), targetPrecision); } div(arg, resultPrecision, rounding) { if (rounding === undefined && resultPrecision === undefined && this.precision === arg.precision) { return new FixedPoint((this.base * (0, math_1.pow10)(this.precision)) / arg.base, this.precision); } return this.divGeneral(arg, resultPrecision, rounding); } divGeneral(arg, resultPrecision, rounding) { const aPrecision = this.precision; const bPrecision = arg.precision; const targetPrecision = pickPrecision(aPrecision, bPrecision, resultPrecision ?? Decimals.max); const shift = bPrecision + targetPrecision - aPrecision; let numerator; let denominator; if (shift >= 0n) { numerator = this.base * (0, math_1.pow10)(shift); denominator = arg.base; } else { numerator = this.base; denominator = arg.base * (0, math_1.pow10)(-shift); } if (rounding === undefined || rounding === Rounding.ROUND_DOWN) { return new FixedPoint(numerator / denominator, targetPrecision); } return new FixedPoint(roundDiv(numerator, denominator, rounding), targetPrecision); } cmp(arg, comparator) { const aPrecision = this.precision; const bPrecision = arg.precision; const newPrecision = (0, math_1.max2)(aPrecision, bPrecision); const aBase = (0, math_1.toPrecision)(this.base, newPrecision, aPrecision); const bBase = (0, math_1.toPrecision)(arg.base, newPrecision, bPrecision); return comparator(aBase, bBase); } eq(arg) { if (this.precision === arg.precision) { return this.base === arg.base; } return this.cmp(arg, (a, b) => a === b); } gt(arg) { if (this.precision === arg.precision) { return this.base > arg.base; } return this.cmp(arg, (a, b) => a > b); } lt(arg) { if (this.precision === arg.precision) { return this.base < arg.base; } return this.cmp(arg, (a, b) => a < b); } gte(arg) { if (this.precision === arg.precision) { return this.base >= arg.base; } return this.cmp(arg, (a, b) => a >= b); } lte(arg) { if (this.precision === arg.precision) { return this.base <= arg.base; } return this.cmp(arg, (a, b) => a <= b); } neg() { return new FixedPoint(-this.base, this.precision); } abs() { return new FixedPoint((0, math_1.abs)(this.base), this.precision); } sqrt() { if (this.isNegative()) { throw new Error('Cannot calculate square root of negative number'); } if (this.isZero()) { return new FixedPoint(0n, this.precision); } const workingPrecision = this.precision + 10n; const workingThis = new FixedPoint((0, math_1.toPrecision)(this.base, workingPrecision, this.precision), workingPrecision); let x = new FixedPoint(workingThis.base >> (workingPrecision / 2n), workingPrecision); if (x.isZero()) { x = new FixedPoint(10n ** (workingPrecision / 2n), workingPrecision); } const two = new FixedPoint(2n * (10n ** workingPrecision), workingPrecision); const epsilon = new FixedPoint(1n, workingPrecision); for (let i = 0; i < 50; i++) { const quotient = workingThis.div(x, workingPrecision); const newX = x.add(quotient, workingPrecision).div(two, workingPrecision); if (newX.sub(x, workingPrecision).abs().lte(epsilon)) { break; } x = newX; } return x.toPrecision(this.precision); } isZero() { return this.base === 0n; } isPositive() { return this.base > 0n; } isNegative() { return this.base < 0n; } floor() { return this.round(Rounding.ROUND_FLOOR); } ceil() { return this.round(Rounding.ROUND_CEIL); } round(mode = Rounding.ROUND_HALF_UP) { if (this.precision === 0n) { return new FixedPoint(this.base, this.precision); } const isNegative = this.isNegative(); const absBase = (0, math_1.abs)(this.base); const divisor = (0, math_1.pow10)(this.precision); const integerPart = absBase / divisor; const fractionalPart = absBase % divisor; const isHalfwayCase = fractionalPart * 2n === divisor; let rounded = integerPart; switch (mode) { case Rounding.ROUND_UP: if (fractionalPart > 0n) { rounded = integerPart + 1n; } break; case Rounding.ROUND_DOWN: rounded = integerPart; break; case Rounding.ROUND_CEIL: if (fractionalPart > 0n) { if (!isNegative) { rounded = integerPart + 1n; } else { rounded = integerPart; } } break; case Rounding.ROUND_FLOOR: if (fractionalPart > 0n) { if (!isNegative) { rounded = integerPart; } else { rounded = integerPart + 1n; } } break; case Rounding.ROUND_HALF_UP: if (fractionalPart > divisor / 2n || (isHalfwayCase)) { rounded = integerPart + 1n; } break; case Rounding.ROUND_HALF_DOWN: if (fractionalPart > divisor / 2n) { rounded = integerPart + 1n; } break; case Rounding.ROUND_HALF_EVEN: if (fractionalPart > divisor / 2n) { rounded = integerPart + 1n; } else if (isHalfwayCase) { if (integerPart % 2n === 1n) { rounded = integerPart + 1n; } } break; case Rounding.ROUND_HALF_CEIL: if (fractionalPart > divisor / 2n) { rounded = integerPart + 1n; } else if (isHalfwayCase) { if (!isNegative) { rounded = integerPart + 1n; } } break; case Rounding.ROUND_HALF_FLOOR: if (fractionalPart > divisor / 2n) { rounded = integerPart + 1n; } else if (isHalfwayCase) { if (isNegative) { rounded = integerPart + 1n; } } break; } const roundedBase = isNegative ? -rounded * divisor : rounded * divisor; return new FixedPoint(roundedBase, this.precision); } setPrecision(newPrecision, rounding = Rounding.ROUND_DOWN) { if (newPrecision < this.precision) { if (rounding === Rounding.ROUND_DOWN) { this.base = this.base / (0, math_1.pow10)(this.precision - newPrecision); this.precision = newPrecision; } else { const rounded = new FixedPoint(this.base, this.precision - newPrecision).round(rounding); this.base = (0, math_1.toPrecision)(rounded.base, newPrecision, this.precision); this.precision = newPrecision; } } else if (newPrecision > this.precision) { this.base = (0, math_1.toPrecision)(this.base, newPrecision, this.precision); this.precision = newPrecision; } } toPrecision(resultPrecision, rounding = Rounding.ROUND_DOWN) { const newPrecision = BigInt(resultPrecision); if (newPrecision < this.precision) { if (rounding === Rounding.ROUND_DOWN) { return new FixedPoint(this.base / (0, math_1.pow10)(this.precision - newPrecision), newPrecision); } const rounded = new FixedPoint(this.base, this.precision - newPrecision).round(rounding); return new FixedPoint((0, math_1.toPrecision)(rounded.base, newPrecision, this.precision), newPrecision); } else { return new FixedPoint((0, math_1.toPrecision)(this.base, newPrecision, this.precision), newPrecision); } } toString() { return this.base.toString(); } toJSON() { return this.toString(); } toDecimalString(trimTrailingZeros = false) { const isNegative = this.isNegative(); let str = (0, math_1.abs)(this.base).toString().padStart(Number(this.precision) + 1, '0'); if (isNegative) { str = `-${str}`; } if (this.precision === 0n) { return str; } const intPart = str.slice(0, -Number(this.precision)); const fracPart = str.slice(-Number(this.precision)); if (!trimTrailingZeros) { return `${intPart}.${fracPart}`; } let end = fracPart.length; while (end > 0 && fracPart.charCodeAt(end - 1) === 48) { end -= 1; } return end === 0 ? intPart : `${intPart}.${fracPart.slice(0, end)}`; } toDecimal() { return Number(this.toDecimalString()); } valueOf() { return this.toDecimal(); } } exports.FixedPoint = FixedPoint; const proto = FixedPoint.prototype; proto.plus = proto.add; proto.minus = proto.sub; proto.times = proto.mul; proto.multipliedBy = proto.mul; proto.dividedBy = proto.div; proto.isEqualTo = proto.eq; proto.isGreaterThan = proto.gt; proto.isLessThan = proto.lt; proto.isGreaterThanOrEqualTo = proto.gte; proto.isLessThanOrEqualTo = proto.lte; proto.negated = proto.neg; proto.absoluteValue = proto.abs; proto.squareRoot = proto.sqrt;