/** * Utilidades para análisis y métricas */ // Tipos export type GroupByPeriod = 'day' | 'week' | 'month'; /** * Formatear monto a moneda * @param amount Monto en centavos * @param currency Código de moneda (default: ARS) * @returns String formateado */ export function formatCurrency(amount: number, currency: string = 'ARS'): string { const amountInUnits = amount / 100; const formatter = new Intl.NumberFormat('es-AR', { style: 'currency', currency: currency, minimumFractionDigits: 2, maximumFractionDigits: 2, }); return formatter.format(amountInUnits); } /** * Formatear monto simple (sin símbolo de moneda) * @param amount Monto en centavos * @returns String formateado */ export function formatAmount(amount: number): string { const amountInUnits = amount / 100; return amountInUnits.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2, }); } /** * Calcular crecimiento porcentual * @param current Valor actual * @param previous Valor anterior (null si no hay datos previos) * @returns Porcentaje de crecimiento o null */ export function calculateGrowth(current: number, previous: number | null): number | null { if (previous === null || previous === undefined) { return null; } if (previous === 0) { return current > 0 ? 100 : 0; } const growth = ((current - previous) / previous) * 100; return Math.round(growth * 100) / 100; } /** * Agrupar datos por fecha * @param data Array de datos con campo de fecha * @param dateField Nombre del campo de fecha * @param groupBy Período de agrupación (day, week, month) * @returns Objeto con datos agrupados */ export function groupByDate>( data: T[], dateField: string, groupBy: GroupByPeriod ): Record { const grouped: Record = {}; for (const item of data) { const date = new Date(item[dateField]); let key: string; switch (groupBy) { case 'day': key = date.toISOString().split('T')[0]; // YYYY-MM-DD break; case 'week': key = getWeekKey(date); // YYYY-WXX break; case 'month': key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; // YYYY-MM break; default: key = date.toISOString().split('T')[0]; } if (!grouped[key]) { grouped[key] = []; } grouped[key].push(item); } return grouped; } /** * Rellenar fechas faltantes con valores en cero * @param data Array de datos con campo date * @param startDate Fecha de inicio * @param endDate Fecha de fin * @param groupBy Período de agrupación * @returns Array con todas las fechas incluidas las faltantes */ export function fillMissingDates( data: T[], startDate: Date, endDate: Date, groupBy: GroupByPeriod ): T[] { const result: T[] = []; const dataMap = new Map(data.map(d => [d.date, d])); const current = new Date(startDate); const end = new Date(endDate); while (current <= end) { let key: string; switch (groupBy) { case 'day': key = current.toISOString().split('T')[0]; current.setDate(current.getDate() + 1); break; case 'week': key = getWeekKey(current); current.setDate(current.getDate() + 7); break; case 'month': key = `${current.getFullYear()}-${String(current.getMonth() + 1).padStart(2, '0')}`; current.setMonth(current.getMonth() + 1); break; default: key = current.toISOString().split('T')[0]; current.setDate(current.getDate() + 1); } if (dataMap.has(key)) { result.push(dataMap.get(key)!); } else { // Crear objeto vacío con valores por defecto const emptyItem: any = { date: key }; // Detectar campos numéricos del primer elemento y ponerlos en 0 if (data.length > 0) { const firstItem = data[0]; for (const [field, value] of Object.entries(firstItem)) { if (field !== 'date' && typeof value === 'number') { emptyItem[field] = 0; } } } result.push(emptyItem); } } return result; } /** * Calcular promedio de un array de números * @param values Array de números * @returns Promedio */ export function calculateAverage(values: number[]): number { if (values.length === 0) return 0; return values.reduce((sum, val) => sum + val, 0) / values.length; } /** * Calcular mediana de un array de números * @param values Array de números * @returns Mediana */ export function calculateMedian(values: number[]): number { if (values.length === 0) return 0; const sorted = [...values].sort((a, b) => a - b); const middle = Math.floor(sorted.length / 2); if (sorted.length % 2 === 0) { return (sorted[middle - 1] + sorted[middle]) / 2; } return sorted[middle]; } /** * Calcular percentil * @param values Array de números * @param percentile Percentil a calcular (0-100) * @returns Valor del percentil */ export function calculatePercentile(values: number[], percentile: number): number { if (values.length === 0) return 0; if (percentile < 0 || percentile > 100) return 0; const sorted = [...values].sort((a, b) => a - b); const index = (percentile / 100) * (sorted.length - 1); const lower = Math.floor(index); const upper = Math.ceil(index); const weight = index - lower; if (upper >= sorted.length) return sorted[lower]; return sorted[lower] * (1 - weight) + sorted[upper] * weight; } /** * Calcular desviación estándar * @param values Array de números * @returns Desviación estándar */ export function calculateStandardDeviation(values: number[]): number { if (values.length === 0) return 0; const avg = calculateAverage(values); const squareDiffs = values.map(value => Math.pow(value - avg, 2)); const avgSquareDiff = calculateAverage(squareDiffs); return Math.sqrt(avgSquareDiff); } /** * Agrupar datos por campo * @param data Array de datos * @param field Campo para agrupar * @returns Objeto con datos agrupados */ export function groupByField>( data: T[], field: string ): Record { return data.reduce((acc, item) => { const key = item[field]?.toString() || 'unknown'; if (!acc[key]) { acc[key] = []; } acc[key].push(item); return acc; }, {} as Record); } /** * Calcular tasa de conversión * @param completed Número de completados * @param total Número total * @returns Porcentaje de conversión */ export function calculateConversionRate(completed: number, total: number): number { if (total === 0) return 0; return Math.round((completed / total) * 100 * 100) / 100; } /** * Formatear número con separadores de miles * @param value Número a formatear * @returns String formateado */ export function formatNumber(value: number): string { return value.toLocaleString('es-AR'); } /** * Formatear porcentaje * @param value Valor decimal (0-1) o porcentaje (0-100) * @param isDecimal Si el valor está en formato decimal * @returns String formateado con símbolo % */ export function formatPercentage(value: number, isDecimal: boolean = false): string { const percentage = isDecimal ? value * 100 : value; return `${Math.round(percentage * 100) / 100}%`; } /** * Obtener clave de semana para una fecha * @param date Fecha * @returns String en formato YYYY-WXX */ function getWeekKey(date: Date): string { const year = date.getFullYear(); // Calcular número de semana (aproximado) const startOfYear = new Date(year, 0, 1); const days = Math.floor((date.getTime() - startOfYear.getTime()) / (24 * 60 * 60 * 1000)); const weekNumber = Math.ceil((days + startOfYear.getDay() + 1) / 7); return `${year}-W${String(weekNumber).padStart(2, '0')}`; } /** * Comparar dos períodos de fechas * @param currentStart Inicio período actual * @param currentEnd Fin período actual * @param previousStart Inicio período anterior * @param previousEnd Fin período anterior * @returns Objeto con comparación */ export function comparePeriods( currentStart: Date, currentEnd: Date, previousStart: Date, previousEnd: Date ): { currentDuration: number; previousDuration: number; durationRatio: number; } { const currentDuration = currentEnd.getTime() - currentStart.getTime(); const previousDuration = previousEnd.getTime() - previousStart.getTime(); return { currentDuration, previousDuration, durationRatio: previousDuration > 0 ? currentDuration / previousDuration : 1, }; } /** * Generar rango de fechas * @param startDate Fecha de inicio * @param endDate Fecha de fin * @returns Array de fechas */ export function generateDateRange(startDate: Date, endDate: Date): Date[] { const dates: Date[] = []; const current = new Date(startDate); const end = new Date(endDate); while (current <= end) { dates.push(new Date(current)); current.setDate(current.getDate() + 1); } return dates; } /** * Redondear a número específico de decimales * @param value Valor a redondear * @param decimals Número de decimales * @returns Valor redondeado */ export function roundToDecimals(value: number, decimals: number = 2): number { const multiplier = Math.pow(10, decimals); return Math.round(value * multiplier) / multiplier; } export default { formatCurrency, formatAmount, calculateGrowth, groupByDate, fillMissingDates, calculateAverage, calculateMedian, calculatePercentile, calculateStandardDeviation, groupByField, calculateConversionRate, formatNumber, formatPercentage, comparePeriods, generateDateRange, roundToDecimals, };