// Pure streak logic over local calendar days — no React, no storage. Days are // "YYYY-MM-DD" keys in the device's local time zone, so a session at 23:50 // counts for the day it was actually done, not the UTC one. export type DayKey = string; export function dayKey(date: Date): DayKey { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, "0"); const d = String(date.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; } /** Local-midnight Date for a day key. */ export function fromKey(key: DayKey): Date { const [y, m, d] = key.split("-").map(Number); return new Date(y, m - 1, d); } function shiftKey(key: DayKey, days: number): DayKey { const date = fromKey(key); date.setDate(date.getDate() + days); return dayKey(date); } /** Unique, ascending day keys from a list of ISO session timestamps. */ export function sessionDays(sessionsIso: string[]): DayKey[] { return Array.from(new Set(sessionsIso.map((iso) => dayKey(new Date(iso))))).sort(); } /** The last `n` day keys ending on `today`, oldest first. */ export function lastNDays(n: number, today: DayKey): DayKey[] { const out: DayKey[] = []; for (let i = n - 1; i >= 0; i--) out.push(shiftKey(today, -i)); return out; } /** * Current streak = consecutive trained days ending today or yesterday (a * streak isn't broken until a full day is missed). Best = longest run ever. */ export function computeStreaks(days: DayKey[], today: DayKey): { current: number; best: number } { const sorted = Array.from(new Set(days)).sort(); if (sorted.length === 0) return { current: 0, best: 0 }; let best = 1; let run = 1; for (let i = 1; i < sorted.length; i++) { run = shiftKey(sorted[i - 1], 1) === sorted[i] ? run + 1 : 1; if (run > best) best = run; } const last = sorted[sorted.length - 1]; const alive = last === today || last === shiftKey(today, -1); return { current: alive ? run : 0, best }; }