// Date helpers for human-friendly display. All task dates are local ISO // "YYYY-MM-DD" strings; comparisons are lexicographic. const DAY_MS = 86_400_000; export function todayIso(): string { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } export function fromIso(s: string): Date { const [y, m, d] = s.split("-").map(Number); return new Date(y, m - 1, d); } export function toIso(d: Date): string { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } /** Deferred = hidden from action lists until the date arrives. */ export function isDeferred(deferDate: string | null, today = todayIso()): boolean { return !!deferDate && deferDate > today; } export function daysFromToday(dateIso: string): number { return Math.round((fromIso(dateIso).getTime() - fromIso(todayIso()).getTime()) / DAY_MS); } /** "Today" / "Tomorrow" / "Mon 18 Aug" — no ISO date math for the reader. */ export function friendlyDate(dateIso: string): string { const diff = daysFromToday(dateIso); if (diff === 0) return "Today"; if (diff === 1) return "Tomorrow"; if (diff === -1) return "Yesterday"; const d = fromIso(dateIso); const wd = d.toLocaleDateString("en-AU", { weekday: "short" }); const mo = d.toLocaleDateString("en-AU", { month: "short" }); const yr = d.getFullYear() !== new Date().getFullYear() ? ` ${d.getFullYear()}` : ""; return `${wd} ${d.getDate()} ${mo}${yr}`; } export type DueTone = "overdue" | "today" | "later"; /** Row label for a due date, with a tone the UI can color. */ export function dueMeta(dueIso: string): { label: string; tone: DueTone } { const diff = daysFromToday(dueIso); if (diff < 0) return { label: diff === -1 ? "1d overdue" : `${-diff}d overdue`, tone: "overdue" }; if (diff === 0) return { label: "due today", tone: "today" }; if (diff === 1) return { label: "due tomorrow", tone: "later" }; return { label: `due ${friendlyDate(dueIso)}`, tone: "later" }; }