• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lennart-k / ical-rs / #73

12 Jan 2026 01:50PM UTC coverage: 77.771% (-0.03%) from 77.802%
#73

Pull #2

lennart-k
IcalCalendarObjectBuilder: Make attributes public
Pull Request #2: Major overhaul

908 of 1184 new or added lines in 30 files covered. (76.69%)

32 existing lines in 10 files now uncovered.

1221 of 1570 relevant lines covered (77.77%)

2.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.07
/src/parser/ical/component/event/mod.rs
1
use std::collections::HashSet;
2

3
use crate::{
4
    component::IcalAlarm,
5
    parser::{
6
        Component, ICalProperty, IcalDTENDProperty, IcalDTSTAMPProperty, IcalDTSTARTProperty,
7
        IcalDURATIONProperty, IcalEXDATEProperty, IcalRDATEProperty, IcalRECURIDProperty,
8
        IcalSUMMARYProperty, RecurIdRange,
9
    },
10
    property::ContentLine,
11
    types::{CalDate, CalDateOrDateTime, CalDateTime, Timezone},
12
};
13
use chrono::{DateTime, Duration, Utc};
14
use itertools::Itertools;
15

16
pub use builder::IcalEventBuilder;
17
use rrule::{RRule, RRuleSet};
18
mod builder;
19

20
#[derive(Debug, Clone)]
21
pub struct IcalEvent {
22
    uid: String,
23
    dtstamp: IcalDTSTAMPProperty,
24
    pub dtstart: IcalDTSTARTProperty,
25
    pub dtend: Option<IcalDTENDProperty>,
26
    duration: Option<IcalDURATIONProperty>,
27
    rdates: Vec<IcalRDATEProperty>,
28
    rrules: Vec<RRule>,
29
    exdates: Vec<IcalEXDATEProperty>,
30
    exrules: Vec<RRule>,
31
    pub(crate) recurid: Option<IcalRECURIDProperty>,
32
    summary: Option<IcalSUMMARYProperty>,
33
    pub(crate) properties: Vec<ContentLine>,
34
    pub(crate) alarms: Vec<IcalAlarm>,
35
}
36

37
impl IcalEvent {
38
    pub fn get_uid(&self) -> &str {
1✔
39
        &self.uid
1✔
40
    }
41
}
42

43
impl Component for IcalEvent {
44
    const NAMES: &[&str] = &["VEVENT"];
45
    type Unverified = IcalEventBuilder;
46

47
    fn get_properties(&self) -> &Vec<ContentLine> {
1✔
48
        &self.properties
1✔
49
    }
50

NEW
51
    fn mutable(self) -> Self::Unverified {
×
52
        IcalEventBuilder {
NEW
53
            properties: self.properties,
×
NEW
54
            alarms: self.alarms.into_iter().map(Component::mutable).collect(),
×
55
        }
56
    }
57
}
58

