@hastom/fixed-point
Version:
Light lib for fixed point math made around native bigint
331 lines (330 loc) • 12.5 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.min)(aPrecision, bPrecision);
case Decimals.max:
return (0, math_1.max)(aPrecision, bPrecision);
case Decimals.add:
return aPrecision + bPrecision;
case Decimals.sub:
return (0, math_1.max)(aPrecision, bPrecision) - (0, math_1.min)(aPrecision, bPrecision);
}
};
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.plus = this.add;
this.minus = this.sub;
this.times = this.mul;
this.multipliedBy = this.mul;
this.dividedBy = this.div;
this.isEqualTo = this.eq;
this.isGreaterThan = this.gt;
this.isLessThan = this.lt;
this.isGreaterThanOrEqualTo = this.gte;
this.isLessThanOrEqualTo = this.lte;
this.negated = this.neg;
this.absoluteValue = this.abs;
this.squareRoot = this.sqrt;
this._base = base;
this._precision = precision;
}
get base() {
return this._base;
}
get precision() {
return this._precision;
}
add(arg, resultPrecision) {
const aPrecision = this.precision;
const bPrecision = arg.precision;
const calcPrecision = (0, math_1.max)(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);
const result = new FixedPoint(aBase + bBase, calcPrecision);
result.setPrecision(targetPrecision);
return result;
}
sub(arg, resultPrecision) {
const aPrecision = this.precision;
const bPrecision = arg.precision;
const calcPrecision = (0, math_1.max)(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);
const result = new FixedPoint(aBase - bBase, calcPrecision);
result.setPrecision(targetPrecision);
return result;
}
mul(arg, resultPrecision) {
const aPrecision = this.precision;
const bPrecision = arg.precision;
const calcPrecision = aPrecision + bPrecision;
const targetPrecision = pickPrecision(aPrecision, bPrecision, resultPrecision ?? Decimals.max);
const aBase = this.base;
const bBase = arg.base;
const result = new FixedPoint(aBase * bBase, calcPrecision);
result.setPrecision(targetPrecision);
return result;
}
div(arg, resultPrecision) {
const aPrecision = this.precision;
const bPrecision = arg.precision;
const calcPrecision = aPrecision + bPrecision;
const targetPrecision = pickPrecision(aPrecision, bPrecision, resultPrecision ?? Decimals.max);
const aBase = this.base;
const bBase = arg.base;
const newBase = (0, math_1.toPrecision)(aBase, calcPrecision, aPrecision) / bBase;
const result = new FixedPoint((0, math_1.toPrecision)(newBase, calcPrecision, aPrecision), calcPrecision);
result.setPrecision(targetPrecision);
return result;
}
cmp(arg, comparator) {
const aPrecision = this.precision;
const bPrecision = arg.precision;
const newPrecision = (0, math_1.max)(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) {
return this.cmp(arg, (a, b) => a === b);
}
gt(arg) {
return this.cmp(arg, (a, b) => a > b);
}
lt(arg) {
return this.cmp(arg, (a, b) => a < b);
}
gte(arg) {
return this.cmp(arg, (a, b) => a >= b);
}
lte(arg) {
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 = 10n ** 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) {
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) {
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() {
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;
}
return str.slice(0, -Number(this.precision)) + '.' + str.slice(-Number(this.precision));
}
toDecimal() {
return Number(this.toDecimalString());
}
valueOf() {
return this.toDecimal();
}
}
exports.FixedPoint = FixedPoint;