All files hours.js

91.3% Statements 63/69
90.14% Branches 64/71
100% Functions 5/5
100% Lines 56/56

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110                            1x     40x 34x 4x 4x   2x         26x 26x 26x 26x 26x 26x 26x 26x 26x       4x 11x 11x 11x 3x     1x       22x 22x 18x 48x 48x 36x   5x       18x 18x   14x 3x     11x 11x   11x 2x     9x 9x 9x   9x       9x 9x 9x 4x 4x 4x 1x         8x 2x 1x   1x     6x 2x   4x 2x   2x    
/**
 * 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) {
  Iif (hm == null) return null;
  const parts = String(hm).split(':');
  Iif (parts.length !== 2) return null;
  const h = Number(parts[0]);
  const m = Number(parts[1]);
  Iif (!Number.isFinite(h) || !Number.isFinite(m)) return null;
  Iif (h === 24 && m === 0) return 1440;
  Iif (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]);
  Iif (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] };
}