UNPKG

@empellio/business-hours

Version:

A lightweight and accurate TypeScript library for handling business opening hours. Supports multiple daily time slots, holidays, exceptions, overnight openings, and timezone-aware calculations.

302 lines (294 loc) 9.61 kB
'use strict'; var luxon = require('luxon'); // src/core/compute.ts // src/helpers/parseHHMM.ts function isHHMM(value) { return /^\d{2}:\d{2}$/.test(value); } // src/helpers/mergeSlots.ts function validateAndSortDay(slots, strict) { const valid = []; for (const s of slots) { if (!isHHMM(s.open) || !isHHMM(s.close)) { if (strict) throw new Error(`Invalid HH:MM in slot ${s.open}-${s.close}`); else continue; } valid.push({ open: s.open, close: s.close }); } valid.sort((a, b) => a.open < b.open ? -1 : a.open > b.open ? 1 : 0); const merged = []; for (const s of valid) { const last = merged[merged.length - 1]; if (!last) { merged.push(s); continue; } if (s.open <= last.close) { if (s.close > last.close) last.close = s.close; } else { merged.push({ ...s }); } } return merged; } // src/core/normalize.ts var WEEK_KEYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]; function normalizeConfig(input) { const strict = Boolean(input.strictValidation); const locale = input.locale ?? "en-US"; const firstDayOfWeek = input.firstDayOfWeek ?? "mon"; const holidays = input.holidays ?? []; const exceptions = input.exceptions ?? []; const week = { ...input.week }; for (const k of WEEK_KEYS) { if (!(k in week)) { if (strict) throw new Error(`Missing weekday: ${k}`); week[k] = "closed"; } const v = week[k]; if (v !== "closed") { week[k] = validateAndSortDay(v, strict); } } return { timezone: input.timezone, week, holidays, exceptions, locale, firstDayOfWeek, strictValidation: strict }; } function dayKeyFromLuxon(dt) { const map = { 1: "mon", 2: "tue", 3: "wed", 4: "thu", 5: "fri", 6: "sat", 7: "sun" }; return map[dt.weekday]; } function toJSON(config) { return JSON.parse(JSON.stringify(config)); } function resolveDaySpecForDate(dateISO, cfg) { const ex = cfg.exceptions?.find((e) => e.date === dateISO); if (ex) return ex.closed ? "closed" : ex.slots ?? "closed"; const hol = cfg.holidays?.find((h) => h.date === dateISO); if (hol) return hol.closed ? "closed" : hol.slots ?? "closed"; const dt = luxon.DateTime.fromISO(dateISO, { zone: cfg.timezone }); const key = dayKeyFromLuxon(dt); return cfg.week[key]; } function dtFrom(date, zone) { if (date === void 0) return luxon.DateTime.now().setZone(zone); if (date instanceof Date) return luxon.DateTime.fromJSDate(date).setZone(zone); const parsed = luxon.DateTime.fromISO(date, { setZone: true }); return parsed.isValid ? parsed.setZone(zone) : luxon.DateTime.invalid("Invalid date"); } // src/core/compute.ts function createBusinessHours(config) { const cfg = normalizeConfig(config); const cache = /* @__PURE__ */ new Map(); const MAX_CACHE_DAYS = 14; function slotsOn(date) { const dt = dtFrom(date, cfg.timezone); const startOfDay = dt.startOf("day"); const dateISO = startOfDay.toISODate(); const cached = cache.get(dateISO); if (cached) return cached; const spec = resolveDaySpecForDate(dateISO, cfg); const ranges = []; if (spec !== "closed") { for (const s of spec) { if (s.open <= s.close) { ranges.push(rangeFor(startOfDay, s)); } else { ranges.push(rangeFor(startOfDay, { open: s.open, close: "23:59" })); } } } const prevDay = startOfDay.minus({ days: 1 }).startOf("day"); const prevISO = prevDay.toISODate(); const prevSpec = resolveDaySpecForDate(prevISO, cfg); if (prevSpec !== "closed") { for (const s of prevSpec) { if (s.open > s.close) { ranges.push(rangeFor(startOfDay, { open: "00:00", close: s.close })); } } } const result = ranges.map((r) => ({ open: r.open.toJSDate(), close: r.close.toJSDate() })).sort((a, b) => a.open.getTime() - b.open.getTime()); cache.set(dateISO, result); if (cache.size > MAX_CACHE_DAYS) { const firstKey = cache.keys().next().value; if (firstKey) cache.delete(firstKey); } return result; } function currentSlot(at) { const now = dtFrom(at, cfg.timezone); const todays = slotsOn(now.toJSDate()); for (const s of todays) { const o = luxon.DateTime.fromJSDate(s.open); const c = luxon.DateTime.fromJSDate(s.close); if (luxon.Interval.fromDateTimes(o, c).contains(now)) return s; } return null; } function isOpenAt(date) { return currentSlot(date) !== null; } function isOpenNow() { return isOpenAt(dtFrom(void 0, cfg.timezone).toJSDate()); } function nextOpen(from) { const start = dtFrom(from, cfg.timezone); const todaySlots = slotsOn(start.toJSDate()); for (const slot of todaySlots) { const o = luxon.DateTime.fromJSDate(slot.open); const c = luxon.DateTime.fromJSDate(slot.close); if (c <= start) continue; if (start < o) return { start: o.toJSDate(), end: c.toJSDate() }; if (luxon.Interval.fromDateTimes(o, c).contains(start)) return { start: start.toJSDate(), end: c.toJSDate() }; } for (let i = 1; i <= 14; i++) { const day = start.plus({ days: i }).startOf("day"); const slots = slotsOn(day.toJSDate()); if (slots.length > 0) { return { start: slots[0].open, end: slots[0].close }; } } return null; } function nextClose(from) { const start = dtFrom(from, cfg.timezone); const slot = currentSlot(start.toJSDate()); if (slot) return { at: slot.close }; const upcoming = nextOpen(start.toJSDate()); if (!upcoming) return null; return { at: upcoming.end }; } function timeUntilClose(at) { const start = dtFrom(at, cfg.timezone); const slot = currentSlot(start.toJSDate()); if (!slot) return null; const diff = luxon.DateTime.fromJSDate(slot.close).diff(start).as("milliseconds"); if (diff <= 0) return null; return { ms: diff, minutes: Math.ceil(diff / 6e4) }; } function timeUntilOpen(at) { const start = dtFrom(at, cfg.timezone); if (currentSlot(start.toJSDate())) return { ms: 0, minutes: 0 }; const upcoming = nextOpen(start.toJSDate()); if (!upcoming) return null; const diff = luxon.DateTime.fromJSDate(upcoming.start).diff(start).as("milliseconds"); return { ms: diff, minutes: Math.ceil(diff / 6e4) }; } function todaysSlots(at) { const dt = dtFrom(at, cfg.timezone); return slotsOn(dt.toJSDate()); } function weeklySummary(options) { const lines = []; const start = luxon.DateTime.now().setZone(cfg.timezone).startOf("week"); for (let i = 0; i < 7; i++) { const day = start.plus({ days: i }); const slots = slotsOn(day.toJSDate()); if (slots.length === 0) { lines.push(`${day.toFormat("EEE")} closed`); } else { const parts = slots.map((s) => { const o = luxon.DateTime.fromJSDate(s.open).setZone(cfg.timezone); const c = luxon.DateTime.fromJSDate(s.close).setZone(cfg.timezone); return `${o.toFormat("HH:mm")}\u2013${c.toFormat("HH:mm")}`; }); lines.push(`${day.toFormat("EEE")} ${parts.join(", ")}`); } } return options?.join ? lines.join("\n") : lines; } function listUpcomingSlots(daysAhead = 14) { const now = luxon.DateTime.now().setZone(cfg.timezone).startOf("day"); const out = []; for (let i = 0; i <= daysAhead; i++) { const day = now.plus({ days: i }); const dateISO = day.toISODate(); const slots = slotsOn(day.toJSDate()); out.push({ date: dateISO, slots }); } return out; } function withOverrides(overrides) { return createBusinessHours({ ...cfg, ...overrides }); } function toJSONLocal() { return { timezone: cfg.timezone, week: cfg.week, holidays: cfg.holidays, exceptions: cfg.exceptions, locale: cfg.locale, firstDayOfWeek: cfg.firstDayOfWeek, strictValidation: cfg.strictValidation }; } return { isOpenNow, isOpenAt, nextOpen, nextClose, currentSlot, timeUntilClose, timeUntilOpen, todaysSlots, slotsOn, weeklySummary, listUpcomingSlots, withOverrides, toJSON: toJSONLocal }; } function rangeFor(dayStart, slot) { const [oh, om] = slot.open.split(":").map(Number); const [ch, cm] = slot.close.split(":").map(Number); const open = dayStart.set({ hour: oh, minute: om, second: 0, millisecond: 0 }); const close = dayStart.set({ hour: ch, minute: cm, second: 0, millisecond: 0 }); return { open, close }; } function formatSlot(slot, options) { const zone = options?.timezone; const locale = options?.locale ?? "en-US"; const o = luxon.DateTime.fromJSDate(slot.open).setZone(zone).setLocale(locale); const c = luxon.DateTime.fromJSDate(slot.close).setZone(zone).setLocale(locale); return `${o.toFormat("HH:mm")}\u2013${c.toFormat("HH:mm")}`; } function formatDayName(weekday, options) { const locale = options?.locale ?? "en-US"; const map = { mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6, sun: 7 }; const dt = luxon.DateTime.now().set({ weekday: map[weekday] }).setLocale(locale); return dt.toFormat("EEE"); } // src/index.ts function fromJSON(configLike) { return createBusinessHours(configLike); } exports.createBusinessHours = createBusinessHours; exports.formatDayName = formatDayName; exports.formatSlot = formatSlot; exports.fromJSON = fromJSON; exports.toJSON = toJSON; //# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map