Files
bakery-ia/frontend/src/utils/numberFormatting.ts

307 lines
7.4 KiB
TypeScript
Raw Normal View History

2025-10-21 19:50:07 +02:00
/**
* 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}`;
}