u_schedule/models/
calendar.rs1use serde::{Deserialize, Serialize};
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21pub struct TimeWindow {
22 pub start_ms: i64,
24 pub end_ms: i64,
26}
27
28impl TimeWindow {
29 pub fn new(start_ms: i64, end_ms: i64) -> Self {
31 Self { start_ms, end_ms }
32 }
33
34 #[inline]
36 pub fn duration_ms(&self) -> i64 {
37 self.end_ms - self.start_ms
38 }
39
40 #[inline]
42 pub fn contains(&self, time_ms: i64) -> bool {
43 time_ms >= self.start_ms && time_ms < self.end_ms
44 }
45
46 pub fn overlaps(&self, other: &Self) -> bool {
48 self.start_ms < other.end_ms && other.start_ms < self.end_ms
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct Calendar {
59 pub id: String,
61 pub time_windows: Vec<TimeWindow>,
64 pub blocked_periods: Vec<TimeWindow>,
66}
67
68impl Calendar {
69 pub fn new(id: impl Into<String>) -> Self {
71 Self {
72 id: id.into(),
73 time_windows: Vec::new(),
74 blocked_periods: Vec::new(),
75 }
76 }
77
78 pub fn always_available(id: impl Into<String>) -> Self {
80 Self::new(id)
81 }
82
83 pub fn with_window(mut self, start_ms: i64, end_ms: i64) -> Self {
85 self.time_windows.push(TimeWindow::new(start_ms, end_ms));
86 self
87 }
88
89 pub fn with_blocked(mut self, start_ms: i64, end_ms: i64) -> Self {
91 self.blocked_periods.push(TimeWindow::new(start_ms, end_ms));
92 self
93 }
94
95 pub fn is_working_time(&self, time_ms: i64) -> bool {
100 if self.blocked_periods.iter().any(|w| w.contains(time_ms)) {
102 return false;
103 }
104
105 if self.time_windows.is_empty() {
107 return true;
108 }
109
110 self.time_windows.iter().any(|w| w.contains(time_ms))
112 }
113
114 pub fn next_available_time(&self, from_ms: i64) -> Option<i64> {
121 if self.is_working_time(from_ms) {
122 return Some(from_ms);
123 }
124
125 if self.time_windows.is_empty() {
127 for bp in &self.blocked_periods {
129 if bp.contains(from_ms) {
130 let candidate = bp.end_ms;
131 if self.is_working_time(candidate) {
132 return Some(candidate);
133 }
134 }
135 }
136 return None;
137 }
138
139 let mut candidates: Vec<i64> = self
141 .time_windows
142 .iter()
143 .filter(|w| w.end_ms > from_ms)
144 .map(|w| w.start_ms.max(from_ms))
145 .collect();
146 candidates.sort();
147
148 for candidate in candidates {
149 if self.is_working_time(candidate) {
150 return Some(candidate);
151 }
152 for bp in &self.blocked_periods {
154 if bp.contains(candidate) && bp.end_ms < i64::MAX && self.is_working_time(bp.end_ms)
155 {
156 return Some(bp.end_ms);
157 }
158 }
159 }
160
161 None
162 }
163
164 pub fn available_time_in_range(&self, start_ms: i64, end_ms: i64) -> i64 {
166 if end_ms <= start_ms {
167 return 0;
168 }
169
170 let range = TimeWindow::new(start_ms, end_ms);
171
172 if self.time_windows.is_empty() {
174 let blocked: i64 = self
175 .blocked_periods
176 .iter()
177 .filter_map(|bp| overlap_duration(&range, bp))
178 .sum();
179 return range.duration_ms() - blocked;
180 }
181
182 let mut available: i64 = 0;
184 for w in &self.time_windows {
185 if let Some(dur) = overlap_duration(&range, w) {
186 available += dur;
187 }
188 }
189
190 let blocked: i64 = self
192 .blocked_periods
193 .iter()
194 .filter_map(|bp| overlap_duration(&range, bp))
195 .sum();
196
197 (available - blocked).max(0)
198 }
199}
200
201fn overlap_duration(a: &TimeWindow, b: &TimeWindow) -> Option<i64> {
203 let start = a.start_ms.max(b.start_ms);
204 let end = a.end_ms.min(b.end_ms);
205 if end > start {
206 Some(end - start)
207 } else {
208 None
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn test_time_window() {
218 let w = TimeWindow::new(100, 200);
219 assert_eq!(w.duration_ms(), 100);
220 assert!(w.contains(100));
221 assert!(w.contains(199));
222 assert!(!w.contains(200)); assert!(!w.contains(50));
224 }
225
226 #[test]
227 fn test_time_window_overlap() {
228 let a = TimeWindow::new(0, 100);
229 let b = TimeWindow::new(50, 150);
230 assert!(a.overlaps(&b));
231 assert!(b.overlaps(&a));
232
233 let c = TimeWindow::new(100, 200); assert!(!a.overlaps(&c));
235 }
236
237 #[test]
238 fn test_calendar_always_available() {
239 let cal = Calendar::always_available("cal1");
240 assert!(cal.is_working_time(0));
241 assert!(cal.is_working_time(1_000_000));
242 }
243
244 #[test]
245 fn test_calendar_with_windows() {
246 let cal = Calendar::new("shifts")
247 .with_window(0, 8_000) .with_window(16_000, 24_000); assert!(cal.is_working_time(4_000)); assert!(!cal.is_working_time(10_000)); assert!(cal.is_working_time(20_000)); }
254
255 #[test]
256 fn test_calendar_blocked_overrides() {
257 let cal = Calendar::new("cal")
258 .with_window(0, 100_000)
259 .with_blocked(50_000, 60_000); assert!(cal.is_working_time(40_000)); assert!(!cal.is_working_time(55_000)); assert!(cal.is_working_time(70_000)); }
265
266 #[test]
267 fn test_next_available_time() {
268 let cal = Calendar::new("shifts")
269 .with_window(0, 8_000)
270 .with_window(16_000, 24_000);
271
272 assert_eq!(cal.next_available_time(4_000), Some(4_000)); assert_eq!(cal.next_available_time(10_000), Some(16_000)); }
275
276 #[test]
277 fn test_next_available_blocked() {
278 let cal = Calendar::always_available("cal").with_blocked(50_000, 60_000);
279
280 assert_eq!(cal.next_available_time(40_000), Some(40_000));
281 assert_eq!(cal.next_available_time(55_000), Some(60_000));
282 }
283
284 #[test]
285 fn test_available_time_in_range() {
286 let cal = Calendar::new("cal")
287 .with_window(0, 100_000)
288 .with_blocked(40_000, 60_000); let avail = cal.available_time_in_range(0, 100_000);
291 assert_eq!(avail, 80_000); let avail2 = cal.available_time_in_range(50_000, 70_000);
294 assert_eq!(avail2, 10_000); }
296
297 #[test]
298 fn test_available_time_no_windows() {
299 let cal = Calendar::always_available("cal").with_blocked(20_000, 30_000);
300
301 let avail = cal.available_time_in_range(0, 50_000);
302 assert_eq!(avail, 40_000); }
304}