/** * Number Formatting Utilities * Provides locale-aware number, currency, and measurement formatting * Fixes floating point precision issues */ export interface FormatNumberOptions { minimumFractionDigits?: number; maximumFractionDigits?: number; useGrouping?: boolean; } export interface FormatCurrencyOptions extends FormatNumberOptions { currency?: string; currencyDisplay?: 'symbol' | 'code' | 'name'; } export interface FormatWeightOptions extends FormatNumberOptions { unit?: 'kg' | 'g' | 'lb' | 'oz'; } /** * Round number to specified decimal places to avoid floating point errors */ export function roundToPrecision(value: number, decimals: number = 2): number { const multiplier = Math.pow(10, decimals); return Math.round(value * multiplier) / multiplier; } /** * Format number with locale-specific formatting */ export function formatNumber( value: number | string, locale: string = 'es-ES', options: FormatNumberOptions = {} ): string { const numValue = typeof value === 'string' ? parseFloat(value) : value; if (isNaN(numValue)) { return '0'; } // Round to avoid floating point errors const rounded = roundToPrecision(numValue, options.maximumFractionDigits ?? 2); const defaultOptions: Intl.NumberFormatOptions = { minimumFractionDigits: options.minimumFractionDigits ?? 0, maximumFractionDigits: options.maximumFractionDigits ?? 2, useGrouping: options.useGrouping ?? true, }; return new Intl.NumberFormat(locale, defaultOptions).format(rounded); } /** * Format currency with locale-specific formatting */ export function formatCurrency( value: number | string, locale: string = 'es-ES', options: FormatCurrencyOptions = {} ): string { const numValue = typeof value === 'string' ? parseFloat(value) : value; if (isNaN(numValue)) { return '€0,00'; } // Round to avoid floating point errors const rounded = roundToPrecision(numValue, options.maximumFractionDigits ?? 2); const defaultOptions: Intl.NumberFormatOptions = { style: 'currency', currency: options.currency ?? 'EUR', currencyDisplay: options.currencyDisplay ?? 'symbol', minimumFractionDigits: options.minimumFractionDigits ?? 2, maximumFractionDigits: options.maximumFractionDigits ?? 2, }; return new Intl.NumberFormat(locale, defaultOptions).format(rounded); } /** * Format weight/mass with unit */ export function formatWeight( value: number | string, locale: string = 'es-ES', options: FormatWeightOptions = {} ): string { const numValue = typeof value === 'string' ? parseFloat(value) : value; if (isNaN(numValue)) { return '0 kg'; } const unit = options.unit ?? 'kg'; // Round to avoid floating point errors const rounded = roundToPrecision(numValue, options.maximumFractionDigits ?? 2); const formatted = formatNumber(rounded, locale, { minimumFractionDigits: options.minimumFractionDigits ?? 0, maximumFractionDigits: options.maximumFractionDigits ?? 2, useGrouping: options.useGrouping ?? true, }); return `${formatted} ${unit}`; } /** * Format volume with unit */ export function formatVolume( value: number | string, locale: string = 'es-ES', unit: 'L' | 'mL' | 'gal' = 'L', decimals: number = 2 ): string { const numValue = typeof value === 'string' ? parseFloat(value) : value; if (isNaN(numValue)) { return `0 ${unit}`; } const rounded = roundToPrecision(numValue, decimals); const formatted = formatNumber(rounded, locale, { maximumFractionDigits: decimals, }); return `${formatted} ${unit}`; } /** * Format percentage */ export function formatPercentage( value: number | string, locale: string = 'es-ES', decimals: number = 1 ): string { const numValue = typeof value === 'string' ? parseFloat(value) : value; if (isNaN(numValue)) { return '0%'; } const rounded = roundToPrecision(numValue, decimals); const formatted = formatNumber(rounded, locale, { minimumFractionDigits: 0, maximumFractionDigits: decimals, }); return `${formatted}%`; } /** * Format compact number (1K, 1M, etc.) */ export function formatCompactNumber( value: number | string, locale: string = 'es-ES' ): string { const numValue = typeof value === 'string' ? parseFloat(value) : value; if (isNaN(numValue)) { return '0'; } const rounded = roundToPrecision(numValue, 1); return new Intl.NumberFormat(locale, { notation: 'compact', maximumFractionDigits: 1, }).format(rounded); } /** * Format integer (no decimals) */ export function formatInteger( value: number | string, locale: string = 'es-ES' ): string { const numValue = typeof value === 'string' ? parseFloat(value) : value; if (isNaN(numValue)) { return '0'; } return new Intl.NumberFormat(locale, { minimumFractionDigits: 0, maximumFractionDigits: 0, }).format(Math.round(numValue)); } /** * Parse localized number string to number */ export function parseLocalizedNumber( value: string, locale: string = 'es-ES' ): number { // Get locale-specific decimal and group separators const parts = new Intl.NumberFormat(locale).formatToParts(1234.5); const decimalSeparator = parts.find(p => p.type === 'decimal')?.value ?? ','; const groupSeparator = parts.find(p => p.type === 'group')?.value ?? '.'; // Remove group separators and replace decimal separator with dot const normalized = value .replace(new RegExp(`\\${groupSeparator}`, 'g'), '') .replace(decimalSeparator, '.'); return parseFloat(normalized); } /** * Format unit of measure with value */ export function formatMeasurement( value: number | string, unit: string, locale: string = 'es-ES', decimals: number = 2 ): string { const numValue = typeof value === 'string' ? parseFloat(value) : value; if (isNaN(numValue)) { return `0 ${unit}`; } const rounded = roundToPrecision(numValue, decimals); const formatted = formatNumber(rounded, locale, { minimumFractionDigits: 0, maximumFractionDigits: decimals, }); return `${formatted} ${unit}`; } /** * Safe division to avoid floating point errors */ export function safeDivide( numerator: number, denominator: number, decimals: number = 2 ): number { if (denominator === 0) { return 0; } return roundToPrecision(numerator / denominator, decimals); } /** * Safe multiplication to avoid floating point errors */ export function safeMultiply( a: number, b: number, decimals: number = 2 ): number { return roundToPrecision(a * b, decimals); } /** * Safe addition to avoid floating point errors */ export function safeAdd( ...values: number[] ): number { const sum = values.reduce((acc, val) => acc + val, 0); return roundToPrecision(sum, 2); } /** * Format quantity with appropriate unit of measure * Handles common bakery units */ export function formatQuantity( value: number | string, unit: string, locale: string = 'es-ES' ): string { const numValue = typeof value === 'string' ? parseFloat(value) : value; if (isNaN(numValue)) { return `0 ${unit}`; } // Determine decimals based on unit let decimals = 2; if (unit.toLowerCase() === 'unidades' || unit.toLowerCase() === 'units') { decimals = 0; } else if (['kg', 'l', 'lb'].includes(unit.toLowerCase())) { decimals = 2; } const rounded = roundToPrecision(numValue, decimals); const formatted = formatNumber(rounded, locale, { minimumFractionDigits: 0, maximumFractionDigits: decimals, }); return `${formatted} ${unit}`; }