aimcal_core/
event.rs

1// SPDX-FileCopyrightText: 2025 Zexin Yuan <aim@yzx9.xyz>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{fmt::Display, num::NonZeroU32, str::FromStr};
6
7use chrono::{DateTime, Duration, Local, Timelike};
8use icalendar::{Component, EventLike};
9
10use crate::{DateTimeAnchor, LooseDateTime};
11
12/// Trait representing a calendar event.
13pub trait Event {
14    /// The short identifier for the event.
15    /// It will be `None` if the event does not have a short ID.
16    /// It is used for display purposes and may not be unique.
17    fn short_id(&self) -> Option<NonZeroU32> {
18        None
19    }
20
21    /// The unique identifier for the event.
22    fn uid(&self) -> &str;
23
24    /// The description of the event, if available.
25    fn description(&self) -> Option<&str>;
26
27    /// The location of the event, if available.
28    fn start(&self) -> Option<LooseDateTime>;
29
30    /// The start date and time of the event, if available.
31    fn end(&self) -> Option<LooseDateTime>;
32
33    /// The status of the event, if available.
34    fn status(&self) -> Option<EventStatus>;
35
36    /// The summary of the event.
37    fn summary(&self) -> &str;
38}
39
40impl Event for icalendar::Event {
41    fn uid(&self) -> &str {
42        self.get_uid().unwrap_or_default()
43    }
44
45    fn description(&self) -> Option<&str> {
46        self.get_description()
47    }
48
49    fn start(&self) -> Option<LooseDateTime> {
50        self.get_start().map(Into::into)
51    }
52
53    fn end(&self) -> Option<LooseDateTime> {
54        self.get_end().map(Into::into)
55    }
56
57    fn status(&self) -> Option<EventStatus> {
58        self.get_status().map(EventStatus::from)
59    }
60
61    fn summary(&self) -> &str {
62        self.get_summary().unwrap_or_default()
63    }
64}
65
66/// Darft for an event, used for creating new events.
67#[derive(Debug)]
68pub struct EventDraft {
69    /// The description of the event, if available.
70    pub description: Option<String>,
71
72    /// The start date and time of the event, if available.
73    pub start: Option<LooseDateTime>,
74
75    /// The end date and time of the event, if available.
76    pub end: Option<LooseDateTime>,
77
78    /// The status of the event.
79    pub status: EventStatus,
80
81    /// The summary of the event.
82    pub summary: String,
83}
84
85impl EventDraft {
86    /// Creates a new empty patch.
87    pub(crate) fn default(now: DateTime<Local>) -> Self {
88        // next 00 or 30 minute
89        let start = if now.minute() < 30 {
90            now.with_minute(30).unwrap().with_second(0).unwrap()
91        } else {
92            (now + Duration::hours(1))
93                .with_minute(0)
94                .unwrap()
95                .with_second(0)
96                .unwrap()
97        };
98
99        Self {
100            description: None,
101            start: Some(start.into()),
102            end: Some((start + Duration::hours(1)).into()),
103            status: EventStatus::default(),
104            summary: String::new(),
105        }
106    }
107
108    /// Converts the draft into a icalendar Event component.
109    pub(crate) fn into_ics(self, now: &DateTime<Local>, uid: &str) -> icalendar::Event {
110        let mut event = icalendar::Event::with_uid(uid);
111
112        if let Some(description) = self.description {
113            Component::description(&mut event, &description);
114        }
115
116        let default_duration = Duration::hours(1);
117        let (start, end) = match (self.start, self.end) {
118            (Some(start), Some(end)) => (start, end),
119            (None, Some(end)) => {
120                // If start is not specified, but end is, set start to end - duration
121                let start = match end {
122                    LooseDateTime::DateOnly(d) => d.into(),
123                    LooseDateTime::Floating(dt) => (dt - default_duration).into(),
124                    LooseDateTime::Local(dt) => (dt - default_duration).into(),
125                };
126                (start, end)
127            }
128            (Some(start), None) => {
129                // If end is not specified, but start is, set it to start + duration
130                let end = match start {
131                    LooseDateTime::DateOnly(d) => d.into(),
132                    LooseDateTime::Floating(dt) => (dt + default_duration).into(),
133                    LooseDateTime::Local(dt) => (dt + default_duration).into(),
134                };
135                (start, end)
136            }
137            (None, None) => {
138                let start = *now;
139                let end = (start + default_duration).into();
140                (start.into(), end)
141            }
142        };
143        EventLike::starts(&mut event, start);
144        EventLike::ends(&mut event, end);
145
146        icalendar::Event::status(&mut event, self.status.into());
147
148        Component::summary(&mut event, &self.summary);
149
150        event
151    }
152}
153
154/// Patch for an event, allowing partial updates.
155#[derive(Debug, Default, Clone)]
156pub struct EventPatch {
157    /// The description of the event, if available.
158    pub description: Option<Option<String>>,
159
160    /// The start date and time of the event, if available.
161    pub start: Option<Option<LooseDateTime>>,
162
163    /// The end date and time of the event, if available.
164    pub end: Option<Option<LooseDateTime>>,
165
166    /// The status of the event, if available.
167    pub status: Option<EventStatus>,
168
169    /// The summary of the event, if available.
170    pub summary: Option<String>,
171}
172
173impl EventPatch {
174    /// Is this patch empty, meaning no fields are set
175    pub fn is_empty(&self) -> bool {
176        self.description.is_none()
177            && self.start.is_none()
178            && self.end.is_none()
179            && self.status.is_none()
180            && self.summary.is_none()
181    }
182
183    /// Applies the patch to a mutable event, modifying it in place.
184    pub(crate) fn apply_to<'a>(&self, e: &'a mut icalendar::Event) -> &'a mut icalendar::Event {
185        if let Some(description) = &self.description {
186            match description {
187                Some(desc) => e.description(desc),
188                None => e.remove_description(),
189            };
190        }
191
192        if let Some(start) = &self.start {
193            match start {
194                Some(s) => e.starts(*s),
195                None => e.remove_starts(),
196            };
197        }
198
199        if let Some(end) = &self.end {
200            match end {
201                Some(ed) => e.ends(*ed),
202                None => e.remove_ends(),
203            };
204        }
205
206        if let Some(status) = self.status {
207            e.status(status.into());
208        }
209
210        if let Some(summary) = &self.summary {
211            e.summary(summary);
212        }
213
214        e
215    }
216}
217
218/// The status of an event, which can be tentative, confirmed, or cancelled.
219#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
220#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
221pub enum EventStatus {
222    /// The event is tentative.
223    Tentative,
224
225    /// The event is confirmed.
226    #[default]
227    Confirmed,
228
229    /// The event is cancelled.
230    Cancelled,
231}
232
233const STATUS_TENTATIVE: &str = "TENTATIVE";
234const STATUS_CONFIRMED: &str = "CONFIRMED";
235const STATUS_CANCELLED: &str = "CANCELLED";
236
237impl AsRef<str> for EventStatus {
238    fn as_ref(&self) -> &str {
239        match self {
240            EventStatus::Tentative => STATUS_TENTATIVE,
241            EventStatus::Confirmed => STATUS_CONFIRMED,
242            EventStatus::Cancelled => STATUS_CANCELLED,
243        }
244    }
245}
246
247impl Display for EventStatus {
248    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249        write!(f, "{}", self.as_ref())
250    }
251}
252
253impl FromStr for EventStatus {
254    type Err = ();
255
256    fn from_str(value: &str) -> Result<Self, Self::Err> {
257        match value {
258            STATUS_TENTATIVE => Ok(EventStatus::Tentative),
259            STATUS_CONFIRMED => Ok(EventStatus::Confirmed),
260            STATUS_CANCELLED => Ok(EventStatus::Cancelled),
261            _ => Err(()),
262        }
263    }
264}
265
266impl From<EventStatus> for icalendar::EventStatus {
267    fn from(status: EventStatus) -> Self {
268        match status {
269            EventStatus::Tentative => icalendar::EventStatus::Tentative,
270            EventStatus::Confirmed => icalendar::EventStatus::Confirmed,
271            EventStatus::Cancelled => icalendar::EventStatus::Cancelled,
272        }
273    }
274}
275
276impl From<icalendar::EventStatus> for EventStatus {
277    fn from(status: icalendar::EventStatus) -> Self {
278        match status {
279            icalendar::EventStatus::Tentative => EventStatus::Tentative,
280            icalendar::EventStatus::Confirmed => EventStatus::Confirmed,
281            icalendar::EventStatus::Cancelled => EventStatus::Cancelled,
282        }
283    }
284}
285
286/// Conditions for filtering events in a calendar.
287#[derive(Debug, Default, Clone, Copy)]
288pub struct EventConditions {
289    /// Whether to include only startable events.
290    pub startable: Option<DateTimeAnchor>,
291
292    /// The cutoff date and time, events ending after this will be excluded.
293    pub cutoff: Option<DateTimeAnchor>,
294}
295
296#[derive(Debug)]
297pub(crate) struct ParsedEventConditions {
298    /// The date and time after which the event must start
299    pub start_before: Option<DateTime<Local>>,
300
301    /// The date and time after which the event must end
302    pub end_after: Option<DateTime<Local>>,
303}
304
305impl ParsedEventConditions {
306    pub fn parse(now: &DateTime<Local>, conds: &EventConditions) -> Self {
307        Self {
308            start_before: conds.cutoff.map(|w| w.resolve_at_end_of_day(now)),
309            end_after: conds.startable.map(|w| w.resolve_at_start_of_day(now)),
310        }
311    }
312}