import type { HabitCadence } from "@rakazo/contracts"; const DAY_MS = 86_400_000; function utcMidnight(date: string): number { return Date.parse(`${date}T00:00:00Z`); } /** The calendar date at `now` in `timeZone`, e.g. "2026-10-03". */ export function localDateIn(timeZone: string, now: Date = new Date()): string { const parts = new Map( new Intl.DateTimeFormat("en-US", { timeZone, year: "numeric", month: "2-digit", day: "2-digit", }) .formatToParts(now) .map((part) => [part.type, part.value]), ); return `${parts.get("year")}-${parts.get("month")}-${parts.get("day")}`; } /** `date` moved by `days` calendar days. */ export function addDays(date: string, days: number): string { return new Date(utcMidnight(date) + days * DAY_MS).toISOString().slice(0, 10); } /** Day of the week, 0 = Sunday … 6 = Saturday. */ export function weekdayOf(date: string): number { return new Date(utcMidnight(date)).getUTCDay(); } /** The Monday that starts the week containing `date`. */ export function weekStartOf(date: string): string { return addDays(date, -((weekdayOf(date) + 6) % 7)); } function isWeekday(date: string): boolean { const day = weekdayOf(date); return day >= 1 && day <= 5; } /** The latest Monday–Friday date strictly before `date`. */ function previousWeekday(date: string): string { let cursor = addDays(date, -1); while (!isWeekday(cursor)) cursor = addDays(cursor, -1); return cursor; } /** Whether the habit is done in the period that contains `today`. */ export function habitDoneInPeriod( cadence: HabitCadence, doneDates: ReadonlySet, today: string, ): boolean { if (cadence !== "weekly") return doneDates.has(today); const start = weekStartOf(today); for (let date = start; date <= today; date = addDays(date, 1)) { if (doneDates.has(date)) return true; } return false; } /** * Periods in a row the habit was done, ending today. The current period counts once it is done; * until then it stays open and does not break the streak. Weekday habits skip weekends, weekly * habits count Monday-to-Sunday weeks with at least one done day. */ export function habitStreak( cadence: HabitCadence, doneDates: ReadonlySet, today: string, ): number { let streak = 0; if (cadence === "weekly") { const weeks = new Set([...doneDates].map(weekStartOf)); const current = weekStartOf(today); for (let week = weeks.has(current) ? current : addDays(current, -7); weeks.has(week); ) { streak += 1; week = addDays(week, -7); } return streak; } if (cadence === "weekdays") { let cursor = isWeekday(today) && doneDates.has(today) ? today : previousWeekday(today); while (doneDates.has(cursor)) { streak += 1; cursor = previousWeekday(cursor); } return streak; } let cursor = doneDates.has(today) ? today : addDays(today, -1); while (doneDates.has(cursor)) { streak += 1; cursor = addDays(cursor, -1); } return streak; }