UNPKG

dnum

Version:

Small library for big decimal numbers.

350 lines (343 loc) 12.7 kB
var import_node_module = require("node:module"); var __create = Object.create; var __getProtoOf = Object.getPrototypeOf; var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __hasOwnProp = Object.prototype.hasOwnProperty; var __toESM = (mod, isNodeMode, target) => { target = mod != null ? __create(__getProtoOf(mod)) : {}; const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target; for (let key of __getOwnPropNames(mod)) if (!__hasOwnProp.call(to, key)) __defProp(to, key, { get: () => mod[key], enumerable: true }); return to; }; var __moduleCache = /* @__PURE__ */ new WeakMap; var __toCommonJS = (from) => { var entry = __moduleCache.get(from), desc; if (entry) return entry; entry = __defProp({}, "__esModule", { value: true }); if (from && typeof from === "object" || typeof from === "function") __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable })); __moduleCache.set(from, entry); return entry; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true, configurable: true, set: (newValue) => all[name] = () => newValue }); }; // src/index.ts var exports_src = {}; __export(exports_src, { toString: () => toString, toParts: () => toParts, toNumber: () => toNumber, toJSON: () => toJSON, subtract: () => subtract, sub: () => subtract, setDecimals: () => setDecimals, round: () => round, remainder: () => remainder, rem: () => remainder, multiply: () => multiply, mul: () => multiply, lte: () => lessThanOrEqual, lt: () => lessThan, lessThanOrEqual: () => lessThanOrEqual, lessThan: () => lessThan, isDnum: () => isDnum, gte: () => greaterThanOrEqual, gt: () => greaterThan, greaterThanOrEqual: () => greaterThanOrEqual, greaterThan: () => greaterThan, fromJSON: () => fromJSON, from: () => from, format: () => format, floor: () => floor, equalizeDecimals: () => equalizeDecimals, equal: () => equal, eq: () => equal, divide: () => divide, div: () => divide, compare: () => compare, cmp: () => compare, ceil: () => ceil, add: () => add, abs: () => abs2 }); module.exports = __toCommonJS(exports_src); // src/dnum.ts var import_from_exponential = __toESM(require("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 = import_from_exponential.default(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)); }