// ABOUTME: Cycle phase calculation utilities. // ABOUTME: Determines current cycle day and phase from last period date with variable cycle lengths. import type { CyclePhase, PhaseConfig } from "@/types"; // Base phase configurations with weekly limits and training guidance. // Note: The 'days' field is for the default 31-day cycle; actual boundaries // are calculated dynamically by getPhaseBoundaries() based on cycleLength. export const PHASE_CONFIGS: PhaseConfig[] = [ { name: "MENSTRUAL", days: [1, 3], weeklyLimit: 30, dailyAvg: 10, trainingType: "Gentle rebounding only", }, { name: "FOLLICULAR", days: [4, 15], weeklyLimit: 120, dailyAvg: 17, trainingType: "Strength + rebounding", }, { name: "OVULATION", days: [16, 17], weeklyLimit: 80, dailyAvg: 40, trainingType: "Peak performance", }, { name: "EARLY_LUTEAL", days: [18, 24], weeklyLimit: 100, dailyAvg: 14, trainingType: "Moderate training", }, { name: "LATE_LUTEAL", days: [25, 31], weeklyLimit: 50, dailyAvg: 8, trainingType: "Gentle rebounding ONLY", }, ]; // Phase boundaries scale based on cycle length using fixed luteal, variable follicular. // Per spec: luteal phase is biologically consistent (14 days); follicular expands/contracts. // Formula from specs/cycle-tracking.md: // MENSTRUAL: 1-3 (fixed) // FOLLICULAR: 4 to (cycleLength - 16) // OVULATION: (cycleLength - 15) to (cycleLength - 14) // EARLY_LUTEAL: (cycleLength - 13) to (cycleLength - 7) // LATE_LUTEAL: (cycleLength - 6) to cycleLength function getPhaseBoundaries( cycleLength: number, ): Array<{ phase: CyclePhase; start: number; end: number }> { return [ { phase: "MENSTRUAL", start: 1, end: 3 }, { phase: "FOLLICULAR", start: 4, end: cycleLength - 16 }, { phase: "OVULATION", start: cycleLength - 15, end: cycleLength - 14 }, { phase: "EARLY_LUTEAL", start: cycleLength - 13, end: cycleLength - 7 }, { phase: "LATE_LUTEAL", start: cycleLength - 6, end: cycleLength }, ]; } export function getCycleDay( lastPeriodDate: Date, cycleLength: number, currentDate: Date = new Date(), ): number { const diffMs = currentDate.getTime() - lastPeriodDate.getTime(); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); return (diffDays % cycleLength) + 1; } export function getPhase(cycleDay: number, cycleLength = 31): CyclePhase { const boundaries = getPhaseBoundaries(cycleLength); for (const { phase, start, end } of boundaries) { if (cycleDay >= start && cycleDay <= end) { return phase; } } // Default to late luteal for any days beyond cycle length return "LATE_LUTEAL"; } export function getPhaseConfig(phase: CyclePhase): PhaseConfig { const config = PHASE_CONFIGS.find((c) => c.name === phase); if (!config) { throw new Error(`Unknown phase: ${phase}`); } return config; } export function getPhaseLimit(phase: CyclePhase): number { return getPhaseConfig(phase).weeklyLimit; }