/** * Currency utilities for formatting and calculations */ // Currency configuration export const CURRENCY_CONFIG = { EUR: { code: 'EUR', symbol: '€', name: 'Euro', decimals: 2, locale: 'es-ES', }, USD: { code: 'USD', symbol: '$', name: 'US Dollar', decimals: 2, locale: 'en-US', }, GBP: { code: 'GBP', symbol: '£', name: 'British Pound', decimals: 2, locale: 'en-GB', }, } as const; export type CurrencyCode = keyof typeof CURRENCY_CONFIG; // Default currency for the application (Euro) export const DEFAULT_CURRENCY: CurrencyCode = 'EUR'; // Get currency symbol export const getCurrencySymbol = (currencyCode: CurrencyCode = DEFAULT_CURRENCY): string => { return CURRENCY_CONFIG[currencyCode]?.symbol || '€'; }; // Format currency amount export const formatCurrency = ( amount: number, currencyCode: CurrencyCode = 'EUR', options: Intl.NumberFormatOptions = {} ): string => { if (typeof amount !== 'number' || isNaN(amount)) { return formatCurrency(0, currencyCode, options); } const config = CURRENCY_CONFIG[currencyCode]; const defaultOptions: Intl.NumberFormatOptions = { style: 'currency', currency: config.code, minimumFractionDigits: config.decimals, maximumFractionDigits: config.decimals, }; const formatOptions = { ...defaultOptions, ...options }; try { return new Intl.NumberFormat(config.locale, formatOptions).format(amount); } catch (error) { // Fallback to manual formatting if Intl.NumberFormat fails const formattedAmount = amount.toFixed(config.decimals); return `${formattedAmount} ${config.symbol}`; } }; // Format currency without symbol (just the number) export const formatCurrencyAmount = ( amount: number, currencyCode: CurrencyCode = 'EUR', decimals?: number ): string => { if (typeof amount !== 'number' || isNaN(amount)) { return formatCurrencyAmount(0, currencyCode, decimals); } const config = CURRENCY_CONFIG[currencyCode]; const decimalPlaces = decimals ?? config.decimals; try { return new Intl.NumberFormat(config.locale, { minimumFractionDigits: decimalPlaces, maximumFractionDigits: decimalPlaces, }).format(amount); } catch (error) { return amount.toFixed(decimalPlaces); } }; // Parse currency string to number export const parseCurrency = ( currencyString: string, currencyCode: CurrencyCode = 'EUR' ): number => { if (!currencyString || typeof currencyString !== 'string') { return 0; } const config = CURRENCY_CONFIG[currencyCode]; // Remove currency symbols and spaces const cleanString = currencyString .replace(new RegExp(`[${config.symbol}\\s]`, 'g'), '') .replace(/,/g, '.') // Replace comma with dot for decimal separator .trim(); const parsed = parseFloat(cleanString); return isNaN(parsed) ? 0 : parsed; }; // Add two currency amounts export const addCurrency = (amount1: number, amount2: number): number => { if (typeof amount1 !== 'number' || typeof amount2 !== 'number') { return 0; } // Use proper decimal arithmetic to avoid floating point errors return Math.round((amount1 + amount2) * 100) / 100; }; // Subtract two currency amounts export const subtractCurrency = (amount1: number, amount2: number): number => { if (typeof amount1 !== 'number' || typeof amount2 !== 'number') { return 0; } return Math.round((amount1 - amount2) * 100) / 100; }; // Multiply currency amount export const multiplyCurrency = (amount: number, multiplier: number): number => { if (typeof amount !== 'number' || typeof multiplier !== 'number') { return 0; } return Math.round(amount * multiplier * 100) / 100; }; // Divide currency amount export const divideCurrency = (amount: number, divisor: number): number => { if (typeof amount !== 'number' || typeof divisor !== 'number' || divisor === 0) { return 0; } return Math.round((amount / divisor) * 100) / 100; }; // Calculate percentage of amount export const calculatePercentage = (amount: number, percentage: number): number => { if (typeof amount !== 'number' || typeof percentage !== 'number') { return 0; } return multiplyCurrency(amount, percentage / 100); }; // Add percentage to amount export const addPercentage = (amount: number, percentage: number): number => { const percentageAmount = calculatePercentage(amount, percentage); return addCurrency(amount, percentageAmount); }; // Subtract percentage from amount export const subtractPercentage = (amount: number, percentage: number): number => { const percentageAmount = calculatePercentage(amount, percentage); return subtractCurrency(amount, percentageAmount); }; // Calculate tax amount export const calculateTax = (amount: number, taxRate: number): number => { return calculatePercentage(amount, taxRate); }; // Calculate amount before tax (gross to net) export const calculateNetAmount = (grossAmount: number, taxRate: number): number => { if (typeof grossAmount !== 'number' || typeof taxRate !== 'number') { return 0; } return divideCurrency(grossAmount, 1 + (taxRate / 100)); }; // Calculate amount with tax (net to gross) export const calculateGrossAmount = (netAmount: number, taxRate: number): number => { if (typeof netAmount !== 'number' || typeof taxRate !== 'number') { return 0; } return multiplyCurrency(netAmount, 1 + (taxRate / 100)); }; // Calculate discount amount export const calculateDiscount = (amount: number, discountRate: number): number => { return calculatePercentage(amount, discountRate); }; // Apply discount to amount export const applyDiscount = (amount: number, discountRate: number): number => { const discountAmount = calculateDiscount(amount, discountRate); return subtractCurrency(amount, discountAmount); }; // Calculate profit margin export const calculateProfitMargin = (revenue: number, cost: number): number => { if (typeof revenue !== 'number' || typeof cost !== 'number' || revenue === 0) { return 0; } const profit = subtractCurrency(revenue, cost); return Math.round((profit / revenue) * 10000) / 100; // Return as percentage with 2 decimal places }; // Calculate markup export const calculateMarkup = (cost: number, markupPercentage: number): number => { return addPercentage(cost, markupPercentage); }; // Calculate selling price from cost and desired margin export const calculateSellingPrice = (cost: number, marginPercentage: number): number => { if (typeof cost !== 'number' || typeof marginPercentage !== 'number' || marginPercentage >= 100) { return cost; } return divideCurrency(cost, 1 - (marginPercentage / 100)); }; // Calculate cost from selling price and margin export const calculateCostFromMargin = (sellingPrice: number, marginPercentage: number): number => { if (typeof sellingPrice !== 'number' || typeof marginPercentage !== 'number') { return 0; } return multiplyCurrency(sellingPrice, 1 - (marginPercentage / 100)); }; // Format currency range (e.g., "€10 - €20") export const formatCurrencyRange = ( minAmount: number, maxAmount: number, currencyCode: CurrencyCode = 'EUR' ): string => { if (typeof minAmount !== 'number' || typeof maxAmount !== 'number') { return formatCurrency(0, currencyCode); } if (minAmount === maxAmount) { return formatCurrency(minAmount, currencyCode); } const formattedMin = formatCurrency(minAmount, currencyCode); const formattedMax = formatCurrency(maxAmount, currencyCode); return `${formattedMin} - ${formattedMax}`; }; // Round currency to nearest cent export const roundCurrency = (amount: number): number => { if (typeof amount !== 'number') { return 0; } return Math.round(amount * 100) / 100; }; // Check if amount is valid currency value export const isValidCurrencyAmount = (amount: any): boolean => { return typeof amount === 'number' && !isNaN(amount) && isFinite(amount) && amount >= 0; }; // Convert between currencies (simplified - in real app, use exchange rates API) export const convertCurrency = ( amount: number, fromCurrency: CurrencyCode, toCurrency: CurrencyCode, exchangeRate: number = 1 ): number => { if (!isValidCurrencyAmount(amount) || typeof exchangeRate !== 'number') { return 0; } if (fromCurrency === toCurrency) { return amount; } return multiplyCurrency(amount, exchangeRate); }; // Format currency for input fields (without currency symbol) export const formatCurrencyInput = (amount: number | string): string => { if (typeof amount === 'string') { const parsed = parseCurrency(amount); return parsed === 0 && amount !== '0' ? amount : formatCurrencyAmount(parsed); } if (typeof amount === 'number' && !isNaN(amount)) { return formatCurrencyAmount(amount); } return ''; }; // Calculate total from array of amounts export const calculateTotal = (amounts: number[]): number => { if (!Array.isArray(amounts)) { return 0; } return amounts.reduce((total, amount) => { if (isValidCurrencyAmount(amount)) { return addCurrency(total, amount); } return total; }, 0); }; // Calculate average from array of amounts export const calculateAverage = (amounts: number[]): number => { if (!Array.isArray(amounts) || amounts.length === 0) { return 0; } const validAmounts = amounts.filter(isValidCurrencyAmount); if (validAmounts.length === 0) { return 0; } const total = calculateTotal(validAmounts); return divideCurrency(total, validAmounts.length); }; // Find minimum amount in array export const findMinimumAmount = (amounts: number[]): number => { if (!Array.isArray(amounts) || amounts.length === 0) { return 0; } const validAmounts = amounts.filter(isValidCurrencyAmount); return validAmounts.length > 0 ? Math.min(...validAmounts) : 0; }; // Find maximum amount in array export const findMaximumAmount = (amounts: number[]): number => { if (!Array.isArray(amounts) || amounts.length === 0) { return 0; } const validAmounts = amounts.filter(isValidCurrencyAmount); return validAmounts.length > 0 ? Math.max(...validAmounts) : 0; }; // Format compact currency (e.g., €1.2K, €1.5M) export const formatCompactCurrency = ( amount: number, currencyCode: CurrencyCode = 'EUR' ): string => { if (!isValidCurrencyAmount(amount)) { return formatCurrency(0, currencyCode); } const config = CURRENCY_CONFIG[currencyCode]; try { return new Intl.NumberFormat(config.locale, { style: 'currency', currency: config.code, notation: 'compact', compactDisplay: 'short', maximumFractionDigits: 1, }).format(amount); } catch (error) { // Fallback for browsers that don't support compact notation if (amount >= 1000000) { return `${config.symbol}${(amount / 1000000).toFixed(1)}M`; } else if (amount >= 1000) { return `${config.symbol}${(amount / 1000).toFixed(1)}K`; } else { return formatCurrency(amount, currencyCode); } } }; // Calculate compound interest export const calculateCompoundInterest = ( principal: number, rate: number, time: number, compound: number = 1 ): number => { if (!isValidCurrencyAmount(principal) || typeof rate !== 'number' || typeof time !== 'number') { return principal; } const amount = principal * Math.pow(1 + (rate / 100) / compound, compound * time); return roundCurrency(amount); }; // Currency formatting options for different contexts export const getCurrencyFormatOptions = (context: 'display' | 'input' | 'compact' | 'accounting') => { const baseOptions = { style: 'currency' as const, currency: 'EUR' as const, }; switch (context) { case 'display': return { ...baseOptions, minimumFractionDigits: 2, maximumFractionDigits: 2, }; case 'input': return { ...baseOptions, minimumFractionDigits: 0, maximumFractionDigits: 2, }; case 'compact': return { ...baseOptions, notation: 'compact' as const, compactDisplay: 'short' as const, maximumFractionDigits: 1, }; case 'accounting': return { ...baseOptions, currencySign: 'accounting' as const, minimumFractionDigits: 2, maximumFractionDigits: 2, }; default: return baseOptions; } };