Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::time_minutes_between::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.
const ANY_DATE: &str = "2000-01-01";
struct Interval {
date: String,
start: i64,
end: i64,
}
/// Minute of the day, 0 to 1439; panics on a malformed date or time.
fn minute_of_day(date: &str, time: &str) -> i64 {
minutes_between(date, "00:00", date, time)
}
fn interval(kind: &str, date: Option<&str>, start: &str, end: &str) -> Interval {
let on = date.unwrap_or(ANY_DATE);
let from = minute_of_day(on, start);
let to = minute_of_day(on, end);
if to <= from {
panic!("{} on {} must end after it starts ({} to {})", kind, date.unwrap_or("every day"), start, end);
}
Interval { date: on.to_string(), start: from, end: to }
}
fn clock(minute: i64) -> String {
format!("{:02}:{:02}", minute / 60, minute % 60)
}
/// 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.
///
/// # Panics
/// Panics on a bad date or time, an interval that does not end after it
/// starts, overlapping sessions, or slot_minutes outside 1..=480.
pub fn appointment_slots(
sessions: &[ClinicSession],
slot_minutes: i64,
breaks: &[ClinicBreak],
bookings: &[Booking],
) -> Vec<AppointmentSlot> {
if !(1..=480).contains(&slot_minutes) {
panic!("slotMinutes must be a whole number from 1 to 480, received {}", slot_minutes);
}
let mut clinic: Vec<Interval> =
sessions.iter().map(|s| interval("session", Some(&s.date), &s.start, &s.end)).collect();
let closed: Vec<(bool, Interval)> = breaks
.iter()
.map(|b| (b.date.is_none(), interval("break", b.date.as_deref(), &b.start, &b.end)))
.collect();
let booked: Vec<Interval> =
bookings.iter().map(|b| interval("booking", Some(&b.date), &b.start, &b.end)).collect();
clinic.sort_by(|a, b| a.date.cmp(&b.date).then(a.start.cmp(&b.start)));
for pair in clinic.windows(2) {
if pair[1].date == pair[0].date && pair[1].start < pair[0].end {
panic!("sessions overlap on {}", pair[1].date);
}
}
let mut slots = Vec::new();
for session in &clinic {
let mut pauses: Vec<(i64, i64)> = closed
.iter()
.filter(|(always, b)| *always || b.date == session.date)
.map(|(_, b)| (b.start, b.end))
.collect();
pauses.sort();
let mut segments: Vec<(i64, i64)> = Vec::new();
let mut cursor = session.start;
for (p_start, p_end) in pauses {
if p_end <= cursor || p_start >= session.end {
continue;
}
if p_start > cursor {
segments.push((cursor, p_start));
}
cursor = cursor.max(p_end);
}
if cursor < session.end {
segments.push((cursor, session.end));
}
for (from, to) in segments {
let mut t = from;
while t + slot_minutes <= to {
let end = t + slot_minutes;
let taken = booked.iter().any(|b| b.date == session.date && b.start < end && b.end > t);
if !taken {
slots.push(AppointmentSlot { date: session.date.clone(), start: clock(t), end: clock(end) });
}
t += slot_minutes;
}
}
}
slots
}
pub fn appointment_slot_to_value(slot: &AppointmentSlot) -> Value {
Value::obj(vec![
("date", Value::str(&slot.date)),
("start", Value::str(&slot.start)),
("end", Value::str(&slot.end)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
// Refuse what an i64 cannot hold with the wording TypeScript and Python use.
let slot_minutes = match &args[1] {
Value::Int(v) => *v,
other => panic!("slotMinutes must be a whole number from 1 to 480, received {:?}", other),
};
let text = |v: &Value, key: &str| v.get(key).as_str().to_string();
let sessions: Vec<ClinicSession> = args[0]
.as_arr()
.iter()
.map(|v| ClinicSession { date: text(v, "date"), start: text(v, "start"), end: text(v, "end") })
.collect();
let breaks: Vec<ClinicBreak> = args[2]
.as_arr()
.iter()
.map(|v| ClinicBreak {
date: if v.get("date").is_null() { None } else { Some(text(v, "date")) },
start: text(v, "start"),
end: text(v, "end"),
})
.collect();
let bookings: Vec<Booking> = args[3]
.as_arr()
.iter()
.map(|v| Booking { date: text(v, "date"), start: text(v, "start"), end: text(v, "end") })
.collect();
Value::Arr(appointment_slots(&sessions, slot_minutes, &breaks, &bookings).iter().map(appointment_slot_to_value).collect())
}