dnum
Version:
Small library for big decimal numbers.
304 lines (299 loc) • 10.6 kB
JavaScript
// src/dnum.ts
import fromExponential from "from-exponential";
// src/utils.ts
function divideAndRoundUp(dividend, divisor) {
const num = divisor > 0n ? dividend : -dividend;
const den = divisor > 0n ? divisor : -divisor;
const remainder = num % den;
const roundUp = remainder > 0n ? 1n : 0n;
return num / den + roundUp;
}
function divideAndRoundDown(dividend, divisor) {
const num = divisor > 0n ? dividend : -dividend;
const den = divisor > 0n ? divisor : -divisor;
const remainder = num % den;
const roundDown = remainder < 0n ? -1n : 0n;
return num / den + roundDown;
}
function divideAndRoundHalf(dividend, divisor) {
const num = divisor > 0n ? dividend : -dividend;
const den = divisor > 0n ? divisor : -divisor;
const invertSign = num < 0n ? -1n : 1n;
return (num * invertSign + den / 2n) / den * invertSign;
}
function divideAndRound(dividend, divisor, rounding = "ROUND_HALF") {
return rounding === "ROUND_UP" ? divideAndRoundUp(dividend, divisor) : rounding === "ROUND_DOWN" ? divideAndRoundDown(dividend, divisor) : divideAndRoundHalf(dividend, divisor);
}
function splitNumber(number) {
let [whole, fraction = "0"] = number.split(".");
if (whole === "") {
whole = "0";
}
fraction = fraction.replace(/(?!^)0*$/, "");
return [whole, fraction];
}
function powerOfTen(zeroes) {
return BigInt("1" + "0".repeat(zeroes));
}
function roundToPower(value, power) {
const a = value / power * power;
const b = a + power;
return value - a >= b - value ? b : a;
}
function ceilToPower(value, power) {
const remainder = value % power;
return remainder === 0n ? value : value / power * power + power;
}
function floorToPower(value, power) {
return value / power * power;
}
function abs(value) {
return value < 0n ? -value : value;
}
// src/dnum.ts
function isDnum(value) {
return Array.isArray(value) && value.length === 2 && typeof value[0] === "bigint" && typeof value[1] === "number";
}
var NUM_RE = /^-?(?:[0-9]+|(?:[0-9]*(?:\.[0-9]+)))$/;
function from(value, decimals = true) {
if (isDnum(value)) {
return setDecimals(value, decimals === true ? value[1] : decimals);
}
value = String(value);
if (value.includes("e")) {
value = fromExponential(value);
}
if (!value.match(NUM_RE)) {
throw new Error(`dnum: incorrect number (${value})`);
}
const negative = value.startsWith("-");
if (negative) {
value = value.slice(1);
}
const parts = splitNumber(value);
const whole = parts[0];
let fraction = parts[1];
if (decimals === true) {
decimals = fraction === "0" ? 0 : fraction.length;
}
fraction = fraction.slice(0, decimals);
fraction = fraction + "0".repeat(decimals - fraction.length);
const result = (BigInt(whole) * powerOfTen(decimals) + BigInt(fraction)) * (negative ? -1n : 1n);
return [result, decimals];
}
function setValueDecimals(value, decimalsDiff, options = {}) {
options.rounding ??= "ROUND_HALF";
if (decimalsDiff > 0) {
return value * powerOfTen(decimalsDiff);
}
if (decimalsDiff < 0) {
return divideAndRound(value, powerOfTen(-decimalsDiff), options.rounding);
}
return value;
}
function setDecimals(value, decimals, options = {}) {
options.rounding ??= "ROUND_HALF";
if (value[1] === decimals) {
return value;
}
if (value[1] < 0 || decimals < 0) {
throw new Error("dnum: decimals cannot be negative");
}
const decimalsDiff = decimals - value[1];
return [
setValueDecimals(value[0], decimalsDiff, options),
decimals
];
}
function equalizeDecimals(nums, decimals) {
const decimals_ = decimals ?? Math.max(...nums.map(([, decimals2]) => decimals2), 0);
return nums.map((num) => setDecimals(num, decimals_));
}
function toJSON([value, decimals]) {
return JSON.stringify([String(value), decimals]);
}
function fromJSON(jsonValue) {
const [value, decimals] = JSON.parse(jsonValue);
return [BigInt(value), decimals];
}
function toParts(dnum, optionsOrDigits = {}) {
const [value, decimals] = dnum;
const options = typeof optionsOrDigits === "number" ? { digits: optionsOrDigits } : optionsOrDigits;
const {
digits = decimals,
trailingZeros,
decimalsRounding
} = options;
const decimalsDivisor = powerOfTen(decimals);
let whole = value / decimalsDivisor;
const fractionValue = abs(value % decimalsDivisor);
const roundFn = decimalsRounding === "ROUND_UP" ? ceilToPower : decimalsRounding === "ROUND_DOWN" ? floorToPower : roundToPower;
let fraction = String(roundFn(BigInt("1" + "0".repeat(Math.max(0, String(decimalsDivisor).length - String(fractionValue).length - 1)) + String(fractionValue)), powerOfTen(Math.max(0, decimals - digits))));
if (fraction.startsWith("2")) {
whole += 1n;
}
fraction = fraction.slice(1, digits + 1);
fraction = trailingZeros ? fraction.padEnd(digits, "0") : fraction.replace(/0+$/, "");
return [
abs(whole),
fraction === "" || BigInt(fraction) === 0n && !trailingZeros ? null : fraction
];
}
function toNumber(value, optionsOrDigits) {
return Number(toString(value, optionsOrDigits));
}
function toString(value, optionsOrDigits) {
const [whole, fraction] = toParts(value, optionsOrDigits);
return (value[0] >= 0n ? "" : "-") + whole + (fraction ? `.${fraction}` : "");
}
// src/formatting.ts
function format(dnum, optionsOrDigits = {}) {
const options = typeof optionsOrDigits === "number" ? { digits: optionsOrDigits } : optionsOrDigits;
const {
compact,
locale = Intl.NumberFormat().resolvedOptions().locale,
signDisplay = "auto",
...toPartsOptions
} = options;
const [whole, fraction] = toParts(dnum, toPartsOptions);
const decimalsSeparator = new Intl.NumberFormat(locale).formatToParts(0.1).find((v) => v.type === "decimal")?.value ?? ".";
const roundsToZero = whole === 0n && (fraction === null || /^0+$/.test(fraction));
const wholeString = formatSign(dnum, roundsToZero, signDisplay) + BigInt(whole).toLocaleString(locale, {
notation: compact ? "compact" : "standard"
});
return fraction === null || !/\d/.test(wholeString.at(-1)) ? wholeString : `${wholeString}${decimalsSeparator}${fraction}`;
}
function formatSign(dnum, roundsToZero, signDisplay) {
if (signDisplay === "auto") {
return dnum[0] >= 0n ? "" : "-";
}
if (signDisplay === "always") {
return dnum[0] >= 0n ? "+" : "-";
}
if (signDisplay === "exceptZero") {
return roundsToZero ? "" : dnum[0] >= 0n ? "+" : "-";
}
if (signDisplay === "negative") {
return dnum[0] >= 0n || roundsToZero ? "" : "-";
}
return "";
}
// src/operations.ts
function add(num1, num2, decimals) {
const [num1_, num2_] = normalizePairAndDecimals(num1, num2, decimals);
return setDecimals([num1_[0] + num2_[0], num1_[1]], decimals ?? (isDnum(num1) ? num1[1] : num1_[1]));
}
function subtract(num1, num2, decimals) {
const [num1_, num2_] = normalizePairAndDecimals(num1, num2, decimals);
return setDecimals([num1_[0] - num2_[0], num1_[1]], decimals ?? (isDnum(num1) ? num1[1] : num1_[1]));
}
function multiply(num1, num2, optionsOrDecimals = {}) {
const options = typeof optionsOrDecimals === "number" ? { decimals: optionsOrDecimals } : optionsOrDecimals;
options.rounding ??= "ROUND_HALF";
const [num1_, num2_] = normalizePairAndDecimals(num1, num2, options.decimals);
return setDecimals([num1_[0] * num2_[0], num1_[1] * 2], options.decimals ?? (isDnum(num1) ? num1[1] : num1_[1]), { rounding: options.rounding });
}
function divide(num1, num2, optionsOrDecimals = {}) {
const options = typeof optionsOrDecimals === "number" ? { decimals: optionsOrDecimals } : optionsOrDecimals;
options.rounding ??= "ROUND_HALF";
const [num1_, num2_] = normalizePairAndDecimals(num1, num2, options.decimals);
if (num2_[0] === 0n) {
throw new Error("dnum: division by zero");
}
const value1 = setValueDecimals(num1_[0], Math.max(num1_[1], options.decimals ?? 0));
const value2 = setValueDecimals(num2_[0], 0);
return setDecimals([divideAndRound(value1, value2, options.rounding), num1_[1]], options.decimals ?? (isDnum(num1) ? num1[1] : num1_[1]), { rounding: options.rounding });
}
function remainder(num1, num2, decimals) {
const [num1_, num2_] = normalizePairAndDecimals(num1, num2);
return setDecimals([num1_[0] % num2_[0], num1_[1]], decimals ?? (isDnum(num1) ? num1[1] : num1_[1]));
}
function compare(num1, num2) {
const [num1_, num2_] = normalizePairAndDecimals(num1, num2);
return num1_[0] > num2_[0] ? 1 : num1_[0] < num2_[0] ? -1 : 0;
}
function equal(num1, num2) {
const [num1_, num2_] = normalizePairAndDecimals(num1, num2);
return num1_[0] === num2_[0];
}
function greaterThan(num1, num2) {
const [num1_, num2_] = normalizePairAndDecimals(num1, num2);
return num1_[0] > num2_[0];
}
function greaterThanOrEqual(num1, num2) {
return !lessThan(num1, num2);
}
function lessThan(num1, num2) {
const [num1_, num2_] = normalizePairAndDecimals(num1, num2);
return num1_[0] < num2_[0];
}
function lessThanOrEqual(num1, num2) {
return !greaterThan(num1, num2);
}
function abs2(num, decimals) {
const [valueIn, decimalsIn] = from(num);
if (decimals === undefined)
decimals = decimalsIn;
let valueAbs = valueIn;
if (valueAbs < 0n) {
valueAbs = -valueAbs;
}
return setDecimals([valueAbs, decimalsIn], decimals);
}
function floor(num, decimals) {
return round(num, { decimals, rounding: "ROUND_DOWN" });
}
function ceil(num, decimals) {
return round(num, { decimals, rounding: "ROUND_UP" });
}
function round(num, optionsOrDecimals = {}) {
const options = typeof optionsOrDecimals === "number" ? { decimals: optionsOrDecimals } : optionsOrDecimals;
options.rounding ??= "ROUND_HALF";
const numIn = from(num);
return setDecimals(setDecimals(numIn, 0, { rounding: options.rounding }), options.decimals === undefined ? numIn[1] : options.decimals);
}
function normalizePairAndDecimals(num1, num2, decimals) {
const num1_ = from(num1);
const num2_ = from(num2);
if (num1_[1] < 0 || num2_[1] < 0) {
throw new Error("dnum: decimals cannot be negative");
}
return equalizeDecimals([num1_, num2_], Math.max(num1_[1], num2_[1], decimals ?? 0));
}
export {
toString,
toParts,
toNumber,
toJSON,
subtract,
subtract as sub,
setDecimals,
round,
remainder,
remainder as rem,
multiply,
multiply as mul,
lessThanOrEqual as lte,
lessThan as lt,
lessThanOrEqual,
lessThan,
isDnum,
greaterThanOrEqual as gte,
greaterThan as gt,
greaterThanOrEqual,
greaterThan,
fromJSON,
from,
format,
floor,
equalizeDecimals,
equal,
equal as eq,
divide,
divide as div,
compare,
compare as cmp,
ceil,
add,
abs2 as abs
};