export const MONTH_NAMES = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ]; export function pad2(n: number): string { return String(n).padStart(2, "0"); } export function todayIso(): string { const d = new Date(); return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`; } export function fmtDate(iso: string): string { if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) { return ""; } const [y, m, d] = iso.split("-").map(Number); return `${MONTH_NAMES[m - 1].slice(0, 3)} ${d}, ${y}`; } export function addDaysIso(iso: string, days: number): string { const d = new Date(`${iso}T12:00:00`); d.setDate(d.getDate() + days); return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`; } export function mondayOf(iso: string): string { const d = new Date(`${iso}T12:00:00`); const dow = d.getDay(); d.setDate(d.getDate() + (dow === 0 ? -6 : 1 - dow)); return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`; } export function weeksOfMonth(y: number, m: number): string[] { const firstIso = `${y}-${pad2(m)}-01`; const lastIso = `${y}-${pad2(m)}-${pad2(new Date(y, m, 0).getDate())}`; const out: string[] = []; for (let mon = mondayOf(firstIso); mon <= lastIso; mon = addDaysIso(mon, 7)) { if (addDaysIso(mon, 4) >= firstIso) { out.push(mon); } } return out; } export function weekRangeLabel(mondayIso: string): string { if (!/^\d{4}-\d{2}-\d{2}$/.test(mondayIso)) { return ""; } const fri = addDaysIso(mondayIso, 4); const [, sm, sd] = mondayIso.split("-").map(Number); const [, em, ed] = fri.split("-").map(Number); return `${MONTH_NAMES[sm - 1].slice(0, 3)} ${sd} – ${MONTH_NAMES[em - 1].slice(0, 3)} ${ed}`; } export function fmtWeekRange(startIso: string): string { if (!/^\d{4}-\d{2}-\d{2}$/.test(startIso)) { return fmtDate(startIso); } return weekRangeLabel(startIso); } /** Short date for ranges: "Jun 8". */ export function fmtShortDate(iso: string): string { if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) { return "—"; } const [, m, d] = iso.split("-").map(Number); return `${MONTH_NAMES[m - 1].slice(0, 3)} ${d}`; } /** "Jun 8 – Jun 12" for a Schedule On range. */ export function fmtDateRange(start: string, end: string): string { return `${fmtShortDate(start)} – ${fmtShortDate(end)}`; } /** Inclusive day count between two ISO dates (order-independent). */ export function rangeDayCount(a: string, b: string): number { const d1 = new Date(`${a}T12:00:00`).getTime(); const d2 = new Date(`${b}T12:00:00`).getTime(); return Math.abs(Math.round((d2 - d1) / 86400000)) + 1; } export function fmt12(t: string): string { const [h, m] = t.split(":").map(Number); const ampm = h < 12 ? "AM" : "PM"; const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h; return `${h12}:${String(m).padStart(2, "0")} ${ampm}`; } export function calcDuration(s: string, e: string): string { const [sh, sm] = s.split(":").map(Number); const [eh, em] = e.split(":").map(Number); const mins = eh * 60 + em - (sh * 60 + sm); if (mins <= 0) { return ""; } const h = Math.floor(mins / 60); const m = mins % 60; if (h && m) { return `${h}h ${m}m`; } if (h) { return `${h}h`; } return `${m}m`; } export function formatApptRange(start: string, end: string): string { if (!start) { return "—"; } return end ? `${fmt12(start)} – ${fmt12(end)}` : fmt12(start); } export const TIME_SLOTS: string[] = (() => { const out: string[] = []; for (let m = 5 * 60; m <= 22 * 60; m += 30) { const h = Math.floor(m / 60); const mm = m % 60; out.push(`${String(h).padStart(2, "0")}:${String(mm).padStart(2, "0")}`); } return out; })(); export function parseApptTime(appt: string): { start: string; end: string } { const m = appt.match(/(\d{1,2}):(\d{2})\s*[–-]\s*(\d{1,2}):(\d{2})/); if (m) { return { start: `${m[1].padStart(2, "0")}:${m[2]}`, end: `${m[3].padStart(2, "0")}:${m[4]}`, }; } const single = appt.match(/(\d{1,2}):(\d{2})\s*(AM|PM)/i); if (single) { let h = Number(single[1]) % 12; if (/pm/i.test(single[3])) { h += 12; } return { start: `${pad2(h)}:${single[2]}`, end: "" }; } const startOnly = appt.trim().match(/^(\d{1,2}):(\d{2})$/); if (startOnly) { return { start: `${startOnly[1].padStart(2, "0")}:${startOnly[2]}`, end: "", }; } return { start: "", end: "" }; } export function formatApptTimeValue(start: string, end: string): string { if (!start) { return ""; } return end ? `${start} – ${end}` : start; } export function fmtWeekOf(mondayIso: string): string { if (!mondayIso) { return ""; } return `Week of ${weekRangeLabel(mondayIso)}`; }