UNPKG

money-lib

Version:

TypeScript library to work with money

311 lines (257 loc) 7.61 kB
import { config, getCurrency, getDefaultRounder, getLocale } from "./config"; import type { Cents, Money } from "./types"; // ------ Initialization ------ // export const zero = (currency = config.defaultCurrency) => { return { amount: 0, currency }; }; export const fromInt = ( amount: Cents, currency = config.defaultCurrency ): Money => { return { amount, currency }; }; export const fromFloat = ( amount: number, currency = config.defaultCurrency, round = getDefaultRounder() ): Money => { const scale = getCurrencyScale(zero(currency)); return { amount: round(amount * scale, 0), currency, }; }; export const fromIntString = ( amount: string, currency = config.defaultCurrency ): Money => { const parsed = parseInt(amount, 10); return fromInt(Number.isNaN(parsed) || parsed === 0 ? 0 : parsed, currency); }; export const fromFloatString = ( amount: string, currency = config.defaultCurrency, round = getDefaultRounder() ): Money => { const parsed = parseFloat(amount); return fromFloat( Number.isNaN(parsed) || parsed === 0 ? 0 : parsed, currency, round ); }; // ------ Serialization ------ // export const toInt = (m: Money): Cents => { return m.amount; }; export const toFloat = (m: Money): number => { const scale = getCurrencyScale(m); const { whole, cents } = split(m); return whole + cents / scale; }; export const toString = (m: Money): string => { return `${m.amount}`; }; export const toFloatString = (m: Money): string => { const scale = getCurrencyScale(m); const { whole, cents } = split(m); return `${whole}.${cents.toString().padStart(Math.log10(scale), "0")}`; }; // ------ Arithmetics ------ // export const add = (m1: Money, m2: Money, ...m: Money[]): Money => { return { ...m1, amount: [m2, ...m].reduce((sum, curr) => sum + curr.amount, m1.amount), }; }; export const subtract = (m1: Money, m2: Money, ...m: Money[]): Money => { return { ...m1, amount: [m2, ...m].reduce((diff, curr) => diff - curr.amount, m1.amount), }; }; export const multiply = ( m: Money, multiplier: number, // | Money round = getDefaultRounder() ): Money => { return fromInt(round(m.amount * multiplier, 0), m.currency); }; export const divide = ( m: Money, divider: number, // | Money round = getDefaultRounder() ): Money => { return fromInt(round(m.amount / divider, 0), m.currency); }; export const abs = (m: Money): Money => { return { ...m, amount: Math.abs(m.amount), }; }; // ------ Comparison ------ // export const compare = (a: Money, b: Money): -1 | 0 | 1 => { if (a.amount === b.amount) { return 0; } return a.amount > b.amount ? 1 : -1; }; export const equals = (a: Money, b: Money): boolean => { return compare(a, b) === 0; }; export const greaterThan = (a: Money, b: Money): boolean => { return compare(a, b) === 1; }; export const greaterThanOrEqual = (a: Money, b: Money): boolean => { return compare(a, b) >= 0; }; export const lessThan = (a: Money, b: Money): boolean => { return compare(a, b) === -1; }; export const lessThanOrEqual = (a: Money, b: Money): boolean => { return compare(a, b) <= 0; }; export const isZero = (m: Money): boolean => { return m.amount === 0; }; export const isPositive = (m: Money): boolean => { return m.amount > 0; }; export const isNegative = (m: Money): boolean => { return m.amount < 0; }; export const min = (m1: Money, ...m: Money[]): Money => { return [m1, ...m].reduce((acc, curr) => (lessThan(acc, curr) ? acc : curr)); }; export const max = (m1: Money, ...m: Money[]): Money => { return [m1, ...m].reduce((acc, curr) => greaterThan(acc, curr) ? acc : curr ); }; // ------ Validation ------ // export const isValid = (m: any): m is Money => { return Boolean( typeof m === "object" && (m.amount || m.amount === 0) && (typeof m.currency === "undefined" || (typeof m.currency === "string" && m.currency.length > 0 && !!getCurrency(m.currency))) ); }; // ------ Transformation ------ // export const split = (m: Money): { whole: number; cents: number } => { const scale = getCurrencyScale(m); const whole = Math.trunc(m.amount / scale); const cents = m.amount - whole * scale; return { whole, cents }; }; // ------ Formatting ------ // export const format = ( m: Money, ops?: { /** * - true: always show cents * - "no": never show cents * - false | "ifAny": show cents only if they are not zero */ cents?: boolean | "ifAny" | "no"; // default: true; if false, 00 cents will be omitted locale?: string; trailingZeros?: boolean; // default: true; if false, 1.50 will be formatted as 1.5 withPlusSign?: boolean; // default: false; if true, positive numbers will be prefixed with a plus sign } ): string => { const { cents, locale, trailingZeros, withPlusSign } = { cents: ops?.cents ?? true, locale: ops?.locale ?? config.defaultLocale, trailingZeros: ops?.trailingZeros ?? true, withPlusSign: ops?.withPlusSign ?? false, }; const parts = formatParts(m, locale); const signSymbol = parts.sign === "-" ? "-" : ""; let formatted = ""; if ( (!cents || cents === "ifAny") && parts.cents === "0".repeat(getCurrency(m.currency).precision) ) { formatted = `${signSymbol}${parts.currencySymbol}${parts.wholeFormatted}`; } else if (cents === "no") { formatted = `${signSymbol}${parts.currencySymbol}${parts.wholeFormatted}`; } else { formatted = `${signSymbol}${parts.currencySymbol}${parts.wholeFormatted}${parts.decimalSeparator}${parts.cents}`; } if (!trailingZeros) { formatted = formatted.replace(/0+$/, ""); } if (withPlusSign && parts.sign === "+") { formatted = `+${formatted}`; } return formatted; }; export const formatIntegerPart = ( integerPart: number, locale = config.defaultLocale ) => { return new Intl.NumberFormat(locale).format(integerPart); }; export const formatParts = ( m: Money, locale = config.defaultLocale ): { whole: string; wholeFormatted: string; cents: string; currencySymbol: string; decimalSeparator: string; sign: "+" | "-" | ""; } => { const { symbol, precision } = getCurrency(m.currency); const { decimalSeparator } = getLocale(locale); const { whole, cents } = split(m); const absWhole = Math.abs(whole); const wholeFormatted = formatIntegerPart(absWhole, locale); const sign = getAmountSign(m); return { whole: `${absWhole}`, wholeFormatted, cents: `${Math.abs(cents)}`.padStart(precision, "0"), currencySymbol: symbol, decimalSeparator, sign, }; }; // ------ Parsing ------ // export const parse = ( s: string, currency: string, locale = config.defaultLocale, decimalSeparator?: "." | "," ): Money => { const _decimalSeparator = decimalSeparator ?? getLocale(locale).decimalSeparator; const amountFloatString = { ",": () => s .replace(/[^0-9.,]/g, "") .replace(/\./g, "") .replace(/\,/g, "."), ".": () => s.replace(/[^0-9.,]/g, "").replace(/\,/g, ""), }[_decimalSeparator](); const parsedFloat = parseFloat(amountFloatString); const amountFloat = Number.isNaN(parsedFloat) ? 0 : parsedFloat; return fromFloat(amountFloat, currency); }; // ------ Helper methods ------ // const getCurrencyScale = (m: Money): number => { return 10 ** getCurrency(m.currency).precision; }; const getAmountSign = (m: Money) => { if (m.amount > 0) { return "+"; } if (m.amount < 0) { return "-"; } return ""; };