/* Pure helpers for streaks, totals, and the goal-window countdown. */ const DAY_MS = 86_400_000; export function todayISO(d = new Date()) { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; } export function isoToDate(iso) { const [y, m, d] = iso.split('-').map(Number); return new Date(y, m - 1, d); } export function addDays(iso, n) { const d = isoToDate(iso); d.setDate(d.getDate() + n); return todayISO(d); } export function eachDay(startISO, endISO) { const out = []; let cur = startISO; while (cur <= endISO) { out.push(cur); cur = addDays(cur, 1); } return out; } export function clampWindowEnd(goal, refISO = todayISO()) { return goal.end_date < refISO ? goal.end_date : refISO; } export function daysRemaining(endISO, refISO = todayISO()) { if (endISO < refISO) return 0; return Math.round((isoToDate(endISO) - isoToDate(refISO)) / DAY_MS); } /** * Build a Set of completed ISO dates for (user, goal) from a flat completions array. */ export function completedSet(completions, user, goalId) { const set = new Set(); for (const c of completions) { if (c.user === user && c.goal_id === goalId && c.completed) set.add(c.date); } return set; } /** * Current streak ending at (or just before) `refISO`, walking back day-by-day * while completions are present, never crossing the goal start. */ export function currentStreak(completedSetForUser, goal, refISO = todayISO()) { const start = goal.start_date; let end = clampWindowEnd(goal, refISO); if (refISO < start) return 0; // If today isn't completed yet and the goal is still active, // start from yesterday — the day isn't over so don't break the streak. if (end === refISO && !completedSetForUser.has(end)) { end = addDays(end, -1); } let cur = end; let n = 0; while (cur >= start && completedSetForUser.has(cur)) { n += 1; cur = addDays(cur, -1); } return n; } export function longestStreak(completedSetForUser, goal, refISO = todayISO()) { const start = goal.start_date; const end = clampWindowEnd(goal, refISO); if (end < start) return 0; let best = 0; let run = 0; for (const d of eachDay(start, end)) { if (completedSetForUser.has(d)) { run += 1; if (run > best) best = run; } else { run = 0; } } return best; } export function totalCompleted(completedSetForUser, goal, refISO = todayISO()) { const start = goal.start_date; const end = clampWindowEnd(goal, refISO); if (end < start) return 0; let n = 0; for (const d of eachDay(start, end)) { if (completedSetForUser.has(d)) n += 1; } return n; } export function windowLength(goal) { return eachDay(goal.start_date, goal.end_date).length; }