UNPKG

monie-utils

Version:

A comprehensive TypeScript library for money-related utilities including currency formatting, conversion, validation, and financial calculations

1,629 lines (1,613 loc) 59.8 kB
'use strict'; /* monie-utils - A comprehensive TypeScript library for money-related utilities */ // src/errors.ts var MonieUtilsError = class _MonieUtilsError extends Error { constructor(message) { super(message); this.code = "MONIE_UTILS_ERROR"; this.name = "MonieUtilsError"; if (Error.captureStackTrace) { Error.captureStackTrace(this, _MonieUtilsError); } } }; function createError(message) { return new MonieUtilsError(message); } // src/formatCurrency/constants.ts var DEFAULT_FORMAT_OPTIONS = { locale: "en-US", showSymbol: true, showCode: false, useGrouping: true, symbolPosition: "start" }; var CURRENCY_INFO = { // Major World Currencies USD: { code: "USD", symbol: "$", name: "US Dollar", decimalPlaces: 2, usesGrouping: true }, EUR: { code: "EUR", symbol: "\u20AC", name: "Euro", decimalPlaces: 2, usesGrouping: true }, GBP: { code: "GBP", symbol: "\xA3", name: "British Pound", decimalPlaces: 2, usesGrouping: true }, JPY: { code: "JPY", symbol: "\xA5", name: "Japanese Yen", decimalPlaces: 0, usesGrouping: true }, CHF: { code: "CHF", symbol: "CHF", name: "Swiss Franc", decimalPlaces: 2, usesGrouping: true }, CAD: { code: "CAD", symbol: "C$", name: "Canadian Dollar", decimalPlaces: 2, usesGrouping: true }, AUD: { code: "AUD", symbol: "A$", name: "Australian Dollar", decimalPlaces: 2, usesGrouping: true }, // African Currencies NGN: { code: "NGN", symbol: "\u20A6", name: "Nigerian Naira", decimalPlaces: 2, usesGrouping: true }, ZAR: { code: "ZAR", symbol: "R", name: "South African Rand", decimalPlaces: 2, usesGrouping: true }, KES: { code: "KES", symbol: "KSh", name: "Kenyan Shilling", decimalPlaces: 2, usesGrouping: true }, GHS: { code: "GHS", symbol: "\u20B5", name: "Ghanaian Cedi", decimalPlaces: 2, usesGrouping: true }, // Asian Currencies CNY: { code: "CNY", symbol: "\xA5", name: "Chinese Yuan", decimalPlaces: 2, usesGrouping: true }, INR: { code: "INR", symbol: "\u20B9", name: "Indian Rupee", decimalPlaces: 2, usesGrouping: true }, SGD: { code: "SGD", symbol: "S$", name: "Singapore Dollar", decimalPlaces: 2, usesGrouping: true }, // Cryptocurrencies BTC: { code: "BTC", symbol: "\u20BF", name: "Bitcoin", decimalPlaces: 8, usesGrouping: true }, ETH: { code: "ETH", symbol: "\u039E", name: "Ethereum", decimalPlaces: 8, usesGrouping: true }, USDT: { code: "USDT", symbol: "\u20AE", name: "Tether", decimalPlaces: 2, usesGrouping: true } }; var COMPACT_SUFFIXES = { K: 1e3, M: 1e6, B: 1e9, T: 1e12 }; var COMPACT_THRESHOLDS = { THOUSAND: 1e3, MILLION: 1e6, BILLION: 1e9, TRILLION: 1e12 }; // src/formatCurrency/formatCurrency.ts function isValidAmount(amount) { return typeof amount === "number" && Number.isFinite(amount); } function isValidCurrency(currency) { return currency.toUpperCase() in CURRENCY_INFO; } function formatCompactNumber(amount, decimalPlaces = 1) { const absAmount = Math.abs(amount); if (absAmount >= COMPACT_THRESHOLDS.TRILLION) { return `${(absAmount / COMPACT_THRESHOLDS.TRILLION).toFixed(decimalPlaces)}T`; } if (absAmount >= COMPACT_THRESHOLDS.BILLION) { return `${(absAmount / COMPACT_THRESHOLDS.BILLION).toFixed(decimalPlaces)}B`; } if (absAmount >= COMPACT_THRESHOLDS.MILLION) { return `${(absAmount / COMPACT_THRESHOLDS.MILLION).toFixed(decimalPlaces)}M`; } if (absAmount >= COMPACT_THRESHOLDS.THOUSAND) { return `${(absAmount / COMPACT_THRESHOLDS.THOUSAND).toFixed(decimalPlaces)}K`; } return absAmount.toFixed(decimalPlaces); } function formatCurrency(amount, currency, options = {}) { if (!isValidAmount(amount)) { throw new MonieUtilsError( `Invalid amount: ${amount}. Amount must be a finite number.` ); } const upperCurrency = currency.toUpperCase(); if (!isValidCurrency(upperCurrency)) { throw new MonieUtilsError( `Unsupported currency: ${currency}. Check the currency code.` ); } const opts = { ...DEFAULT_FORMAT_OPTIONS, ...options }; const currencyInfo = CURRENCY_INFO[upperCurrency]; const decimalPlaces = opts.decimalPlaces ?? currencyInfo.decimalPlaces; if (opts.compact) { const isNegative = amount < 0; const absAmount = Math.abs(amount); const compactNumber = formatCompactNumber(absAmount, 1); const symbol = opts.customSymbol ?? currencyInfo.symbol; let formatted; if (opts.symbolPosition === "end") { formatted = opts.showCode ? `${compactNumber} ${upperCurrency}` : `${compactNumber}${symbol}`; } else { formatted = opts.showCode ? `${upperCurrency} ${compactNumber}` : `${symbol}${compactNumber}`; } if (isNegative) { formatted = `-${formatted}`; } return { formatted, amount, currency: upperCurrency, locale: opts.locale, isCompact: true }; } try { const isNegative = amount < 0; const absAmount = Math.abs(amount); const formatOptions = { style: "decimal", minimumFractionDigits: decimalPlaces, maximumFractionDigits: decimalPlaces, useGrouping: opts.useGrouping }; const formatter = new Intl.NumberFormat(opts.locale, formatOptions); let formatted = formatter.format(absAmount); const symbol = opts.customSymbol ?? currencyInfo.symbol; if (opts.showCode) { if (opts.symbolPosition === "end") { formatted = `${formatted} ${upperCurrency}`; } else { formatted = `${upperCurrency} ${formatted}`; } } else if (opts.showSymbol !== false) { if (opts.symbolPosition === "end") { formatted = `${formatted}${symbol}`; } else { formatted = `${symbol}${formatted}`; } } if (isNegative) { formatted = `-${formatted}`; } return { formatted, amount, currency: upperCurrency, locale: opts.locale, isCompact: false }; } catch (error) { throw new MonieUtilsError( `Failed to format currency: ${error instanceof Error ? error.message : "Unknown error"}` ); } } function formatMoney(amount, currency, locale) { const options = locale ? { locale } : {}; const result = formatCurrency(amount, currency, options); return result.formatted; } function formatCents(cents, currency, options = {}) { if (!isValidAmount(cents)) { throw new MonieUtilsError( `Invalid cents amount: ${cents}. Amount must be a finite number.` ); } const upperCurrency = currency.toUpperCase(); if (!isValidCurrency(upperCurrency)) { throw new MonieUtilsError( `Unsupported currency: ${currency}. Check the currency code.` ); } const currencyInfo = CURRENCY_INFO[upperCurrency]; const divisor = Math.pow(10, currencyInfo.decimalPlaces); const amount = cents / divisor; return formatCurrency(amount, upperCurrency, options); } function formatCompactCurrency(amount, currency, options = {}) { return formatCurrency(amount, currency, { ...options, compact: true }); } // src/formatPercentage/constants.ts var DEFAULT_PERCENTAGE_OPTIONS = { precision: 2, locale: "en-US", useGrouping: true, spaceBefore: false }; // src/formatPercentage/formatPercentage.ts function isValidDecimal(decimal) { return typeof decimal === "number" && Number.isFinite(decimal); } function formatPercentage(decimal, options = {}) { if (!isValidDecimal(decimal)) { throw new MonieUtilsError( `Invalid decimal: ${decimal}. Decimal must be a finite number.` ); } const opts = { ...DEFAULT_PERCENTAGE_OPTIONS, ...options }; const percentage = decimal * 100; try { const formatOptions = { style: "decimal", minimumFractionDigits: opts.precision, maximumFractionDigits: opts.precision, useGrouping: opts.useGrouping }; const formatter = new Intl.NumberFormat(opts.locale, formatOptions); const formattedNumber = formatter.format(percentage); const suffix = opts.suffix ?? "%"; const formatted = opts.spaceBefore ? `${formattedNumber} ${suffix}` : `${formattedNumber}${suffix}`; return { formatted, decimal, percentage, precision: opts.precision, locale: opts.locale }; } catch (error) { throw new MonieUtilsError( `Failed to format percentage: ${error instanceof Error ? error.message : "Unknown error"}` ); } } // src/localization/constants.ts var LOCALE_CURRENCY_MAP = { "en-US": { currency: "USD", symbol: "$", name: "US Dollar", decimalPlaces: 2, locale: "en-US" }, "en-GB": { currency: "GBP", symbol: "\xA3", name: "British Pound", decimalPlaces: 2, locale: "en-GB" }, "de-DE": { currency: "EUR", symbol: "\u20AC", name: "Euro", decimalPlaces: 2, locale: "de-DE" }, "fr-FR": { currency: "EUR", symbol: "\u20AC", name: "Euro", decimalPlaces: 2, locale: "fr-FR" }, "ja-JP": { currency: "JPY", symbol: "\xA5", name: "Japanese Yen", decimalPlaces: 0, locale: "ja-JP" }, "zh-CN": { currency: "CNY", symbol: "\xA5", name: "Chinese Yuan", decimalPlaces: 2, locale: "zh-CN" }, "es-ES": { currency: "EUR", symbol: "\u20AC", name: "Euro", decimalPlaces: 2, locale: "es-ES" }, "pt-BR": { currency: "BRL", symbol: "R$", name: "Brazilian Real", decimalPlaces: 2, locale: "pt-BR" }, "en-NG": { currency: "NGN", symbol: "\u20A6", name: "Nigerian Naira", decimalPlaces: 2, locale: "en-NG" } }; // src/localization/localization.ts function isValidLocale(locale) { try { new Intl.NumberFormat(locale); return true; } catch { return false; } } function formatCurrencyByLocale(amount, currency, locale, options = {}) { if (typeof amount !== "number" || !Number.isFinite(amount)) { throw new MonieUtilsError( `Invalid amount: ${amount}. Amount must be a finite number.` ); } if (!currency || typeof currency !== "string") { throw new MonieUtilsError( `Invalid currency: ${currency}. Currency must be a valid string.` ); } if (!isValidLocale(locale)) { throw new MonieUtilsError( `Invalid locale: ${locale}. Locale must be a valid locale string.` ); } const formatOptions = { locale }; if (options.useGrouping !== void 0) formatOptions.useGrouping = options.useGrouping; if (options.customSymbol !== void 0) formatOptions.customSymbol = options.customSymbol; if (options.showCode !== void 0) formatOptions.showCode = options.showCode; if (options.symbolPosition !== void 0) formatOptions.symbolPosition = options.symbolPosition; const result = formatCurrency(amount, currency, formatOptions); return result.formatted; } function getLocaleCurrencyInfo(locale) { if (!locale || typeof locale !== "string") { throw new MonieUtilsError( `Invalid locale: ${locale}. Locale must be a valid string.` ); } const currencyInfo = LOCALE_CURRENCY_MAP[locale]; if (!currencyInfo) { throw new MonieUtilsError( `Unsupported locale: ${locale}. Check the locale code.` ); } return currencyInfo; } function formatWithGrouping(amount, locale = "en-US") { if (typeof amount !== "number" || !Number.isFinite(amount)) { throw new MonieUtilsError( `Invalid amount: ${amount}. Amount must be a finite number.` ); } if (!isValidLocale(locale)) { throw new MonieUtilsError( `Invalid locale: ${locale}. Locale must be a valid locale string.` ); } try { const formatter = new Intl.NumberFormat(locale, { style: "decimal", useGrouping: true }); const formatted = formatter.format(amount); return { formatted, amount, locale, hasGrouping: true }; } catch (error) { throw new MonieUtilsError( `Failed to format with grouping: ${error instanceof Error ? error.message : "Unknown error"}` ); } } function formatDecimalPlaces(amount, decimalPlaces) { if (typeof amount !== "number" || !Number.isFinite(amount)) { throw new MonieUtilsError( `Invalid amount: ${amount}. Amount must be a finite number.` ); } if (typeof decimalPlaces !== "number" || decimalPlaces < 0 || !Number.isInteger(decimalPlaces)) { throw new MonieUtilsError( `Invalid decimal places: ${decimalPlaces}. Must be a non-negative integer.` ); } try { const formatted = amount.toFixed(decimalPlaces); return { formatted, amount, decimalPlaces }; } catch (error) { throw new MonieUtilsError( `Failed to format decimal places: ${error instanceof Error ? error.message : "Unknown error"}` ); } } // src/validation/validation.ts function isValidAmount2(amount) { return typeof amount === "number" && Number.isFinite(amount); } function isValidCurrency2(currencyCode) { if (typeof currencyCode !== "string") return false; return currencyCode.toUpperCase() in CURRENCY_INFO; } function validateMoneyObject(moneyObject) { if (!moneyObject || typeof moneyObject !== "object") return false; const obj = moneyObject; return isValidAmount2(obj.amount) && isValidCurrency2(obj.currency); } function isPositiveAmount(amount) { if (!isValidAmount2(amount)) return false; return amount > 0; } function isWithinRange(amount, min, max, options = {}) { if (!isValidAmount2(amount) || !isValidAmount2(min) || !isValidAmount2(max)) return false; const { inclusive = true } = options; if (inclusive) { return amount >= min && amount <= max; } else { return amount > min && amount < max; } } function parseAmount(amountString) { if (typeof amountString !== "string") { return { amount: NaN, isValid: false, originalString: String(amountString) }; } const cleaned = amountString.replace(/[$£€¥₦₹]/g, "").replace(/[,\s]/g, "").replace(/[()]/g, "").trim(); const parsed = parseFloat(cleaned); return { amount: parsed, isValid: isValidAmount2(parsed), originalString: amountString }; } function parseCurrencyString(currencyString) { if (typeof currencyString !== "string") { return { amount: NaN, currency: "", isValid: false, originalString: String(currencyString) }; } const currencyMatch = currencyString.match(/\b([A-Z]{3})\b/); const currency = currencyMatch ? currencyMatch[1] : ""; const amountResult = parseAmount(currencyString); return { amount: amountResult.amount, currency, isValid: amountResult.isValid && isValidCurrency2(currency), originalString: currencyString }; } function normalizeAmount(amount, options = {}) { if (!isValidAmount2(amount)) { throw new MonieUtilsError( `Invalid amount: ${amount}. Amount must be a finite number.` ); } const { decimalPlaces = 2, roundingMode = "round" } = options; if (typeof decimalPlaces !== "number" || decimalPlaces < 0 || !Number.isInteger(decimalPlaces)) { throw new MonieUtilsError( `Invalid decimal places: ${decimalPlaces}. Must be a non-negative integer.` ); } const multiplier = Math.pow(10, decimalPlaces); switch (roundingMode) { case "floor": return Math.floor(amount * multiplier) / multiplier; case "ceil": return Math.ceil(amount * multiplier) / multiplier; case "round": default: return Math.round(amount * multiplier) / multiplier; } } function parseFormattedCurrency(formattedString, locale = "en-US") { if (typeof formattedString !== "string") { throw new MonieUtilsError( `Invalid formatted string: ${formattedString}. Must be a string.` ); } try { let cleaned = formattedString.replace(/[$£€¥₦₹]/g, "").replace(/[A-Z]{3}/g, "").trim(); if (locale.includes("de") || locale.includes("fr") || locale.includes("es")) { const parts = cleaned.split(","); if (parts.length === 2) { const integerPart = parts[0].replace(/\./g, ""); const decimalPart = parts[1]; cleaned = `${integerPart}.${decimalPart}`; } } else { cleaned = cleaned.replace(/,/g, ""); } const parsed = parseFloat(cleaned); if (!isValidAmount2(parsed)) { throw new MonieUtilsError( `Failed to parse formatted currency: ${formattedString}` ); } return parsed; } catch (error) { throw new MonieUtilsError( `Failed to parse formatted currency: ${error instanceof Error ? error.message : "Unknown error"}` ); } } // src/conversion/conversion.ts var DEFAULT_RATES = { USD: { EUR: 0.85, GBP: 0.73, JPY: 110, NGN: 460 }, EUR: { USD: 1.18, GBP: 0.86, JPY: 129, NGN: 542 }, GBP: { USD: 1.37, EUR: 1.16, JPY: 150, NGN: 630 }, JPY: { USD: 91e-4, EUR: 77e-4, GBP: 67e-4, NGN: 4.18 }, NGN: { USD: 22e-4, EUR: 18e-4, GBP: 16e-4, JPY: 0.24 } }; function getExchangeRate(from, to, customRate) { if (customRate !== void 0) { return customRate; } if (from === to) { return 1; } const rate = DEFAULT_RATES[from]?.[to]; if (!rate) { throw new MonieUtilsError( `Exchange rate not available for ${from} to ${to}` ); } return rate; } function convertCurrency(amount, fromCurrency, toCurrency, rate) { if (!isValidAmount2(amount)) { throw new MonieUtilsError( `Invalid amount: ${amount}. Amount must be a finite number.` ); } if (!isValidCurrency2(fromCurrency)) { throw new MonieUtilsError(`Invalid source currency: ${fromCurrency}`); } if (!isValidCurrency2(toCurrency)) { throw new MonieUtilsError(`Invalid target currency: ${toCurrency}`); } const exchangeRate = getExchangeRate( fromCurrency.toUpperCase(), toCurrency.toUpperCase(), rate ); const convertedAmount = amount * exchangeRate; return { originalAmount: amount, convertedAmount, fromCurrency: fromCurrency.toUpperCase(), toCurrency: toCurrency.toUpperCase(), exchangeRate, timestamp: /* @__PURE__ */ new Date() }; } function convertWithFee(amount, rate, feePercentage) { if (!isValidAmount2(amount)) { throw new MonieUtilsError( `Invalid amount: ${amount}. Amount must be a finite number.` ); } if (!isValidAmount2(rate) || rate <= 0) { throw new MonieUtilsError( `Invalid exchange rate: ${rate}. Rate must be a positive number.` ); } if (!isValidAmount2(feePercentage) || feePercentage < 0 || feePercentage > 100) { throw new MonieUtilsError( `Invalid fee percentage: ${feePercentage}. Must be between 0 and 100.` ); } const feeAmount = amount * feePercentage / 100; const amountAfterFee = amount - feeAmount; const convertedAmount = amountAfterFee * rate; return { originalAmount: amount, convertedAmount, fromCurrency: "", // Not specified in this function toCurrency: "", // Not specified in this function exchangeRate: rate, timestamp: /* @__PURE__ */ new Date(), feeAmount, feePercentage, amountAfterFee }; } function bulkConvert(amounts, fromCurrency, toCurrency, rate) { if (!Array.isArray(amounts) || amounts.length === 0) { throw new MonieUtilsError("Amounts must be a non-empty array"); } if (!isValidCurrency2(fromCurrency)) { throw new MonieUtilsError(`Invalid source currency: ${fromCurrency}`); } if (!isValidCurrency2(toCurrency)) { throw new MonieUtilsError(`Invalid target currency: ${toCurrency}`); } for (const amount of amounts) { if (!isValidAmount2(amount)) { throw new MonieUtilsError(`Invalid amount in array: ${amount}`); } } const exchangeRate = getExchangeRate( fromCurrency.toUpperCase(), toCurrency.toUpperCase(), rate ); const conversions = amounts.map((amount) => ({ originalAmount: amount, convertedAmount: amount * exchangeRate, fromCurrency: fromCurrency.toUpperCase(), toCurrency: toCurrency.toUpperCase(), exchangeRate, timestamp: /* @__PURE__ */ new Date() })); const totalOriginalAmount = amounts.reduce((sum, amount) => sum + amount, 0); const totalConvertedAmount = totalOriginalAmount * exchangeRate; return { conversions, totalOriginalAmount, totalConvertedAmount, exchangeRate, fromCurrency: fromCurrency.toUpperCase(), toCurrency: toCurrency.toUpperCase() }; } // src/arithmetic/arithmetic.ts function roundMoney(amount, precision = 2, mode = "round") { if (!isValidAmount2(amount)) { throw new MonieUtilsError( `Invalid amount: ${amount}. Amount must be a finite number.` ); } if (typeof precision !== "number" || precision < 0 || !Number.isInteger(precision)) { throw new MonieUtilsError( `Invalid precision: ${precision}. Must be a non-negative integer.` ); } const multiplier = Math.pow(10, precision); switch (mode) { case "floor": return Math.floor(amount * multiplier) / multiplier; case "ceil": return Math.ceil(amount * multiplier) / multiplier; case "bankers": const scaled = amount * multiplier; const rounded = Math.round(scaled); if (Math.abs(scaled - Math.floor(scaled) - 0.5) < Number.EPSILON) { return (rounded % 2 === 0 ? rounded : rounded - Math.sign(rounded)) / multiplier; } return rounded / multiplier; case "round": default: return Math.round(amount * multiplier) / multiplier; } } function addMoney(amount1, amount2, currency) { if (!isValidAmount2(amount1)) { throw new MonieUtilsError( `Invalid first amount: ${amount1}. Amount must be a finite number.` ); } if (!isValidAmount2(amount2)) { throw new MonieUtilsError( `Invalid second amount: ${amount2}. Amount must be a finite number.` ); } if (currency && !isValidCurrency2(currency)) { throw new MonieUtilsError(`Invalid currency: ${currency}`); } return roundMoney(amount1 + amount2); } function subtractMoney(amount1, amount2, currency) { if (!isValidAmount2(amount1)) { throw new MonieUtilsError( `Invalid first amount: ${amount1}. Amount must be a finite number.` ); } if (!isValidAmount2(amount2)) { throw new MonieUtilsError( `Invalid second amount: ${amount2}. Amount must be a finite number.` ); } if (currency && !isValidCurrency2(currency)) { throw new MonieUtilsError(`Invalid currency: ${currency}`); } return roundMoney(amount1 - amount2); } function multiplyMoney(amount, multiplier) { if (!isValidAmount2(amount)) { throw new MonieUtilsError( `Invalid amount: ${amount}. Amount must be a finite number.` ); } if (!isValidAmount2(multiplier)) { throw new MonieUtilsError( `Invalid multiplier: ${multiplier}. Multiplier must be a finite number.` ); } return roundMoney(amount * multiplier); } function divideMoney(amount, divisor) { if (!isValidAmount2(amount)) { throw new MonieUtilsError( `Invalid amount: ${amount}. Amount must be a finite number.` ); } if (!isValidAmount2(divisor)) { throw new MonieUtilsError( `Invalid divisor: ${divisor}. Divisor must be a finite number.` ); } if (divisor === 0) { throw new MonieUtilsError("Cannot divide by zero"); } return roundMoney(amount / divisor); } function calculateTip(amount, percentage) { if (!isValidAmount2(amount)) { throw new MonieUtilsError( `Invalid amount: ${amount}. Amount must be a finite number.` ); } if (!isValidAmount2(percentage) || percentage < 0) { throw new MonieUtilsError( `Invalid percentage: ${percentage}. Percentage must be a non-negative number.` ); } return roundMoney(amount * percentage / 100); } function calculateTax(amount, taxRate) { if (!isValidAmount2(amount)) { throw new MonieUtilsError( `Invalid amount: ${amount}. Amount must be a finite number.` ); } if (!isValidAmount2(taxRate) || taxRate < 0) { throw new MonieUtilsError( `Invalid tax rate: ${taxRate}. Tax rate must be a non-negative number.` ); } return roundMoney(amount * taxRate / 100); } function calculateDiscount(amount, discountRate) { if (!isValidAmount2(amount)) { throw new MonieUtilsError( `Invalid amount: ${amount}. Amount must be a finite number.` ); } if (!isValidAmount2(discountRate) || discountRate < 0 || discountRate > 100) { throw new MonieUtilsError( `Invalid discount rate: ${discountRate}. Discount rate must be between 0 and 100.` ); } return roundMoney(amount * discountRate / 100); } function calculateSimpleInterest(principal, rate, time) { if (!isValidAmount2(principal) || principal < 0) { throw new MonieUtilsError( `Invalid principal: ${principal}. Principal must be a non-negative number.` ); } if (!isValidAmount2(rate) || rate < 0) { throw new MonieUtilsError( `Invalid rate: ${rate}. Rate must be a non-negative number.` ); } if (!isValidAmount2(time) || time < 0) { throw new MonieUtilsError( `Invalid time: ${time}. Time must be a non-negative number.` ); } const interest = roundMoney(principal * rate * time / 100); const finalAmount = roundMoney(principal + interest); return { principal, rate, time, interest, finalAmount, type: "simple" }; } function calculateCompoundInterest(principal, rate, time, frequency = 1) { if (!isValidAmount2(principal) || principal < 0) { throw new MonieUtilsError( `Invalid principal: ${principal}. Principal must be a non-negative number.` ); } if (!isValidAmount2(rate) || rate < 0) { throw new MonieUtilsError( `Invalid rate: ${rate}. Rate must be a non-negative number.` ); } if (!isValidAmount2(time) || time < 0) { throw new MonieUtilsError( `Invalid time: ${time}. Time must be a non-negative number.` ); } if (!isValidAmount2(frequency) || frequency <= 0 || !Number.isInteger(frequency)) { throw new MonieUtilsError( `Invalid frequency: ${frequency}. Frequency must be a positive integer.` ); } const finalAmount = principal * Math.pow(1 + rate / 100 / frequency, frequency * time); const roundedFinal = roundMoney(finalAmount); const interest = roundMoney(roundedFinal - principal); return { principal, rate, time, interest, finalAmount: roundedFinal, type: "compound", frequency }; } function splitAmount(totalAmount, numberOfParts) { if (!isValidAmount2(totalAmount)) { throw new MonieUtilsError( `Invalid total amount: ${totalAmount}. Amount must be a finite number.` ); } if (!Number.isInteger(numberOfParts) || numberOfParts <= 0) { throw new MonieUtilsError( `Invalid number of parts: ${numberOfParts}. Must be a positive integer.` ); } const baseAmount = Math.floor(totalAmount * 100 / numberOfParts) / 100; const remainder = roundMoney(totalAmount - baseAmount * numberOfParts); const amounts = new Array(numberOfParts).fill(baseAmount); let remainderCents = Math.round(remainder * 100); for (let i = amounts.length - 1; i >= 0 && remainderCents > 0; i--) { amounts[i] = roundMoney(amounts[i] + 0.01); remainderCents--; } return { amounts, totalAmount, numberOfParts, remainder: remainderCents / 100 }; } function distributeProportionally(totalAmount, ratios) { if (!isValidAmount2(totalAmount)) { throw new MonieUtilsError( `Invalid total amount: ${totalAmount}. Amount must be a finite number.` ); } if (!Array.isArray(ratios) || ratios.length === 0) { throw new MonieUtilsError("Ratios must be a non-empty array"); } for (const ratio of ratios) { if (!isValidAmount2(ratio) || ratio < 0) { throw new MonieUtilsError( `Invalid ratio: ${ratio}. All ratios must be non-negative numbers.` ); } } const totalRatio = ratios.reduce((sum, ratio) => sum + ratio, 0); if (totalRatio === 0) { throw new MonieUtilsError("Sum of ratios cannot be zero"); } const amounts = ratios.map( (ratio) => roundMoney(totalAmount * ratio / totalRatio) ); const distributedTotal = amounts.reduce((sum, amount) => sum + amount, 0); const remainder = roundMoney(totalAmount - distributedTotal); return { amounts, totalAmount, ratios, remainder }; } function calculatePercentageOfTotal(amount, total) { if (!isValidAmount2(amount)) { throw new MonieUtilsError( `Invalid amount: ${amount}. Amount must be a finite number.` ); } if (!isValidAmount2(total)) { throw new MonieUtilsError( `Invalid total: ${total}. Total must be a finite number.` ); } if (total === 0) { throw new MonieUtilsError("Total cannot be zero"); } const percentage = roundMoney(amount / total * 100); return { percentage, amount, total }; } // src/loans/loans.ts function calculateMonthlyPayment(principal, rate, termMonths) { if (!isValidAmount2(principal) || principal <= 0) { throw new MonieUtilsError( `Invalid principal: ${principal}. Principal must be a positive number.` ); } if (!isValidAmount2(rate) || rate < 0) { throw new MonieUtilsError( `Invalid rate: ${rate}. Rate must be a non-negative number.` ); } if (!Number.isInteger(termMonths) || termMonths <= 0) { throw new MonieUtilsError( `Invalid term: ${termMonths}. Term must be a positive integer.` ); } if (rate === 0) { const monthlyPayment2 = roundMoney(principal / termMonths); return { monthlyPayment: monthlyPayment2, principal, rate, termMonths, totalAmount: roundMoney(monthlyPayment2 * termMonths), totalInterest: 0 }; } const monthlyRate = rate / 100 / 12; const monthlyPayment = principal * (monthlyRate * Math.pow(1 + monthlyRate, termMonths)) / (Math.pow(1 + monthlyRate, termMonths) - 1); const roundedPayment = roundMoney(monthlyPayment); const totalAmount = roundMoney(roundedPayment * termMonths); const totalInterest = roundMoney(totalAmount - principal); return { monthlyPayment: roundedPayment, principal, rate, termMonths, totalAmount, totalInterest }; } function calculateLoanBalance(principal, rate, termMonths, paymentsMade) { if (!isValidAmount2(principal) || principal <= 0) { throw new MonieUtilsError( `Invalid principal: ${principal}. Principal must be a positive number.` ); } if (!isValidAmount2(rate) || rate < 0) { throw new MonieUtilsError( `Invalid rate: ${rate}. Rate must be a non-negative number.` ); } if (!Number.isInteger(termMonths) || termMonths <= 0) { throw new MonieUtilsError( `Invalid term: ${termMonths}. Term must be a positive integer.` ); } if (!Number.isInteger(paymentsMade) || paymentsMade < 0 || paymentsMade > termMonths) { throw new MonieUtilsError( `Invalid payments made: ${paymentsMade}. Must be between 0 and ${termMonths}.` ); } if (paymentsMade === 0) { return { remainingBalance: principal, principalPaid: 0, interestPaid: 0, paymentsMade: 0, paymentsRemaining: termMonths }; } const { monthlyPayment } = calculateMonthlyPayment( principal, rate, termMonths ); if (rate === 0) { const principalPaid2 = roundMoney(monthlyPayment * paymentsMade); const remainingBalance2 = roundMoney(principal - principalPaid2); return { remainingBalance: Math.max(0, remainingBalance2), principalPaid: principalPaid2, interestPaid: 0, paymentsMade, paymentsRemaining: termMonths - paymentsMade }; } const monthlyRate = rate / 100 / 12; const remainingBalance = principal * (Math.pow(1 + monthlyRate, termMonths) - Math.pow(1 + monthlyRate, paymentsMade)) / (Math.pow(1 + monthlyRate, termMonths) - 1); const roundedBalance = roundMoney(Math.max(0, remainingBalance)); const principalPaid = roundMoney(principal - roundedBalance); const totalPaid = roundMoney(monthlyPayment * paymentsMade); const interestPaid = roundMoney(totalPaid - principalPaid); return { remainingBalance: roundedBalance, principalPaid, interestPaid, paymentsMade, paymentsRemaining: termMonths - paymentsMade }; } function calculateTotalInterest(principal, rate, termMonths) { const loanResult = calculateMonthlyPayment(principal, rate, termMonths); return loanResult.totalInterest; } function generateAmortizationSchedule(principal, rate, termMonths) { const summary = calculateMonthlyPayment(principal, rate, termMonths); const payments = []; let remainingBalance = principal; const monthlyRate = rate / 100 / 12; for (let i = 1; i <= termMonths; i++) { const interestAmount = rate === 0 ? 0 : roundMoney(remainingBalance * monthlyRate); const principalAmount = roundMoney(summary.monthlyPayment - interestAmount); const actualPrincipalAmount = i === termMonths ? remainingBalance : principalAmount; const actualPaymentAmount = roundMoney( actualPrincipalAmount + interestAmount ); remainingBalance = roundMoney( Math.max(0, remainingBalance - actualPrincipalAmount) ); payments.push({ paymentNumber: i, paymentAmount: actualPaymentAmount, principalAmount: actualPrincipalAmount, interestAmount, remainingBalance }); } return { payments, summary }; } function calculateCreditUtilization(usedCredit, totalCredit) { if (!isValidAmount2(usedCredit) || usedCredit < 0) { throw new MonieUtilsError( `Invalid used credit: ${usedCredit}. Must be a non-negative number.` ); } if (!isValidAmount2(totalCredit) || totalCredit <= 0) { throw new MonieUtilsError( `Invalid total credit: ${totalCredit}. Must be a positive number.` ); } if (usedCredit > totalCredit) { throw new MonieUtilsError( `Used credit (${usedCredit}) cannot exceed total credit (${totalCredit}).` ); } const utilizationPercentage = roundMoney(usedCredit / totalCredit * 100); const availableCredit = roundMoney(totalCredit - usedCredit); let riskLevel; if (utilizationPercentage <= 10) { riskLevel = "low"; } else if (utilizationPercentage <= 30) { riskLevel = "medium"; } else { riskLevel = "high"; } return { utilizationPercentage, usedCredit, totalCredit, availableCredit, riskLevel }; } function calculateMinimumPayment(balance, rate, minimumRate) { if (!isValidAmount2(balance) || balance < 0) { throw new MonieUtilsError( `Invalid balance: ${balance}. Balance must be a non-negative number.` ); } if (!isValidAmount2(rate) || rate < 0) { throw new MonieUtilsError( `Invalid rate: ${rate}. Rate must be a non-negative number.` ); } if (!isValidAmount2(minimumRate) || minimumRate <= 0 || minimumRate > 100) { throw new MonieUtilsError( `Invalid minimum rate: ${minimumRate}. Must be between 0 and 100.` ); } const monthlyInterestRate = rate / 100 / 12; const interestPortion = roundMoney(balance * monthlyInterestRate); const minimumBasedOnRate = roundMoney(balance * minimumRate / 100); const minimumPayment = Math.max(minimumBasedOnRate, interestPortion + 15); const principalPortion = roundMoney(minimumPayment - interestPortion); return { minimumPayment: roundMoney(minimumPayment), balance, interestRate: rate, minimumRate, interestPortion, principalPortion }; } function calculatePayoffTime(balance, payment, rate) { if (!isValidAmount2(balance) || balance <= 0) { throw new MonieUtilsError( `Invalid balance: ${balance}. Balance must be a positive number.` ); } if (!isValidAmount2(payment) || payment <= 0) { throw new MonieUtilsError( `Invalid payment: ${payment}. Payment must be a positive number.` ); } if (!isValidAmount2(rate) || rate < 0) { throw new MonieUtilsError( `Invalid rate: ${rate}. Rate must be a non-negative number.` ); } const monthlyRate = rate / 100 / 12; const monthlyInterest = balance * monthlyRate; if (payment <= monthlyInterest) { throw new MonieUtilsError( `Payment (${payment}) must be greater than monthly interest (${roundMoney(monthlyInterest)}).` ); } if (rate === 0) { const monthsToPayoff2 = Math.ceil(balance / payment); const totalAmountPaid2 = roundMoney(payment * monthsToPayoff2); return { monthsToPayoff: monthsToPayoff2, yearsToPayoff: roundMoney(monthsToPayoff2 / 12), totalInterestPaid: 0, totalAmountPaid: totalAmountPaid2, monthlyPayment: payment }; } const monthsToPayoff = Math.ceil( -Math.log(1 - balance * monthlyRate / payment) / Math.log(1 + monthlyRate) ); const totalAmountPaid = roundMoney(payment * monthsToPayoff); const totalInterestPaid = roundMoney(totalAmountPaid - balance); return { monthsToPayoff, yearsToPayoff: roundMoney(monthsToPayoff / 12), totalInterestPaid, totalAmountPaid, monthlyPayment: payment }; } // src/investment/investment.ts function calculateROI(initialInvestment, finalValue) { if (!isValidAmount2(initialInvestment) || !isValidAmount2(finalValue)) { throw new MonieUtilsError( "Initial investment and final value must be valid numbers" ); } if (initialInvestment <= 0) { throw new MonieUtilsError("Initial investment must be greater than zero"); } const gainLoss = finalValue - initialInvestment; const roi = gainLoss / initialInvestment; const roiPercentage = roi * 100; return { roi: roundMoney(roi, 6), roiPercentage: roundMoney(roiPercentage, 2), gainLoss: roundMoney(gainLoss, 2), isGain: gainLoss >= 0 }; } function calculateAnnualizedReturn(initialValue, finalValue, years) { if (!isValidAmount2(initialValue) || !isValidAmount2(finalValue) || !isValidAmount2(years)) { throw new MonieUtilsError( "Initial value, final value, and years must be valid numbers" ); } if (initialValue <= 0) { throw new MonieUtilsError("Initial value must be greater than zero"); } if (years <= 0) { throw new MonieUtilsError("Years must be greater than zero"); } const totalReturn = (finalValue - initialValue) / initialValue; const annualizedReturn = Math.pow(finalValue / initialValue, 1 / years) - 1; return { annualizedReturn: roundMoney(annualizedReturn, 6), annualizedReturnPercentage: roundMoney(annualizedReturn * 100, 2), totalReturn: roundMoney(totalReturn, 6), totalReturnPercentage: roundMoney(totalReturn * 100, 2) }; } function calculateDividendYield(dividendPerShare, pricePerShare) { if (!isValidAmount2(dividendPerShare) || !isValidAmount2(pricePerShare)) { throw new MonieUtilsError( "Dividend per share and price per share must be valid numbers" ); } if (pricePerShare <= 0) { throw new MonieUtilsError("Price per share must be greater than zero"); } if (dividendPerShare < 0) { throw new MonieUtilsError("Dividend per share cannot be negative"); } const yield_ = dividendPerShare / pricePerShare; const yieldPercentage = yield_ * 100; return { yield: roundMoney(yield_, 6), yieldPercentage: roundMoney(yieldPercentage, 2), dividendPerShare: roundMoney(dividendPerShare, 2), sharePrice: roundMoney(pricePerShare, 2) }; } function calculateFutureValue(presentValue, rate, periods) { if (!isValidAmount2(presentValue) || !isValidAmount2(rate) || !isValidAmount2(periods)) { throw new MonieUtilsError( "Present value, rate, and periods must be valid numbers" ); } if (presentValue <= 0) { throw new MonieUtilsError("Present value must be greater than zero"); } if (rate < 0) { throw new MonieUtilsError("Interest rate cannot be negative"); } if (periods < 0 || !Number.isInteger(periods)) { throw new MonieUtilsError("Periods must be a non-negative integer"); } const futureValue = presentValue * Math.pow(1 + rate, periods); const totalInterest = futureValue - presentValue; return { futureValue: roundMoney(futureValue, 2), presentValue: roundMoney(presentValue, 2), totalInterest: roundMoney(totalInterest, 2), effectiveRate: roundMoney(rate, 6), periods }; } // src/subscription/subscription.ts function calculateSubscriptionValue(monthlyAmount, months, currency = "USD") { if (!isValidAmount2(monthlyAmount) || !isValidAmount2(months)) { throw new MonieUtilsError( "Monthly amount and months must be valid numbers" ); } if (monthlyAmount < 0) { throw new MonieUtilsError("Monthly amount cannot be negative"); } if (months <= 0 || !Number.isInteger(months)) { throw new MonieUtilsError("Months must be a positive integer"); } const totalCost = monthlyAmount * months; return { totalCost: roundMoney(totalCost, 2), monthlyAmount: roundMoney(monthlyAmount, 2), months, currency, averageMonthlyCost: roundMoney(monthlyAmount, 2) }; } function compareSubscriptionPlans(plans) { if (!Array.isArray(plans) || plans.length === 0) { throw new MonieUtilsError("Plans must be a non-empty array"); } if (plans.length === 1) { throw new MonieUtilsError("At least two plans are required for comparison"); } for (const plan of plans) { if (!plan.id || !plan.name || !isValidAmount2(plan.monthlyAmount)) { throw new MonieUtilsError( "Each plan must have valid id, name, and monthlyAmount" ); } } const planAnalyses = plans.map((plan) => { const annualDiscount = plan.annualDiscount || 0; const effectiveMonthlyRate = plan.monthlyAmount * (1 - annualDiscount); const annualCost = effectiveMonthlyRate * 12; const costPerUser = plan.maxUsers ? annualCost / plan.maxUsers : void 0; const featureCount = plan.features?.length || 0; const baseScore = Math.max(0, 100 - effectiveMonthlyRate * 2); const featureBonus = Math.min(20, featureCount * 2); const valueScore = Math.min(100, baseScore + featureBonus); const analysis = { plan, effectiveMonthlyRate: roundMoney(effectiveMonthlyRate, 2), annualCost: roundMoney(annualCost, 2), valueScore: roundMoney(valueScore, 1) }; if (costPerUser !== void 0) { analysis.costPerUser = roundMoney(costPerUser, 2); } return analysis; }); const recommendedPlan = planAnalyses.reduce( (best, current) => current.valueScore > best.valueScore ? current : best ); const highestCost = Math.max(...planAnalyses.map((p) => p.annualCost)); const lowestCost = Math.min(...planAnalyses.map((p) => p.annualCost)); const maxSavings = roundMoney(highestCost - lowestCost, 2); return { plans: planAnalyses, recommendedPlan, maxSavings }; } function calculateProrationAmount(amount, daysUsed, totalDays) { if (!isValidAmount2(amount) || !isValidAmount2(daysUsed) || !isValidAmount2(totalDays)) { throw new MonieUtilsError( "Amount, days used, and total days must be valid numbers" ); } if (amount < 0) { throw new MonieUtilsError("Amount cannot be negative"); } if (daysUsed < 0 || totalDays <= 0) { throw new MonieUtilsError( "Days used cannot be negative and total days must be positive" ); } if (daysUsed > totalDays) { throw new MonieUtilsError("Days used cannot exceed total days"); } const usagePercentage = daysUsed / totalDays * 100; const proratedAmount = amount * daysUsed / totalDays; return { proratedAmount: roundMoney(proratedAmount, 2), fullAmount: roundMoney(amount, 2), daysUsed: Math.round(daysUsed), totalDays: Math.round(totalDays), usagePercentage: roundMoney(usagePercentage, 2) }; } function calculateUpgradeCredit(oldPlan, newPlan, daysRemaining) { if (!oldPlan || !newPlan) { throw new MonieUtilsError("Both old and new plans must be provided"); } if (!isValidAmount2(oldPlan.monthlyAmount) || !isValidAmount2(newPlan.monthlyAmount)) { throw new MonieUtilsError("Both plans must have valid monthly amounts"); } if (!isValidAmount2(daysRemaining) || daysRemaining < 0 || daysRemaining > 31) { throw new MonieUtilsError("Days remaining must be between 0 and 31"); } const assumedDaysInMonth = 30; const effectiveDaysRemaining = Math.min(daysRemaining, assumedDaysInMonth); const oldPlanRemainingValue = oldPlan.monthlyAmount * effectiveDaysRemaining / assumedDaysInMonth; const newPlanProratedCost = newPlan.monthlyAmount * effectiveDaysRemaining / assumedDaysInMonth; const creditAmount = oldPlanRemainingValue; const netAmountDue = newPlanProratedCost - creditAmount; return { creditAmount: roundMoney(creditAmount, 2), oldPlanRemainingValue: roundMoney(oldPlanRemainingValue, 2), newPlanProratedCost: roundMoney(newPlanProratedCost, 2), netAmountDue: roundMoney(netAmountDue, 2), daysRemaining: Math.round(effectiveDaysRemaining) }; } function calculateAnnualEquivalent(amount, frequency) { if (!isValidAmount2(amount)) { throw new MonieUtilsError("Amount must be a valid number"); } if (amount < 0) { throw new MonieUtilsError("Amount cannot be negative"); } const frequencyMultipliers = { daily: 365, weekly: 52, "bi-weekly": 26, monthly: 12, quarterly: 4, "semi-annually": 2, annually: 1 }; const paymentsPerYear = frequencyMultipliers[frequency]; if (!paymentsPerYear) { throw new MonieUtilsError(`Invalid frequency: ${frequency}`); } const annualAmount = amount * paymentsPerYear; return { annualAmount: roundMoney(annualAmount, 2), originalAmount: roundMoney(amount, 2), frequency, paymentsPerYear }; } function calculateNextPaymentDate(startDate, frequency) { if (!(startDate instanceof Date) || isNaN(startDate.getTime())) { throw new MonieUtilsError("Start date must be a valid Date object"); } const nextDate = new Date(startDate); switch (frequency) { case "daily": nextDate.setDate(nextDate.getDate() + 1); break; case "weekly": nextDate.setDate(nextDate.getDate() + 7); break; case "bi-weekly": nextDate.setDate(nextDate.getDate() + 14); break; case "monthly": nextDate.setMonth(nextDate.getMonth() + 1); break; case "quarterly": nextDate.setMonth(nextDate.getMonth() + 3); break; case "semi-annually": nextDate.setMonth(nextDate.getMonth() + 6); break; case "annually": nextDate.setFullYear(nextDate.getFullYear() + 1); break; default: throw new MonieUtilsError(`Invalid frequency: ${frequency}`); } return nextDate; } function calculateTotalRecurringCost(amount, frequency, duration) { if (!isValidAmount2(amount) || !isValidAmount2(duration)) { throw new MonieUtilsError("Amount and duration must be valid numbers"); } if (amount < 0) { throw new MonieUtilsError("Amount cannot be negative"); } if (duration <= 0) { throw new MonieUtilsError("Duration must be positive"); } const frequencyToMonthlyMultiplier = { daily: 30.44, // Average days per month weekly: 4.33, // Average weeks per month "bi-weekly": 2.17, // Average bi-weeks per month monthly: 1, quarterly: 1 / 3, "semi-annually": 1 / 6, annually: 1 / 12 }; const paymentsPerMonth = frequencyToMonthlyMultiplier[frequency]; if (!paymentsPerMonth) { throw new MonieUtilsError(`Invalid frequency: ${frequency}`); } const numberOfPayments = Math.round(duration * paymentsPerMonth); const totalCost = amount * numberOfPayments; return { totalCost: roundMoney(totalCost, 2), numberOfPayments, amountPerPeriod: roundMoney(amount, 2), frequency, duration: roundMoney(duration, 2), durationUnit: "months" }; } // src/utils/utils.ts function roundToNearestCent(amount) { if (!isValidAmount2(amount)) { throw new MonieUtilsError("Amount must be a valid number"); } return Math.round(amount * 100) / 100; } function roundToBankersRounding(amount, decimalPlaces = 2, mode = "half-even") { if (!isValidAmount2(amount)) { throw new MonieUtilsError("Amount must be a valid number"); } if (!Number.isInteger(decimalPlaces) || decimalPlaces < 0) { throw new MonieUtilsError("Decimal places must be a non-negative integer"); } const multiplier = Math.pow(10, decimalPlaces); const shifted = amount * multiplier; const rounded = Math.round(shifted * 10) / 10; const floor = Math.floor(rounded); const remainder = rounded - floor; if (Math.abs(remainder - 0.5) < 1e-10) { const isEven = floor % 2 === 0; if (mode === "half-even") { return isEven ? floor / multiplier : (floor + 1) / multiplier; } else { return isEven ? (floor + 1) / multiplier : floor / multiplier; } } return Math.round(shifted) / multiplier; } function truncateToDecimalPlaces(amount, places) { if (!isValidAmount2(amount)) { throw new MonieUtilsError("Amount must be a valid number"); } if (!Number.isInteger(places) || places < 0) { throw new MonieUtilsError("Decimal places must be a non-negative integer"); } const multiplier = Math.pow(10, places); return Math.trunc(amount * multiplier) / multiplier; } function ceilToNearestCent(amount) { if (!isValidAmount2(amount)) { throw new MonieUtilsError("Amount must be a valid number"); } return Math.ceil(amount * 100) / 100; } function formatThousands(number, options = {}) { if (!isValidAmount2(number)) { throw new MonieUtilsError("Number must be a valid number"); } const { separator = ",", locale = "en-US", includeDecimals = true, decimalPlaces } = options; try {