@juici/math
Version:
A mathematics utility library
631 lines (622 loc) • 18.4 kB
JavaScript
'use strict';
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: 'Module' } });
function divRem(numer, denom) {
const q = numer / denom;
const r = numer % denom;
return [q, r];
}
function cmp(x, y) {
return x === y ? 0 : x < y ? -1 : 1;
}
const name = "@juici/math";
const version = "0.1.0";
const customInspectSymbol = Symbol.for("nodejs.util.inspect.custom");
function setHasInstance(cls) {
// Use a symbol to indentify instances, this helps to provide better
// compatibility for bundled copies of the class.
const symbol = Symbol.for(`${name}[${cls.name}]`);
Object.defineProperty(cls.prototype, symbol, {
configurable: false,
enumerable: false,
value: version,
writable: false,
});
Object.defineProperty(cls, Symbol.hasInstance, {
configurable: false,
enumerable: false,
value: (instance) => typeof instance === "object" && instance !== null && symbol in instance,
writable: false,
});
}
/**
* An error parsing a decimal from a string.
*/
class ParseDecimalError extends Error {
}
ParseDecimalError.prototype.name = "ParseDecimalError";
setHasInstance(ParseDecimalError);
const isInt = (s) => /^[+-]?\d+$/.test(s);
function parseDecimal(s) {
let e = s.indexOf("e");
if (e === -1) {
e = s.indexOf("E");
}
let mantissa = s;
let exp = 0;
if (e !== -1) {
mantissa = s.slice(0, e);
const expStr = s.slice(e + 1);
if (expStr.length === 0) {
throw new ParseDecimalError(`Cannot parse empty exponent: ${s}`);
}
if (!isInt(expStr)) {
throw new ParseDecimalError(`Cannot parse integer exponent: ${expStr}`);
}
exp = Number(expStr);
}
if (mantissa.length === 0) {
throw new ParseDecimalError(`Cannot parse empty mantissa: ${s}`);
}
const dot = mantissa.indexOf(".");
let digits = mantissa;
let decimalOffset = 0;
if (dot !== -1) {
const trailing = mantissa.slice(dot + 1);
digits = mantissa.slice(0, dot) + trailing;
decimalOffset = trailing.length;
}
if (!isInt(digits)) {
throw new ParseDecimalError(`Cannot parse integer digits: ${digits}`);
}
const value = BigInt(digits);
const scale = decimalOffset - exp;
return [value, scale];
}
/**
* A big decimal type.
*/
class BigDecimal {
constructor(n, scale) {
if (scale !== undefined) {
if (!Number.isInteger(scale)) {
throw new TypeError("Argument 'scale' must be an integer");
}
if (typeof n !== "bigint") {
throw new TypeError("Argument 'digits' must be a bigint");
}
this.digits = n;
this.scale = scale;
}
else {
[this.digits, this.scale] = components(n);
}
[this.digits, this.scale] = normalize(this.digits, this.scale);
}
/**
* The number of decimal places in this BigDecimal.
*/
get dp() {
return this.scale < 0 ? 0 : this.scale;
}
/**
* The sign of this BigDecimal.
*/
get sign() {
return this.digits === 0n ? 0 : this.digits < 0n ? -1 : 1;
}
/**
* Checks if this BigDecimal is an integer.
*/
isInt() {
return this.scale <= 0;
}
/**
* Checks if this BigDecimal is negative.
*/
isNeg() {
return this.digits < 0n;
}
/**
* Checks if this BigDecimal is positive.
*/
isPos() {
return this.digits > 0n;
}
/**
* Checks if this BigDecimal is 0.
*/
isZero() {
return this.digits === 0n;
}
/**
* Checks if this BigDecimal is 1.
*/
isOne() {
return this.scale === 0 && this.digits === 1n;
}
/**
* Checks for equality of this BigDecimal with the given value.
*/
eq(other) {
const [digits, scale] = normalize(...components(other));
return this.digits === digits && this.scale === scale;
}
/**
* Returns the the ordering of this BigDecimal and the given value.
*/
cmp(other) {
const n = new BigDecimal(other);
const s1 = this.sign;
const s2 = n.sign;
// Either zero?
if (s1 === 0 || s2 === 0) {
return s1 !== 0 ? s1 : s2 !== 0 ? -s2 : 0;
}
// Compare signs.
if (s1 !== s2) {
return s1;
}
let d1 = this.digits;
let d2 = n.digits;
// Adjust the digits to the same scale.
if (this.scale < n.scale) {
d1 *= 10n ** BigInt(n.scale - this.scale);
}
else if (this.scale > n.scale) {
d2 *= 10n ** BigInt(this.scale - n.scale);
}
return cmp(d1, d2);
}
/**
* Checks if this BigDecimal is less than the given value.
*/
lt(other) {
return this.cmp(other) < 0;
}
/**
* Checks if this BigDecimal is less than or equal to the given value.
*/
le(other) {
return this.cmp(other) <= 0;
}
/**
* Checks if this BigDecimal is greater than the given value.
*/
gt(other) {
return this.cmp(other) > 0;
}
/**
* Checks if this BigDecimal is greater than or equal to the given value.
*/
ge(other) {
return this.cmp(other) >= 0;
}
/**
* Returns the absolute value of this BigDecimal.
*/
abs() {
return this.isNeg() ? this.neg() : this;
}
/**
* Returns the negation of this BigDecimal.
*/
neg() {
return new BigDecimal(-this.digits, this.scale);
}
/**
* Returns the addition of this BigDecimal with the given value.
*/
add(other) {
const { digits: ld, scale: ls } = this;
const [rd, rs] = components(other);
const [l, r, scale] = withScale(ld, ls, rd, rs);
return new BigDecimal(l + r, scale);
}
/**
* Returns the subtraction of this BigDecimal by the given value.
*/
sub(other) {
const { digits: ld, scale: ls } = this;
const [rd, rs] = components(other);
const [l, r, scale] = withScale(ld, ls, rd, rs);
return new BigDecimal(l - r, scale);
}
/**
* Returns the multiplication of this BigDecimal with the given value.
*/
mul(other) {
let [digits, scale] = components(other);
digits *= this.digits;
scale += this.scale;
return new BigDecimal(digits, scale);
}
/**
* Returns the division of this BigDecimal by the given value.
*
* The result is rounded if necessary.
*
* @param dp The maximum number of decimal places precision, default `20`.
* @throws {RangeError} If `other` is `0`.
*/
div(other, dp = 20) {
dp = validateDP(dp, "dp");
const n = new BigDecimal(other);
if (n.isZero()) {
throw new RangeError("Division by zero");
}
if (this.isZero() || n.isOne()) {
return this;
}
const scale = this.scale - n.scale;
if (this.digits === n.digits) {
return new BigDecimal(1n, scale);
}
return implDiv(this.digits, n.digits, scale, dp);
}
/**
* Returns the remainder of this BigDecimal divided by the given value.
*
* @throws {RangeError} If `other` is `0`.
*/
rem(other) {
const [rd, rs] = components(other);
if (rd === 0n) {
throw new RangeError("Division by zero");
}
const { digits: ld, scale: ls } = this;
const [l, r, scale] = withScale(ld, ls, rd, rs);
return new BigDecimal(l % r, scale);
}
/**
* Returns the division of this BigDecimal by `2`.
*
* This function is more efficient than `.div(2)`.
*/
half() {
if (this.isZero()) {
return this;
}
else if (this.digits % 2n === 0n) {
return new BigDecimal(this.digits / 2n, this.scale);
}
else {
return new BigDecimal(this.digits * 5n, this.scale + 1);
}
}
/**
* Returns the value of this BigDecimal rounded to the given number of decimal places.
*/
toDP(dp) {
dp = validateDP(dp, "dp");
if (dp >= this.scale) {
return this;
}
const factor = 10n ** BigInt(this.scale - dp);
const [q, r] = divRem(this.digits, factor);
return new BigDecimal(q + roundingTerm(r), dp);
}
/**
* Returns the value of this BigDecimal converted to a primitive number.
*/
toNumber() {
return Number(this.toString());
}
/**
* Returns the value of this BigDecimal rounded to a primitive bigint.
*/
toBigInt() {
if (this.scale === 0) {
return this.digits;
}
else if (this.scale > 0) {
const factor = 10n ** BigInt(this.scale);
const [q, r] = divRem(this.digits, factor);
return q + roundingTerm(r);
}
else {
const factor = 10n ** BigInt(-this.scale);
return this.digits * factor;
}
}
/**
* Returns a string representing the value of this BigDecimal.
*/
toString() {
const neg = this.digits < 0n;
const digits = neg ? (-this.digits).toString() : this.digits.toString();
const len = digits.length;
let before;
let after;
if (this.scale >= len) {
before = "0";
after = "0".repeat(this.scale - len) + digits;
}
else {
const pos = len - this.scale;
if (pos > len) {
before = digits + "0".repeat(pos - len);
after = "";
}
else {
before = digits.slice(0, pos);
after = digits.slice(pos);
}
}
let s = before;
if (after.length > 0) {
s += `.${after}`;
}
return neg ? `-${s}` : s;
}
/**
* Returns this BigDecimal formatted using fixed-point notation.
*
* The result is rounded if necessary, and the fractional component is padded
* with zeros if necessary so that it has the specified length.
*
* @param dp The number of decimal places, default `0`.
* @throws {RangeError} If `dp` is less than `0`.
*/
toFixed(dp = 0) {
dp = validateDP(dp, "dp");
let digits = this.digits;
let scale = this.scale;
if (dp < scale) {
const factor = 10n ** BigInt(scale - dp);
const [q, r] = divRem(digits, factor);
digits = q + roundingTerm(r);
scale = dp;
}
const neg = digits < 0n;
let repr = neg ? (-digits).toString() : digits.toString();
const len = repr.length;
let before;
let after;
if (scale >= len) {
before = "0";
after = "0".repeat(scale - len) + repr;
}
else {
const pos = len - scale;
if (pos > len) {
before = repr + "0".repeat(pos - len);
after = "";
}
else {
before = repr.slice(0, pos);
after = repr.slice(pos);
}
}
if (after.length < dp) {
after += "0".repeat(dp - after.length);
}
repr = before;
if (after.length > 0) {
repr += `.${after}`;
}
return neg ? `-${repr}` : repr;
}
/**
* Returns this BigDecimal formatted using exponential notation, with one
* digit before the decimal point.
*
* The result is rounded if necessary, and the fractional component is padded
* with zeros if necessary so that it has the specified length.
*
* @param dp The number of decimal places, default to the number of decimal
* places required to represent the value uniquely.
* @throws {RangeError} If `dp` is less than `0`.
*/
toExponential(dp) {
const neg = this.digits < 0n;
let digits = neg ? -this.digits : this.digits;
let scale = this.scale;
if (dp !== undefined) {
dp = validateDP(dp, "dp");
if (scale < 0) {
const factor = 10n ** BigInt(-scale);
digits *= factor;
scale = 0;
}
const extra = digits.toString().length - 1 - dp;
if (extra > 0) {
const factor = 10n ** BigInt(extra);
const [q, r] = divRem(digits, factor);
digits = q + roundingTerm(r);
scale -= extra;
}
}
let exp = -scale;
let before = digits.toString();
let after = "";
const len = before.length;
if (len > 1) {
after = before.slice(1);
before = before.slice(0, 1);
exp += len - 1;
}
if (dp !== undefined && after.length < dp) {
after += "0".repeat(dp - after.length);
}
let repr = before;
if (after.length > 0) {
repr += `.${after}`;
}
repr += `e${exp >= 0 ? "+" : ""}${exp}`;
return neg ? `-${repr}` : repr;
}
/**
* Returns a string representing the value of this BigDecimal.
*/
toJSON() {
return this.toString();
}
/**
* Returns a string representing the value of this BigDecimal.
*/
valueOf() {
return this.toString();
}
/**
* Getter for the string tag used in the `Object.prototype.toString` method.
*/
get [Symbol.toStringTag]() {
return "BigDecimal";
}
/**
* Converts a BigDecimal into a string or number.
*
* @param hint The string "string", "number", or "default" to specify what primitive to return.
*
* @throws {TypeError} If `hint` was given something other than "string", "number", or "default".
* @returns A number if `hint` was "number", a string if 'hint' was "string" or "default".
*/
[Symbol.toPrimitive](hint) {
switch (hint) {
case "number":
return this.toNumber();
case "string":
case "default":
return this.toString();
default:
throw new TypeError(`Invalid hint: ${hint}`);
}
}
/**
* Custom inspection function for Node.js.
*
* @internal
*/
[customInspectSymbol](_depth, options) {
return options.stylize(this.toString(), "number");
}
}
setHasInstance(BigDecimal);
function isBigDecimalLike(n) {
return (typeof n === "object" &&
n !== null &&
typeof n.digits === "bigint" &&
typeof n.scale === "number");
}
function implDiv(numer, denom, scale, dp) {
if (numer === 0n) {
return new BigDecimal(0n);
}
// Shuffle signs around to have position `numer` and `denom`.
let neg = false;
if (numer < 0n) {
numer = -numer;
if (denom < 0n) {
denom = -denom;
}
else {
neg = true;
}
}
else if (denom < 0n) {
denom = -denom;
neg = true;
}
// Shift digits of `numer` until larger than `denom`, adjusting `scale` appropriately.
while (numer < denom) {
numer *= 10n;
scale++;
}
// First division.
let [quotient, remainder] = divRem(numer, denom);
if (scale > dp) {
while (scale > dp) {
[quotient, remainder] = divRem(quotient, 10n);
scale--;
}
if (remainder !== 0n) {
// Round the final number with the remainder.
quotient += roundingTerm(remainder);
}
}
else {
// Shift remainder by 1 place, before loop to find digits of decimal places.
remainder *= 10n;
while (remainder !== 0n && scale < dp) {
const [q, r] = divRem(remainder, denom);
quotient = quotient * 10n + q;
remainder = r * 10n;
scale++;
}
if (remainder !== 0n) {
// Round the final number with the remainder.
quotient += roundingTerm(remainder / denom);
}
}
return new BigDecimal(neg ? -quotient : quotient, scale);
}
function validateDP(dp, arg) {
if (dp < 0) {
throw new RangeError(`Argument '${arg}' must be >= 0`);
}
return Math.trunc(dp);
}
function roundingTerm(n) {
// Compare by char code to avoid parsing digit.
// 53 is the UTF-16 char code of "5".
if (n < 0n) {
const digit = (-n).toString().charCodeAt(0);
return digit >= 53 ? -1n : 0n;
}
else {
const digit = n.toString().charCodeAt(0);
return digit >= 53 ? 1n : 0n;
}
}
function components(n) {
if (isBigDecimalLike(n)) {
return [n.digits, n.scale];
}
switch (typeof n) {
case "bigint":
return [n, 0];
case "number":
if (!Number.isFinite(n)) {
throw new RangeError(`BigDecimal must be finite: ${n}`);
}
if (Number.isSafeInteger(n)) {
return [BigInt(n), 0];
}
return parseDecimal(n.toExponential());
case "string":
return parseDecimal(n);
}
// Whilst our API says we don't accept any other types here, we'll make a
// best attempt to try to parse a big decimal from the string representation.
const repr = String(n);
try {
return parseDecimal(repr);
}
catch (err) {
throw new TypeError(`Cannot convert '${repr}' to a BigDecimal`, { cause: err });
}
}
function normalize(digits, scale) {
if (digits === 0n) {
return [0n, 0];
}
while (digits % 10n === 0n) {
digits /= 10n;
scale--;
}
return [digits, scale];
}
function withScale(ld, ls, rd, rs) {
let scale = ls;
if (ls < rs) {
ld *= 10n ** BigInt(rs - ls);
scale = rs;
}
else if (ls > rs) {
rd *= 10n ** BigInt(ls - rs);
}
return [ld, rd, scale];
}
exports.BigDecimal = BigDecimal;
exports.ParseDecimalError = ParseDecimalError;
//# sourceMappingURL=index.js.map