Files
bakery-ia/frontend/src/utils/format.ts
2025-08-28 10:41:04 +02:00

388 lines
9.0 KiB
TypeScript

/**
* Formatting utilities for various data types
*/
// Number formatting
export const formatNumber = (
value: number,
options: Intl.NumberFormatOptions = {}
): string => {
if (typeof value !== 'number' || isNaN(value)) {
return '0';
}
const defaultOptions: Intl.NumberFormatOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
};
return new Intl.NumberFormat('es-ES', { ...defaultOptions, ...options }).format(value);
};
// Currency formatting
export const formatCurrency = (
amount: number,
currency: string = 'EUR',
locale: string = 'es-ES'
): string => {
if (typeof amount !== 'number' || isNaN(amount)) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(0);
}
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(amount);
};
// Percentage formatting
export const formatPercentage = (
value: number,
decimals: number = 1,
locale: string = 'es-ES'
): string => {
if (typeof value !== 'number' || isNaN(value)) {
return '0%';
}
return new Intl.NumberFormat(locale, {
style: 'percent',
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
}).format(value / 100);
};
// Compact number formatting (1K, 1M, etc.)
export const formatCompactNumber = (
value: number,
locale: string = 'es-ES'
): string => {
if (typeof value !== 'number' || isNaN(value)) {
return '0';
}
return new Intl.NumberFormat(locale, {
notation: 'compact',
compactDisplay: 'short',
}).format(value);
};
// Decimal formatting with specific precision
export const formatDecimal = (
value: number,
decimals: number = 2,
locale: string = 'es-ES'
): string => {
if (typeof value !== 'number' || isNaN(value)) {
return '0';
}
return new Intl.NumberFormat(locale, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
}).format(value);
};
// File size formatting
export const formatFileSize = (bytes: number): string => {
if (typeof bytes !== 'number' || isNaN(bytes)) {
return '0 B';
}
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
if (bytes === 0) return '0 B';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
const size = bytes / Math.pow(1024, i);
return `${formatDecimal(size, i === 0 ? 0 : 1)} ${sizes[i]}`;
};
// Duration formatting (milliseconds to human readable)
export const formatDuration = (milliseconds: number): string => {
if (typeof milliseconds !== 'number' || isNaN(milliseconds) || milliseconds < 0) {
return '0s';
}
const seconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days}d ${hours % 24}h`;
} else if (hours > 0) {
return `${hours}h ${minutes % 60}m`;
} else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
} else {
return `${seconds}s`;
}
};
// Short duration formatting (for performance metrics)
export const formatShortDuration = (milliseconds: number): string => {
if (typeof milliseconds !== 'number' || isNaN(milliseconds)) {
return '0ms';
}
if (milliseconds < 1000) {
return `${Math.round(milliseconds)}ms`;
}
const seconds = milliseconds / 1000;
if (seconds < 60) {
return `${formatDecimal(seconds, 1)}s`;
}
const minutes = seconds / 60;
return `${formatDecimal(minutes, 1)}m`;
};
// Text formatting
export const truncateText = (
text: string,
maxLength: number,
suffix: string = '...'
): string => {
if (!text || typeof text !== 'string') {
return '';
}
if (text.length <= maxLength) {
return text;
}
return text.slice(0, maxLength - suffix.length) + suffix;
};
// Capitalize first letter
export const capitalize = (text: string): string => {
if (!text || typeof text !== 'string') {
return '';
}
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
};
// Title case formatting
export const titleCase = (text: string): string => {
if (!text || typeof text !== 'string') {
return '';
}
return text
.toLowerCase()
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
// Kebab case to title case
export const kebabToTitle = (text: string): string => {
if (!text || typeof text !== 'string') {
return '';
}
return text
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
// Snake case to title case
export const snakeToTitle = (text: string): string => {
if (!text || typeof text !== 'string') {
return '';
}
return text
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
// Camel case to title case
export const camelToTitle = (text: string): string => {
if (!text || typeof text !== 'string') {
return '';
}
return text
.replace(/([A-Z])/g, ' $1')
.replace(/^./, str => str.toUpperCase())
.trim();
};
// Phone number formatting
export const formatPhoneNumber = (phone: string): string => {
if (!phone || typeof phone !== 'string') {
return '';
}
// Remove all non-digit characters
const digits = phone.replace(/\D/g, '');
// Spanish phone number format
if (digits.length === 9) {
return `${digits.slice(0, 3)} ${digits.slice(3, 6)} ${digits.slice(6)}`;
}
// International format
if (digits.length === 11 && digits.startsWith('34')) {
return `+34 ${digits.slice(2, 5)} ${digits.slice(5, 8)} ${digits.slice(8)}`;
}
// Return original if doesn't match expected formats
return phone;
};
// Unit of measure formatting
export const formatUnit = (value: number, unit: string): string => {
if (typeof value !== 'number' || isNaN(value)) {
return `0 ${unit}`;
}
const formattedValue = value % 1 === 0 ? value.toString() : formatDecimal(value);
return `${formattedValue} ${unit}`;
};
// Weight formatting with unit conversion
export const formatWeight = (grams: number): string => {
if (typeof grams !== 'number' || isNaN(grams)) {
return '0 g';
}
if (grams >= 1000) {
const kg = grams / 1000;
return `${formatDecimal(kg)} kg`;
}
return `${formatDecimal(grams, 0)} g`;
};
// Volume formatting with unit conversion
export const formatVolume = (milliliters: number): string => {
if (typeof milliliters !== 'number' || isNaN(milliliters)) {
return '0 ml';
}
if (milliliters >= 1000) {
const liters = milliliters / 1000;
return `${formatDecimal(liters)} l`;
}
return `${formatDecimal(milliliters, 0)} ml`;
};
// Temperature formatting
export const formatTemperature = (
celsius: number,
unit: 'C' | 'F' = 'C'
): string => {
if (typeof celsius !== 'number' || isNaN(celsius)) {
return `${unit}`;
}
if (unit === 'F') {
const fahrenheit = (celsius * 9/5) + 32;
return `${formatDecimal(fahrenheit, 0)}°F`;
}
return `${formatDecimal(celsius, 0)}°C`;
};
// List formatting (join with commas and "and")
export const formatList = (
items: string[],
conjunction: string = 'y'
): string => {
if (!Array.isArray(items) || items.length === 0) {
return '';
}
if (items.length === 1) {
return items[0];
}
if (items.length === 2) {
return `${items[0]} ${conjunction} ${items[1]}`;
}
const lastItem = items[items.length - 1];
const otherItems = items.slice(0, -1);
return `${otherItems.join(', ')} ${conjunction} ${lastItem}`;
};
// Address formatting
export const formatAddress = (address: {
street?: string;
city?: string;
postal_code?: string;
country?: string;
}): string => {
const parts = [];
if (address.street) parts.push(address.street);
if (address.postal_code && address.city) {
parts.push(`${address.postal_code} ${address.city}`);
} else if (address.city) {
parts.push(address.city);
} else if (address.postal_code) {
parts.push(address.postal_code);
}
if (address.country) parts.push(address.country);
return parts.join(', ');
};
// HTML stripping (for displaying rich text as plain text)
export const stripHtml = (html: string): string => {
if (!html || typeof html !== 'string') {
return '';
}
// Create a temporary div element to strip HTML tags
const tmp = document.createElement('div');
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || '';
};
// URL slug generation
export const slugify = (text: string): string => {
if (!text || typeof text !== 'string') {
return '';
}
return text
.toLowerCase()
.replace(/[^\w ]+/g, '')
.replace(/ +/g, '-');
};
// Name formatting (last name, first name)
export const formatName = (
firstName: string,
lastName: string,
format: 'first_last' | 'last_first' | 'initials' = 'first_last'
): string => {
const first = firstName?.trim() || '';
const last = lastName?.trim() || '';
if (!first && !last) return '';
if (!first) return last;
if (!last) return first;
switch (format) {
case 'last_first':
return `${last}, ${first}`;
case 'initials':
return `${first.charAt(0)}.${last.charAt(0)}.`;
case 'first_last':
default:
return `${first} ${last}`;
}
};