59
impl IcalEvent {
60
    pub fn get_tzids(&self) -> HashSet<&str> {
2✔
61
        self.properties
2✔
62
            .iter()
63
            .filter_map(|prop| prop.params.get_tzid())
6✔
64
            .unique()
65
            .collect()
66
    }
67

68
    pub fn to_utc_or_local(self) -> Self {
2✔
69
        // Very naive way to replace known properties with UTC props
70
        let dtstart = self.dtstart.utc_or_local();
3✔
71
        let dtstamp = self.dtstamp.utc_or_local();
4✔
72
        let exdates = self
2✔
73
            .exdates
74
            .into_iter()
75
            .map(|dt| dt.utc_or_local())
2✔
76
            .collect();
77
        let rdates = self
2✔
78
            .rdates
79
            .into_iter()
80
            .map(|dt| dt.utc_or_local())
2✔
81
            .collect();
82
        let dtend = self.dtend.map(|dt| dt.utc_or_local());
5✔
83

84
        let mut ev = Self {
85
            uid: self.uid,
1✔
86
            dtstamp: dtstamp.clone(),
1✔
87
            dtstart: dtstart.clone(),
1✔
88
            dtend: dtend.clone(),
1✔
89
            duration: self.duration,
1✔
90
            rrules: self.rrules,
2✔
91
            rdates,
92
            exrules: self.exrules,
2✔
93
            exdates,
94
            summary: self.summary,
2✔
95
            recurid: self.recurid,
2✔
96
            properties: self.properties,
2✔
97
            alarms: self.alarms,
2✔
98
        };
99
        ev.replace_or_push_property(dtstart);
2✔
100
        ev.replace_or_push_property(dtstamp);
2✔
101
        if let Some(dtend) = dtend {
2✔
102
            ev.replace_or_push_property(dtend);
2✔
103
        }
104
        ev
1✔
105
    }
106

107
    pub fn get_duration(&self) -> Option<Duration> {
1✔
108
        if let Some(IcalDTENDProperty(dtend, _)) = self.dtend.as_ref() {
1✔
109
            return Some(dtend.clone() - &self.dtstart.0);
1✔
110
        };
111
        self.duration
1✔
112
            .as_ref()
113
            .map(|IcalDURATIONProperty(duration, _)| duration.to_owned())
1✔
114
    }
115

116
    pub fn has_rruleset(&self) -> bool {
1✔
117
        !self.rrules.is_empty()
2✔
118
            || !self.rdates.is_empty()
2✔
119
            || !self.exrules.is_empty()
2✔
120
            || !self.exdates.is_empty()
1✔
121
    }
122

123
    pub fn get_rruleset(&self, dtstart: DateTime<rrule::Tz>) -> Option<RRuleSet> {
1✔
124
        if !self.has_rruleset() {
1✔
NEW
125
            return None;
×
126
        }
127
        Some(
128
            RRuleSet::new(dtstart)
5✔
129
                .set_rrules(self.rrules.to_owned())
2✔
130
                .set_rdates(
1✔
131
                    self.rdates
1✔
132
                        .iter()
1✔
133
                        .flat_map(|IcalRDATEProperty(dates, _)| {
1✔
134
                            // TODO: Support periods
NEW
135
                            dates.iter().map(|date| date.start().into())
×
136
                        })
137
                        .collect(),
1✔
138
                )
139
                .set_exrules(self.exrules.to_owned())
2✔
140
                .set_exdates(
1✔
141
                    self.exdates
1✔
142
                        .iter()
1✔
143
                        .flat_map(|IcalEXDATEProperty(dates, _)| {
1✔
NEW
144
                            dates.iter().map(|date| date.to_owned().into())
×
145
                        })
146
                        .collect(),
1✔
147
                ),
148
        )
149
    }
150

151
    fn replace_or_push_property<T: ICalProperty + Into<ContentLine>>(&mut self, prop: T) {
7✔
152
        let position = self.properties.iter().position(|prop| T::NAME == prop.name);
28✔
153
        if let Some(pos) = position {
7✔
154
            self.properties.retain(|line| line.name != T::NAME);
15✔
155
            self.properties.insert(pos, prop.into());
5✔
156
        } else {
157
            self.properties.push(prop.into());
5✔
158
        }
159
    }
160

161
    pub fn expand_recurrence(
1✔
162
        &self,
163
        start: Option<DateTime<Utc>>,
164
        end: Option<DateTime<Utc>>,
165
        overrides: &[Self],
166
    ) -> Vec<Self> {
167
        let main = self.clone().to_utc_or_local();
1✔
168
        let mut overrides: Vec<Self> = overrides
169
            .iter()
170
            .map(|over| over.clone().to_utc_or_local())
3✔
171
            .collect();
172
        overrides.sort_by_key(|over| over.recurid.as_ref().unwrap().0.clone());
2✔
173
        let dtstart_utc = main.dtstart.0.utc().with_timezone(&rrule::Tz::UTC);
1✔
174
        let Some(mut rrule_set) = main.get_rruleset(dtstart_utc) else {
1✔
NEW
175
            return std::iter::once(main).chain(overrides).collect();
×
176
        };
177

178
        if let Some(start) = start {
1✔
NEW
179
            rrule_set = rrule_set.after(start.with_timezone(&rrule::Tz::UTC));
×
180
        }
181
        if let Some(end) = end {
1✔
NEW
182
            rrule_set = rrule_set.before(end.with_timezone(&rrule::Tz::UTC));
×
183
        }
184

185
        let mut events = vec![];
1✔
186

187
        let mut template = &main;
1✔
188
        'recurrence: for instance in rrule_set.all(2048).dates {
4✔
189
            let recurid = if main.dtstart.0.is_date() {
3✔
190
                CalDateOrDateTime::Date(CalDate(instance.to_utc().date_naive(), Timezone::utc()))
2✔
191
            } else {
192
                CalDateOrDateTime::DateTime(CalDateTime::from(instance))
2✔
193
            };
194

195
            for over in &overrides {
2✔
196
                let IcalRECURIDProperty(override_recurid, range) = over.recurid.as_ref().unwrap();
2✔
197
                if override_recurid != &recurid {
1✔
198
                    continue;
199
                }
200
                // RECURRENCE IDs match
201
                events.push(over.clone());
1✔
202

203
                if range == &RecurIdRange::ThisAndFuture {
1✔
204
                    // Set this override as the base event for the future
NEW
205
                    template = over;
×
206
                }
207
                continue 'recurrence;
208
            }
209

210
            // We were not overriden, construct recurrence instance:
211
            let mut properties = template.properties.clone();
1✔
212
            // Remove recurrence props
213
            properties.retain(|prop| {
2✔
214
                !["RRULE", "RDATE", "EXRULE", "EXDATE"].contains(&prop.name.as_str())
1✔
215
            });
216
            properties.retain(|prop| prop.name != "DTEND");
3✔
217
            let mut ev = IcalEvent {
218
                uid: template.uid.clone(),
1✔
219
                dtstamp: template.dtstamp.clone(),
1✔
220
                summary: template.summary.clone(),
1✔
221
                dtstart: IcalDTSTARTProperty(recurid.clone(), Default::default()),
2✔
222
                recurid: Some(IcalRECURIDProperty(recurid.clone(), RecurIdRange::This)),
2✔
223
                dtend: template.get_duration().map(|duration| {
2✔
224
                    IcalDTENDProperty((recurid.clone() + duration).into(), Default::default())
225
                }),
226
                alarms: vec![],
1✔
227
                duration: None, // Set by DTEND
228
                rdates: vec![],
1✔
229
                rrules: vec![],
1✔
230
                exdates: vec![],
1✔
231
                exrules: vec![],
1✔
232
                properties,
233
            };
234
            ev.replace_or_push_property(IcalDTSTARTProperty(recurid.clone(), Default::default()));
2✔
235
            ev.replace_or_push_property(IcalRECURIDProperty(recurid, RecurIdRange::This));
1✔
236
            if let Some(duration) = template.get_duration() {
2✔
237
                ev.replace_or_push_property(IcalDURATIONProperty(duration, Default::default()));
2✔
238
            }
239

240
            events.push(ev);
1✔
241
        }
242

243
        events
1✔
244
    }
245
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc