import type { Activity, MetricACWRData } from '@/types'; /** * Format date to YYYY-MM-DD in local timezone (not UTC) */ function formatDateLocal(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } /** * Calculate ACWR for a specific metric (distance, duration, or TSS) * @param activities - Array of activities * @param metricExtractor - Function to extract the metric value from an activity * @param dateRange - Optional date range to maintain consistency */ export function calculateMetricACWR( activities: Activity[], metricExtractor: (activity: Activity) => number | undefined, dateRange?: { start: Date; end: Date } ): MetricACWRData { if (activities.length === 0 && !dateRange) { return { dates: [], values: [], average7d: [], average28d: [], acwr: [], }; } // Sort activities by date const sortedActivities = [...activities].sort((a, b) => a.date.getTime() - b.date.getTime()); // Get date range const startDate = dateRange?.start || (sortedActivities.length > 0 ? new Date(sortedActivities[0].date) : new Date()); const endDate = dateRange?.end || (sortedActivities.length > 0 ? new Date(sortedActivities[sortedActivities.length - 1].date) : new Date()); // Create a map of date -> daily sum const dailyValues = new Map(); sortedActivities.forEach(activity => { const dateStr = formatDateLocal(activity.date); const value = metricExtractor(activity); if (value !== undefined) { dailyValues.set(dateStr, (dailyValues.get(dateStr) || 0) + value); } }); const dates: string[] = []; const values: (number | null)[] = []; const average7d: (number | null)[] = []; const average28d: (number | null)[] = []; const acwr: (number | null)[] = []; // Generate all dates in range const allDates: Date[] = []; for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { allDates.push(new Date(d)); } // Calculate for each date allDates.forEach((date, index) => { const dateStr = formatDateLocal(date); dates.push(dateStr); // Get daily value const dailyValue = dailyValues.get(dateStr) || null; values.push(dailyValue); // Calculate 7-day rolling average (acute load) let acuteSum = 0; let acuteCount = 0; for (let i = Math.max(0, index - 6); i <= index; i++) { const checkDateStr = formatDateLocal(allDates[i]); const val = dailyValues.get(checkDateStr); if (val !== undefined) { acuteSum += val; acuteCount++; } } const acuteAvg = acuteCount > 0 ? acuteSum / 7 : null; average7d.push(acuteAvg); // Calculate 28-day rolling average let chronicSum = 0; let chronicCount = 0; for (let i = Math.max(0, index - 27); i <= index; i++) { const checkDateStr = formatDateLocal(allDates[i]); const val = dailyValues.get(checkDateStr); if (val !== undefined) { chronicSum += val; chronicCount++; } } const chronicAvg = chronicCount > 0 ? chronicSum / 28 : null; average28d.push(chronicAvg); // Calculate ACWR (need at least 28 days of data) if (index < 27) { acwr.push(null); return; } // For ACWR, use the full 28-day sum (not average) let acwrChronicSum = 0; for (let i = index - 27; i <= index; i++) { const checkDateStr = formatDateLocal(allDates[i]); const val = dailyValues.get(checkDateStr); if (val !== undefined) { acwrChronicSum += val; } } const acwrChronicAvg = acwrChronicSum / 28; // Calculate ACWR if (acwrChronicAvg > 0 && acuteAvg !== null) { acwr.push(acuteAvg / acwrChronicAvg); } else { acwr.push(null); } }); return { dates, values, average7d, average28d, acwr, }; }