Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import List, Optional, Sequence, Tuple
from .health_appointment_slots_types import AppointmentSlot, Booking, ClinicBreak, ClinicSession
from .time_minutes_between import minutes_between ← from time.minutes-between ^1.0.0 · built alongside by fune
#: A break without a date still needs a real date for the time check.
_ANY_DATE = "2000-01-01"
def _minute_of_day(date: str, time: str) -> int:
"""Minute of the day, 0 to 1439; raises on a malformed date or time."""
return minutes_between(date, "00:00", date, time)
def _interval(kind: str, date: Optional[str], start: str, end: str) -> Tuple[str, int, int]:
on = _ANY_DATE if date is None else date
begin = _minute_of_day(on, start)
finish = _minute_of_day(on, end)
if finish <= begin:
raise ValueError(
"%s on %s must end after it starts (%s to %s)" % (kind, "every day" if date is None else date, start, end)
)
return on, begin, finish
def _clock(minute: int) -> str:
return "%02d:%02d" % (minute // 60, minute % 60)
def appointment_slots(
sessions: Sequence[ClinicSession],
slot_minutes: int,
breaks: Sequence[ClinicBreak],
bookings: Sequence[Booking],
) -> List[AppointmentSlot]:
"""Free appointment slots.
Breaks split a session into segments and the slot grid restarts at the
start of each segment, so a 12:30-13:30 lunch gives slots from 13:30.
Bookings only take slots away: they never move the grid, because they are
booked into slots. Intervals are half-open, so a booking ending at 09:15
does not touch the 09:15 slot.
"""
if isinstance(slot_minutes, bool) or not isinstance(slot_minutes, int) or slot_minutes < 1 or slot_minutes > 480:
raise ValueError("slotMinutes must be a whole number from 1 to 480, received %r" % (slot_minutes,))
clinic = [_interval("session", s.date, s.start, s.end) for s in sessions]
closed = [(b.date is None,) + _interval("break", b.date, b.start, b.end) for b in breaks]
booked = [_interval("booking", b.date, b.start, b.end) for b in bookings]
clinic.sort(key=lambda s: (s[0], s[1]))
for previous, current in zip(clinic, clinic[1:]):
if current[0] == previous[0] and current[1] < previous[2]:
raise ValueError("sessions overlap on %s" % (current[0],))
slots: List[AppointmentSlot] = []
for date, start, end in clinic:
pauses = sorted(
((b_start, b_end) for always, b_date, b_start, b_end in closed if always or b_date == date),
)
segments: List[Tuple[int, int]] = []
cursor = start
for p_start, p_end in pauses:
if p_end <= cursor or p_start >= end:
continue
if p_start > cursor:
segments.append((cursor, p_start))
cursor = max(cursor, p_end)
if cursor < end:
segments.append((cursor, end))
for seg_start, seg_end in segments:
t = seg_start
while t + slot_minutes <= seg_end:
slot_end = t + slot_minutes
taken = any(b_date == date and b_start < slot_end and b_end > t for b_date, b_start, b_end in booked)
if not taken:
slots.append(AppointmentSlot(date=date, start=_clock(t), end=_clock(slot_end)))
t += slot_minutes
return slots