441 lines
12 KiB
TypeScript
441 lines
12 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|
|
}; |