import * as plugins from './calculation.plugins.js'; import { Calculator } from './calculation.classes.calculator.js'; export interface ICurrencyOptions { code: string; symbol?: string; decimals?: number; thousandsSeparator?: string; decimalSeparator?: string; symbolPosition?: 'before' | 'after'; spaceBetweenSymbolAndValue?: boolean; } export interface IExchangeRate { from: string; to: string; rate: plugins.Decimal.Value; timestamp?: Date; } export interface IMoneyValue { amount: plugins.Decimal; currency: string; } /** * Currency calculations and formatting class */ export class Currency extends Calculator { private currencies: Map = new Map(); private exchangeRates: Map = new Map(); constructor() { super({ precision: 10 }); this.initializeCommonCurrencies(); } /** * Initialize common currencies with their default options */ private initializeCommonCurrencies(): void { const commonCurrencies: ICurrencyOptions[] = [ { code: 'USD', symbol: '$', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' }, { code: 'EUR', symbol: '€', decimals: 2, thousandsSeparator: '.', decimalSeparator: ',', symbolPosition: 'before' }, { code: 'GBP', symbol: '£', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' }, { code: 'JPY', symbol: '¥', decimals: 0, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' }, { code: 'CHF', symbol: 'Fr.', decimals: 2, thousandsSeparator: "'", decimalSeparator: '.', symbolPosition: 'after', spaceBetweenSymbolAndValue: true }, { code: 'CAD', symbol: 'C$', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' }, { code: 'AUD', symbol: 'A$', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' }, { code: 'CNY', symbol: '¥', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' }, { code: 'INR', symbol: '₹', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' }, { code: 'BTC', symbol: '₿', decimals: 8, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' } ]; commonCurrencies.forEach(currency => { this.currencies.set(currency.code, currency); }); } /** * Register a new currency or update existing one */ public registerCurrency(options: ICurrencyOptions): void { this.currencies.set(options.code, { ...options, decimals: options.decimals ?? 2, thousandsSeparator: options.thousandsSeparator ?? ',', decimalSeparator: options.decimalSeparator ?? '.', symbolPosition: options.symbolPosition ?? 'before', spaceBetweenSymbolAndValue: options.spaceBetweenSymbolAndValue ?? false }); } /** * Set exchange rate between two currencies * @param from Source currency code * @param to Target currency code * @param rate Exchange rate (how many 'to' units per 'from' unit) */ public setExchangeRate(from: string, to: string, rate: plugins.Decimal.Value): void { const key = `${from}_${to}`; this.exchangeRates.set(key, this.decimal(rate)); // Also set the inverse rate const inverseKey = `${to}_${from}`; const inverseRate = this.divide(1, rate); this.exchangeRates.set(inverseKey, inverseRate); } /** * Set multiple exchange rates at once */ public setExchangeRates(rates: IExchangeRate[]): void { rates.forEach(rate => { this.setExchangeRate(rate.from, rate.to, rate.rate); }); } /** * Convert amount from one currency to another * @param amount The amount to convert * @param from Source currency code * @param to Target currency code */ public convert( amount: plugins.Decimal.Value, from: string, to: string ): plugins.Decimal { if (from === to) { return this.decimal(amount); } const key = `${from}_${to}`; const rate = this.exchangeRates.get(key); if (!rate) { throw new Error(`Exchange rate not found for ${from} to ${to}`); } return this.multiply(amount, rate); } /** * Create a money value object */ public money(amount: plugins.Decimal.Value, currency: string): IMoneyValue { return { amount: this.decimal(amount), currency }; } /** * Add two money values (must be same currency) */ public addMoney(money1: IMoneyValue, money2: IMoneyValue): IMoneyValue { if (money1.currency !== money2.currency) { throw new Error('Cannot add money values with different currencies'); } return { amount: this.add(money1.amount, money2.amount), currency: money1.currency }; } /** * Subtract two money values (must be same currency) */ public subtractMoney(money1: IMoneyValue, money2: IMoneyValue): IMoneyValue { if (money1.currency !== money2.currency) { throw new Error('Cannot subtract money values with different currencies'); } return { amount: this.subtract(money1.amount, money2.amount), currency: money1.currency }; } /** * Format currency value for display * @param amount The amount to format * @param currencyCode The currency code * @param options Override formatting options */ public format( amount: plugins.Decimal.Value, currencyCode: string, options?: Partial ): string { const currency = this.currencies.get(currencyCode); if (!currency) { throw new Error(`Currency ${currencyCode} not registered`); } const formatOptions = { ...currency, ...options }; const value = this.decimal(amount); // Round to the correct number of decimal places const rounded = this.round(value, formatOptions.decimals || 0); // Convert to string with fixed decimals let numberStr = this.toFixed(rounded, formatOptions.decimals || 0); // Split into integer and decimal parts const parts = numberStr.split('.'); // Apply thousands separator to integer part if (formatOptions.thousandsSeparator) { parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, formatOptions.thousandsSeparator); } // Join parts with appropriate decimal separator if (parts.length === 2) { numberStr = parts.join(formatOptions.decimalSeparator || '.'); } else { numberStr = parts[0]; } // Add currency symbol if (formatOptions.symbol) { const space = formatOptions.spaceBetweenSymbolAndValue ? ' ' : ''; if (formatOptions.symbolPosition === 'after') { return `${numberStr}${space}${formatOptions.symbol}`; } else { return `${formatOptions.symbol}${space}${numberStr}`; } } return numberStr; } /** * Parse a formatted currency string back to a decimal value * @param formattedValue The formatted string * @param currencyCode The currency code */ public parse(formattedValue: string, currencyCode: string): plugins.Decimal { const currency = this.currencies.get(currencyCode); if (!currency) { throw new Error(`Currency ${currencyCode} not registered`); } // Remove currency symbol let cleanValue = formattedValue; if (currency.symbol) { cleanValue = cleanValue.replace(currency.symbol, ''); } // Remove thousands separator if (currency.thousandsSeparator) { cleanValue = cleanValue.replace(new RegExp(`\\${currency.thousandsSeparator}`, 'g'), ''); } // Replace decimal separator with standard dot if (currency.decimalSeparator && currency.decimalSeparator !== '.') { cleanValue = cleanValue.replace(currency.decimalSeparator, '.'); } // Remove any remaining spaces and parse cleanValue = cleanValue.trim(); return this.decimal(cleanValue); } /** * Round amount to currency's standard decimal places */ public roundToCurrency(amount: plugins.Decimal.Value, currencyCode: string): plugins.Decimal { const currency = this.currencies.get(currencyCode); if (!currency) { throw new Error(`Currency ${currencyCode} not registered`); } return this.round(amount, currency.decimals || 0); } /** * Calculate percentage of amount * @param amount The base amount * @param percentage The percentage (e.g., 15 for 15%) */ public percentage(amount: plugins.Decimal.Value, percentage: plugins.Decimal.Value): plugins.Decimal { const percent = this.divide(percentage, 100); return this.multiply(amount, percent); } /** * Calculate markup amount * @param cost The base cost * @param markupPercentage The markup percentage */ public markup(cost: plugins.Decimal.Value, markupPercentage: plugins.Decimal.Value): plugins.Decimal { const markupAmount = this.percentage(cost, markupPercentage); return this.add(cost, markupAmount); } /** * Calculate discount amount * @param price The original price * @param discountPercentage The discount percentage */ public discount(price: plugins.Decimal.Value, discountPercentage: plugins.Decimal.Value): plugins.Decimal { const discountAmount = this.percentage(price, discountPercentage); return this.subtract(price, discountAmount); } /** * Calculate tax amount * @param amount The base amount * @param taxRate The tax rate (e.g., 0.08 for 8%) */ public tax(amount: plugins.Decimal.Value, taxRate: plugins.Decimal.Value): plugins.Decimal { return this.multiply(amount, taxRate); } /** * Calculate total with tax * @param amount The base amount * @param taxRate The tax rate */ public withTax(amount: plugins.Decimal.Value, taxRate: plugins.Decimal.Value): plugins.Decimal { const taxAmount = this.tax(amount, taxRate); return this.add(amount, taxAmount); } /** * Extract base amount from a tax-inclusive price * @param totalAmount The total amount including tax * @param taxRate The tax rate */ public extractTax(totalAmount: plugins.Decimal.Value, taxRate: plugins.Decimal.Value): { baseAmount: plugins.Decimal; taxAmount: plugins.Decimal; } { const divisor = this.add(1, taxRate); const baseAmount = this.divide(totalAmount, divisor); const taxAmount = this.subtract(totalAmount, baseAmount); return { baseAmount, taxAmount }; } }