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};
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() -> Self {
88        Self {
89            description: None,
90            start: None,
91            end: None,
92            status: EventStatus::default(),
93            summary: String::new(),
94        }
95    }
96
97    /// Converts the draft into a icalendar Event component.
98    pub(crate) fn into_ics(self, now: &DateTime<Local>, uid: &str) -> icalendar::Event {
99        let mut event = icalendar::Event::with_uid(uid);
100
101        if let Some(description) = self.description {
102            Component::description(&mut event, &description);
103        }
104
105        let default_duration = Duration::hours(1);
106        let (start, end) = match (self.start, self.end) {
107            (Some(start), Some(end)) => (start, end),
108            (None, Some(end)) => {
109                // If start is not specified, but end is, set start to end - duration
110                let start = match end {
111                    LooseDateTime::DateOnly(d) => d.into(),
112                    LooseDateTime::Floating(dt) => (dt - default_duration).into(),
113                    LooseDateTime::Local(dt) => (dt - default_duration).into(),
114                };
115                (start, end)
116            }
117            (Some(start), None) => {
118                // If end is not specified, but start is, set it to start + duration
119                let end = match start {
120                    LooseDateTime::DateOnly(d) => d.into(),
121                    LooseDateTime::Floating(dt) => (dt + default_duration).into(),
122                    LooseDateTime::Local(dt) => (dt + default_duration).into(),
123                };
124                (start, end)
125            }
126            (None, None) => {
127                let start = *now;
128                let end = (start + default_duration).into();
129                (start.into(), end)
130            }
131        };
132        EventLike::starts(&mut event, start);
133        EventLike::ends(&mut event, end);
134
135        icalendar::Event::status(&mut event, self.status.into());
136
137        Component::summary(&mut event, &self.summary);
138
139        event
140    }
141}
142
143/// Patch for an event, allowing partial updates.
144#[derive(Debug, Default, Clone)]
145pub struct EventPatch {
146    /// The description of the event, if available.
147    pub description: Option<Option<String>>,
148
149    /// The start date and time of the event, if available.
150    pub start: Option<Option<LooseDateTime>>,
151
152    /// The end date and time of the event, if available.
153    pub end: Option<Option<LooseDateTime>>,
154
155    /// The status of the event, if available.
156    pub status: Option<EventStatus>,
157
158    /// The summary of the event, if available.
159    pub summary: Option<String>,
160}
161
162impl EventPatch {
163    /// Is this patch empty, meaning no fields are set
164    pub fn is_empty(&self) -> bool {
165        self.description.is_none()
166            && self.start.is_none()
167            && self.end.is_none()
168            && self.status.is_none()
169            && self.summary.is_none()
170    }
171
172    /// Applies the patch to a mutable event, modifying it in place.
173    pub(crate) fn apply_to<'a>(&self, e: &'a mut icalendar::Event) -> &'a mut icalendar::Event {
174        if let Some(description) = &self.description {
175            match description {
176                Some(desc) => e.description(desc),
177                None => e.remove_description(),
178            };
179        }
180
181        if let Some(start) = &self.start {
182            match start {
183                Some(s) => e.starts(*s),
184                None => e.remove_starts(),
185            };
186        }
187
188        if let Some(end) = &self.end {
189            match end {
190                Some(ed) => e.ends(*ed),
191                None => e.remove_ends(),
192            };
193        }
194
195        if let Some(status) = self.status {
196            e.status(status.into());
197        }
198
199        if let Some(summary) = &self.summary {
200            e.summary(summary);
201        }
202
203        e
204    }
205}
206
207/// The status of an event, which can be tentative, confirmed, or cancelled.
208#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
209#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
210pub enum EventStatus {
211    /// The event is tentative.
212    Tentative,
213
214    /// The event is confirmed.
215    #[default]
216    Confirmed,
217
218    /// The event is cancelled.
219    Cancelled,
220}
221
222const STATUS_TENTATIVE: &str = "TENTATIVE";
223const STATUS_CONFIRMED: &str = "CONFIRMED";
224const STATUS_CANCELLED: &str = "CANCELLED";
225
226impl AsRef<str> for EventStatus {
227    fn as_ref(&self) -> &str {
228        match self {
229            EventStatus::Tentative => STATUS_TENTATIVE,
230            EventStatus::Confirmed => STATUS_CONFIRMED,
231            EventStatus::Cancelled => STATUS_CANCELLED,
232        }
233    }
234}
235
236impl Display for EventStatus {
237    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238        write!(f, "{}", self.as_ref())
239    }
240}
241
242impl FromStr for EventStatus {
243    type Err = ();
244
245    fn from_str(value: &str) -> Result<Self, Self::Err> {
246        match value {
247            STATUS_TENTATIVE => Ok(EventStatus::Tentative),
248            STATUS_CONFIRMED => Ok(EventStatus::Confirmed),
249            STATUS_CANCELLED => Ok(EventStatus::Cancelled),
250            _ => Err(()),
251        }
252    }
253}
254
255impl From<EventStatus> for icalendar::EventStatus {
256    fn from(status: EventStatus) -> Self {
257        match status {
258            EventStatus::Tentative => icalendar::EventStatus::Tentative,
259            EventStatus::Confirmed => icalendar::EventStatus::Confirmed,
260            EventStatus::Cancelled => icalendar::EventStatus::Cancelled,
261        }
262    }
263}
264
265impl From<icalendar::EventStatus> for EventStatus {
266    fn from(status: icalendar::EventStatus) -> Self {
267        match status {
268            icalendar::EventStatus::Tentative => EventStatus::Tentative,
269            icalendar::EventStatus::Confirmed => EventStatus::Confirmed,
270            icalendar::EventStatus::Cancelled => EventStatus::Cancelled,
271        }
272    }
273}
274
275/// Conditions for filtering events in a calendar.
276#[derive(Debug, Default, Clone, Copy)]
277pub struct EventConditions {
278    /// Whether to include only startable events.
279    pub startable: Option<DateTimeAnchor>,
280
281    /// The cutoff date and time, events ending after this will be excluded.
282    pub cutoff: Option<DateTimeAnchor>,
283}
284
285#[derive(Debug)]
286pub(crate) struct ParsedEventConditions {
287    /// The date and time after which the event must start
288    pub start_before: Option<DateTime<Local>>,
289
290    /// The date and time after which the event must end
291    pub end_after: Option<DateTime<Local>>,
292}
293
294impl ParsedEventConditions {
295    pub fn parse(now: &DateTime<Local>, conds: &EventConditions) -> Self {
296        Self {
297            start_before: conds.cutoff.map(|w| w.parse_as_end_of_day(now)),
298            end_after: conds.startable.map(|w| w.parse_as_start_of_day(now)),
299        }
300    }
301}