/** * Time helpers. Everything here works in *local* time — the prototype derived day * keys from `toISOString()`, which is UTC and files late/early work under the wrong * day. Timestamps are epoch milliseconds; rounding is applied at render only. */ export const MINUTE = 60_000; export const HOUR = 3_600_000; export const DAY = 86_400_000; const pad = (n) => String(n).padStart(2, '0'); /** Local 'YYYY-MM-DD' for a timestamp. */ export function localDateKey(ts = Date.now()) { const d = new Date(ts); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; } /** Timestamp of local midnight starting `dateKey`. */ export function startOfDay(dateKey) { const [y, m, d] = dateKey.split('-').map(Number); return new Date(y, m - 1, d).getTime(); } /** Timestamp of 'HH:MM' local time on `dateKey`. */ export function atLocalTime(dateKey, hhmm) { const [y, m, d] = dateKey.split('-').map(Number); const [h, min] = hhmm.split(':').map(Number); return new Date(y, m - 1, d, h, min, 0, 0).getTime(); } /** `dateKey` shifted by `n` days, DST-safe. */ export function addDays(dateKey, n) { const [y, m, d] = dateKey.split('-').map(Number); return localDateKey(new Date(y, m - 1, d + n).getTime()); } /** JS weekday for a date key: 0 = Sunday … 6 = Saturday. */ export function weekday(dateKey) { return new Date(startOfDay(dateKey)).getDay(); } /** * Round to the nearest `minutes`, half up, relative to *local midnight* rather * than the epoch — so zones with a :30 or :45 offset still land on whole * wall-clock steps. Always drops seconds. */ export function roundTs(ts, minutes) { const step = Math.max(1, minutes || 1) * MINUTE; const midnight = startOfDay(localDateKey(ts)); return midnight + Math.round((ts - midnight) / step) * step; } /** 'HH:MM' in local time, rounded to `roundMinutes`. */ export function fmtTime(ts, roundMinutes = 1) { const d = new Date(roundTs(ts, roundMinutes)); return `${pad(d.getHours())}:${pad(d.getMinutes())}`; } /** A duration as '8h15' / '0h45'. */ export function fmtDuration(ms) { const total = Math.max(0, Math.round(ms / MINUTE)); return `${Math.floor(total / 60)}h${pad(total % 60)}`; } /** 'vendredi 5 septembre' — the resume heading. */ export function fmtDayLabel(dateKey) { const s = new Intl.DateTimeFormat('fr-FR', { weekday: 'long', day: 'numeric', month: 'long', }).format(new Date(startOfDay(dateKey))); return s.charAt(0).toUpperCase() + s.slice(1); } /** 'HH:MM' → minutes since midnight. */ export function hhmmToMinutes(hhmm) { const [h, m] = hhmm.split(':').map(Number); return h * 60 + m; }