@hastom/fixed-point
Version:
Light lib for fixed point math made around native bigint
433 lines (432 loc) • 16.8 kB
JavaScript
"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;