use crate::expander::ExpandedEvent;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FreeSlot {
pub start: DateTime<Utc>,
pub end: DateTime<Utc>,
pub duration_minutes: i64,
}
pub(crate) fn merge_busy_periods(
events: &[ExpandedEvent],
window_start: DateTime<Utc>,
window_end: DateTime<Utc>,
) -> Vec<(DateTime<Utc>, DateTime<Utc>)> {
let mut intervals: Vec<(DateTime<Utc>, DateTime<Utc>)> = events
.iter()
.filter(|e| e.start < window_end && e.end > window_start)
.map(|e| (e.start.max(window_start), e.end.min(window_end)))
.collect();
if intervals.is_empty() {
return Vec::new();
}
intervals.sort_by_key(|&(start, end)| (start, end));
let mut merged: Vec<(DateTime<Utc>, DateTime<Utc>)> = Vec::new();
for (start, end) in intervals {
if let Some(last) = merged.last_mut() {
if start <= last.1 {
last.1 = last.1.max(end);
continue;
}
}
merged.push((start, end));
}
merged
}
pub fn find_free_slots(
events: &[ExpandedEvent],
window_start: DateTime<Utc>,
window_end: DateTime<Utc>,
) -> Vec<FreeSlot> {
let merged = merge_busy_periods(events, window_start, window_end);
let mut free_slots = Vec::new();
let mut cursor = window_start;
for (busy_start, busy_end) in &merged {
if cursor < *busy_start {
let duration_minutes = (*busy_start - cursor).num_minutes();
free_slots.push(FreeSlot {
start: cursor,
end: *busy_start,
duration_minutes,
});
}
cursor = cursor.max(*busy_end);
}
if cursor < window_end {
let duration_minutes = (window_end - cursor).num_minutes();
free_slots.push(FreeSlot {
start: cursor,
end: window_end,
duration_minutes,
});
}
free_slots
}
pub fn find_first_free_slot(
events: &[ExpandedEvent],
window_start: DateTime<Utc>,
window_end: DateTime<Utc>,
min_duration_minutes: i64,
) -> Option<FreeSlot> {
find_free_slots(events, window_start, window_end)
.into_iter()
.find(|slot| slot.duration_minutes >= min_duration_minutes)
}