1use crate::expander::ExpandedEvent;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct FreeSlot {
13 pub start: DateTime<Utc>,
14 pub end: DateTime<Utc>,
15 pub duration_minutes: i64,
16}
17
18pub(crate) fn merge_busy_periods(
22 events: &[ExpandedEvent],
23 window_start: DateTime<Utc>,
24 window_end: DateTime<Utc>,
25) -> Vec<(DateTime<Utc>, DateTime<Utc>)> {
26 let mut intervals: Vec<(DateTime<Utc>, DateTime<Utc>)> = events
28 .iter()
29 .filter(|e| e.start < window_end && e.end > window_start)
30 .map(|e| (e.start.max(window_start), e.end.min(window_end)))
31 .collect();
32
33 if intervals.is_empty() {
34 return Vec::new();
35 }
36
37 intervals.sort_by_key(|&(start, end)| (start, end));
39
40 let mut merged: Vec<(DateTime<Utc>, DateTime<Utc>)> = Vec::new();
42 for (start, end) in intervals {
43 if let Some(last) = merged.last_mut() {
44 if start <= last.1 {
45 last.1 = last.1.max(end);
47 continue;
48 }
49 }
50 merged.push((start, end));
51 }
52
53 merged
54}
55
56pub fn find_free_slots(
61 events: &[ExpandedEvent],
62 window_start: DateTime<Utc>,
63 window_end: DateTime<Utc>,
64) -> Vec<FreeSlot> {
65 let merged = merge_busy_periods(events, window_start, window_end);
66
67 let mut free_slots = Vec::new();
68 let mut cursor = window_start;
69
70 for (busy_start, busy_end) in &merged {
71 if cursor < *busy_start {
72 let duration_minutes = (*busy_start - cursor).num_minutes();
73 free_slots.push(FreeSlot {
74 start: cursor,
75 end: *busy_start,
76 duration_minutes,
77 });
78 }
79 cursor = cursor.max(*busy_end);
80 }
81
82 if cursor < window_end {
84 let duration_minutes = (window_end - cursor).num_minutes();
85 free_slots.push(FreeSlot {
86 start: cursor,
87 end: window_end,
88 duration_minutes,
89 });
90 }
91
92 free_slots
93}
94
95pub fn find_first_free_slot(
100 events: &[ExpandedEvent],
101 window_start: DateTime<Utc>,
102 window_end: DateTime<Utc>,
103 min_duration_minutes: i64,
104) -> Option<FreeSlot> {
105 find_free_slots(events, window_start, window_end)
106 .into_iter()
107 .find(|slot| slot.duration_minutes >= min_duration_minutes)
108}