1use crate::error::{EventixError, Result};
4use crate::recurrence::{Recurrence, RecurrenceFilter};
5use crate::timezone::{parse_datetime_with_tz, parse_timezone};
6use chrono::{DateTime, Duration, TimeZone};
7use chrono_tz::Tz;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
13pub enum EventStatus {
14 #[default]
16 Confirmed,
17 Tentative,
19 Cancelled,
21 Blocked,
23}
24
25#[derive(Debug, Clone)]
27pub struct Event {
28 pub title: String,
30
31 pub description: Option<String>,
33
34 pub start_time: DateTime<Tz>,
36
37 pub end_time: DateTime<Tz>,
39
40 pub timezone: Tz,
42
43 pub attendees: Vec<String>,
45
46 pub recurrence: Option<Recurrence>,
48
49 pub recurrence_filter: Option<RecurrenceFilter>,
51
52 pub exdates: Vec<DateTime<Tz>>,
54
55 pub location: Option<String>,
57
58 pub uid: Option<String>,
60
61 pub status: EventStatus,
63}
64
65impl Event {
66 pub fn builder() -> EventBuilder {
81 EventBuilder::new()
82 }
83
84 pub fn occurrences_between(
89 &self,
90 start: DateTime<Tz>,
91 end: DateTime<Tz>,
92 max_occurrences: usize,
93 ) -> Result<Vec<DateTime<Tz>>> {
94 if let Some(ref recurrence) = self.recurrence {
95 let mut occurrences =
96 recurrence.generate_occurrences(self.start_time, max_occurrences)?;
97
98 occurrences.retain(|dt| *dt >= start && *dt <= end);
100
101 if let Some(ref filter) = self.recurrence_filter {
103 occurrences = filter.filter_occurrences(occurrences);
104 }
105
106 occurrences.retain(|dt| {
108 !self.exdates.iter().any(|exdate| exdate.date_naive() == dt.date_naive())
109 });
110
111 Ok(occurrences)
112 } else {
113 if self.start_time >= start && self.start_time <= end {
115 Ok(vec![self.start_time])
116 } else {
117 Ok(vec![])
118 }
119 }
120 }
121
122 pub fn occurs_on(&self, date: DateTime<Tz>) -> Result<bool> {
124 let start = date.date_naive().and_hms_opt(0, 0, 0).ok_or_else(|| {
125 EventixError::ValidationError("Invalid start time for date check".to_string())
126 })?;
127 let end = date.date_naive().and_hms_opt(23, 59, 59).ok_or_else(|| {
128 EventixError::ValidationError("Invalid end time for date check".to_string())
129 })?;
130
131 let start_dt = self.timezone.from_local_datetime(&start).earliest().ok_or_else(|| {
132 EventixError::ValidationError("Ambiguous start time for date check".to_string())
133 })?;
134 let end_dt = self.timezone.from_local_datetime(&end).latest().ok_or_else(|| {
135 EventixError::ValidationError("Ambiguous end time for date check".to_string())
136 })?;
137
138 let occurrences = self.occurrences_between(start_dt, end_dt, 1)?;
139 Ok(!occurrences.is_empty())
140 }
141
142 pub fn duration(&self) -> Duration {
144 self.end_time.signed_duration_since(self.start_time)
145 }
146
147 pub fn is_active(&self) -> bool {
152 matches!(
153 self.status,
154 EventStatus::Confirmed | EventStatus::Tentative | EventStatus::Blocked
155 )
156 }
157
158 pub fn confirm(&mut self) {
160 self.status = EventStatus::Confirmed;
161 }
162
163 pub fn cancel(&mut self) {
165 self.status = EventStatus::Cancelled;
166 }
167
168 pub fn tentative(&mut self) {
170 self.status = EventStatus::Tentative;
171 }
172
173 pub fn block(&mut self) {
175 self.status = EventStatus::Blocked;
176 }
177
178 pub fn reschedule(&mut self, new_start: DateTime<Tz>, new_end: DateTime<Tz>) -> Result<()> {
185 if new_end <= new_start {
186 return Err(EventixError::ValidationError(
187 "Event end time must be after start time".to_string(),
188 ));
189 }
190 self.start_time = new_start;
191 self.end_time = new_end;
192 self.timezone = new_start.timezone();
193
194 if self.status == EventStatus::Cancelled {
196 self.status = EventStatus::Confirmed;
197 }
198 Ok(())
199 }
200}
201
202pub struct EventBuilder {
204 title: Option<String>,
205 description: Option<String>,
206 start_time: Option<DateTime<Tz>>,
207 end_time: Option<DateTime<Tz>>,
208 timezone: Option<Tz>,
209 attendees: Vec<String>,
210 recurrence: Option<Recurrence>,
211 recurrence_filter: Option<RecurrenceFilter>,
212 exdates: Vec<DateTime<Tz>>,
213 location: Option<String>,
214 uid: Option<String>,
215 status: EventStatus,
216}
217
218impl EventBuilder {
219 pub fn new() -> Self {
221 Self {
222 title: None,
223 description: None,
224 start_time: None,
225 end_time: None,
226 timezone: None,
227 attendees: Vec::new(),
228 recurrence: None,
229 recurrence_filter: None,
230 exdates: Vec::new(),
231 location: None,
232 uid: None,
233 status: EventStatus::default(),
234 }
235 }
236
237 pub fn title(mut self, title: impl Into<String>) -> Self {
239 self.title = Some(title.into());
240 self
241 }
242
243 pub fn description(mut self, description: impl Into<String>) -> Self {
245 self.description = Some(description.into());
246 self
247 }
248
249 pub fn start(mut self, datetime: &str, timezone: &str) -> Self {
264 if let Ok(tz) = parse_timezone(timezone) {
265 self.timezone = Some(tz);
266 if let Ok(dt) = parse_datetime_with_tz(datetime, tz) {
267 self.start_time = Some(dt);
268 }
269 }
270 self
271 }
272
273 pub fn start_datetime(mut self, datetime: DateTime<Tz>) -> Self {
275 self.timezone = Some(datetime.timezone());
276 self.start_time = Some(datetime);
277 self
278 }
279
280 pub fn end(mut self, datetime: &str) -> Self {
282 if let Some(tz) = self.timezone {
283 if let Ok(dt) = parse_datetime_with_tz(datetime, tz) {
284 self.end_time = Some(dt);
285 }
286 }
287 self
288 }
289
290 pub fn end_datetime(mut self, datetime: DateTime<Tz>) -> Self {
292 self.end_time = Some(datetime);
293 self
294 }
295
296 pub fn duration_hours(mut self, hours: i64) -> Self {
298 if let Some(start) = self.start_time {
299 self.end_time = Some(start + Duration::hours(hours));
300 }
301 self
302 }
303
304 pub fn duration_minutes(mut self, minutes: i64) -> Self {
306 if let Some(start) = self.start_time {
307 self.end_time = Some(start + Duration::minutes(minutes));
308 }
309 self
310 }
311
312 pub fn duration(mut self, duration: Duration) -> Self {
314 if let Some(start) = self.start_time {
315 self.end_time = Some(start + duration);
316 }
317 self
318 }
319
320 pub fn attendee(mut self, attendee: impl Into<String>) -> Self {
322 self.attendees.push(attendee.into());
323 self
324 }
325
326 pub fn attendees(mut self, attendees: Vec<String>) -> Self {
328 self.attendees = attendees;
329 self
330 }
331
332 pub fn recurrence(mut self, recurrence: Recurrence) -> Self {
334 self.recurrence = Some(recurrence);
335 self
336 }
337
338 pub fn skip_weekends(mut self, skip: bool) -> Self {
340 let filter = self.recurrence_filter.unwrap_or_default();
341 self.recurrence_filter = Some(filter.skip_weekends(skip));
342 self
343 }
344
345 pub fn exception_dates(mut self, dates: Vec<DateTime<Tz>>) -> Self {
347 self.exdates = dates;
348 self
349 }
350
351 pub fn exception_date(mut self, date: DateTime<Tz>) -> Self {
353 self.exdates.push(date);
354 self
355 }
356
357 pub fn location(mut self, location: impl Into<String>) -> Self {
359 self.location = Some(location.into());
360 self
361 }
362
363 pub fn uid(mut self, uid: impl Into<String>) -> Self {
365 self.uid = Some(uid.into());
366 self
367 }
368
369 pub fn status(mut self, status: EventStatus) -> Self {
371 self.status = status;
372 self
373 }
374
375 pub fn build(self) -> Result<Event> {
377 let title = self
378 .title
379 .ok_or_else(|| EventixError::ValidationError("Event title is required".to_string()))?;
380
381 let start_time = self.start_time.ok_or_else(|| {
382 EventixError::ValidationError("Event start time is required".to_string())
383 })?;
384
385 let end_time = self.end_time.ok_or_else(|| {
386 EventixError::ValidationError("Event end time is required".to_string())
387 })?;
388
389 let timezone = self.timezone.ok_or_else(|| {
390 EventixError::ValidationError("Event timezone is required".to_string())
391 })?;
392
393 if end_time <= start_time {
394 return Err(EventixError::ValidationError(
395 "Event end time must be after start time".to_string(),
396 ));
397 }
398
399 Ok(Event {
400 title,
401 description: self.description,
402 start_time,
403 end_time,
404 timezone,
405 attendees: self.attendees,
406 recurrence: self.recurrence,
407 recurrence_filter: self.recurrence_filter,
408 exdates: self.exdates,
409 location: self.location,
410 uid: self.uid,
411 status: self.status,
412 })
413 }
414}
415
416impl Default for EventBuilder {
417 fn default() -> Self {
418 Self::new()
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 #![allow(clippy::unwrap_used)]
425 use super::*;
426
427 #[test]
428 fn test_event_builder() {
429 let event = Event::builder()
430 .title("Test Event")
431 .description("A test event")
432 .start("2025-11-01 10:00:00", "UTC")
433 .duration_hours(2)
434 .attendee("alice@example.com")
435 .build()
436 .unwrap();
437
438 assert_eq!(event.title, "Test Event");
439 assert_eq!(event.description, Some("A test event".to_string()));
440 assert_eq!(event.attendees.len(), 1);
441 assert_eq!(event.duration(), Duration::hours(2));
442 }
443
444 #[test]
445 fn test_event_builder_duration() {
446 let event = Event::builder()
447 .title("Test Event")
448 .description("A test event")
449 .start("2025-11-01 10:00:00", "UTC")
450 .duration(Duration::hours(1) + Duration::minutes(10))
451 .attendee("alice@example.com")
452 .build()
453 .unwrap();
454
455 assert_eq!(event.title, "Test Event");
456 assert_eq!(event.description, Some("A test event".to_string()));
457 assert_eq!(event.attendees.len(), 1);
458 assert_eq!(event.end_time.to_rfc3339(), "2025-11-01T11:10:00+00:00");
459 let duration_in_secs = (60.0 * 60.0) + (10.0 * 60.0); assert_eq!(event.duration().as_seconds_f32(), duration_in_secs);
461 }
462
463 #[test]
464 fn test_event_validation() {
465 let result = Event::builder().start("2025-11-01 10:00:00", "UTC").duration_hours(1).build();
467 assert!(result.is_err());
468
469 let result = Event::builder()
471 .title("Test")
472 .start("2025-11-01 10:00:00", "UTC")
473 .end("2025-11-01 09:00:00")
474 .build();
475 assert!(result.is_err());
476 }
477}