/** * Compute "open now" / "always open" for a pharmacy whose opening_hours is * stored as JSON in the { mon, tue, ..., sun } shape produced by * apps/API/opening-hours-osm.js. * * 24/7 is internally represented as every day ["00:00", "24:00"]. The * literal "24:00" is not valid HH:mm ISO; we accept it as 1440 minutes * (00:00 of the next day) so range math works, and treat a 24h week as * the dedicated `kind: '24h'` result. * * Midnight-crossing ranges (close <= open) are detected by comparing * the previous day's range when "now" is in the early hours. */ const DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; function parse(raw) { if (raw == null || raw === '') return null; if (typeof raw === 'object') return raw; try { return JSON.parse(raw); } catch { return null; } } function toMin(hm) { if (hm == null) return null; const parts = String(hm).split(':'); if (parts.length !== 2) return null; const h = Number(parts[0]); const m = Number(parts[1]); if (!Number.isFinite(h) || !Number.isFinite(m)) return null; if (h === 24 && m === 0) return 1440; if (h < 0 || h > 24 || m < 0 || m >= 60) return null; return h * 60 + m; } function findNextOpen(h, now) { for (let off = 1; off <= 7; off++) { const key = DAYS[(now.getDay() + off) % 7]; const r = h[key]; if (Array.isArray(r) && r.length === 2) { return { day: key, time: r[0] }; } } return null; } export function isAlwaysOpen(rawHours) { const h = parse(rawHours); if (!h || typeof h !== 'object') return false; for (const d of DAYS) { const r = h[d]; if (!Array.isArray(r) || r.length !== 2) return false; if (r[0] !== '00:00' || r[1] !== '24:00') return false; } return true; } export function isOpenNow(rawHours, now = new Date()) { const h = parse(rawHours); if (!h) return null; if (isAlwaysOpen(h)) { return { isOpen: true, kind: '24h' }; } const dayKey = DAYS[now.getDay()]; const range = h[dayKey]; if (!Array.isArray(range) || range.length !== 2) { return { isOpen: false, kind: 'closed', nextOpen: findNextOpen(h, now) }; } const openM = toMin(range[0]); const closeM = toMin(range[1]); if (openM == null || closeM == null) return null; const nowM = now.getHours() * 60 + now.getMinutes(); // First: is "now" still inside yesterday's midnight-crossing range? // (Today is irrelevant while we're still in the previous day's after-midnight tail.) const yestKey = DAYS[(now.getDay() + 6) % 7]; const yest = h[yestKey]; if (Array.isArray(yest) && yest.length === 2) { const yOpen = toMin(yest[0]); const yClose = toMin(yest[1]); if (yOpen != null && yClose != null && yClose <= yOpen && nowM < yClose) { return { isOpen: true, kind: 'open', closesAt: yest[1] }; } } // Midnight-crossing: today's range spans past 24:00. if (closeM <= openM) { if (nowM >= openM) { return { isOpen: true, kind: 'open', closesAt: range[1] }; } return { isOpen: false, kind: 'before-open', opensAt: range[0], nextOpen: null }; } if (nowM < openM) { return { isOpen: false, kind: 'before-open', opensAt: range[0], nextOpen: null }; } if (nowM >= closeM) { return { isOpen: false, kind: 'after-close', nextOpen: findNextOpen(h, now) }; } return { isOpen: true, kind: 'open', closesAt: range[1] }; }