ical/recur/set.rs
1//! # Recurrence set
2//!
3//! The occurrences a *component* denotes, not the ones a single rule does.
4//!
5//! RFC 5545 3.8.5 builds the set a `VEVENT` or `VTODO` actually happens on
6//! out of five properties, plus the overrides that sit in sibling components.
7//!
8//! `DTSTART` and every `RRULE` and `RDATE` add instances, every `EXDATE` and
9//! `EXRULE` take them away, and a component carrying a `RECURRENCE-ID`
10//! replaces one. [`IcalRecurSet`] holds those pieces and
11//! [`IcalRecurSetExpand`] walks them.
12//!
13//! ## Identity, and the order it comes in
14//!
15//! Every occurrence has two times. Its **identity** is the time the rules
16//! place it at, which is what a `RECURRENCE-ID` names and what an `EXDATE`
17//! removes. Its **start** is when it actually happens, which is the identity
18//! unless an override moved it.
19//!
20//! Occurrences come out in the chronological order of their *identity*, which
21//! is what keeps the walk lazy: nothing is buffered, so an endless rule can
22//! be taken from without running it to its end.
23//!
24//! An override that moves an instance is emitted in the place of the instance
25//! it replaces, so its start can fall out of order. A caller that needs
26//! starts in order sorts a window of them, which is a decision about a
27//! window, not about the walk.
28//!
29//! ## Civil, like everything else here
30//!
31//! Nothing here resolves a time zone. `DTSTART`, `RDATE`, `EXDATE` and
32//! `RECURRENCE-ID` are read as the civil times they spell, and a `TZID`
33//! parameter is ignored, exactly as [expansion](crate::recur::expand) ignores
34//! it.
35
36use alloc::{vec, vec::Vec};
37
38use crate::{
39 component::IcalComponent,
40 param::IcalParam,
41 prop::{IcalPropKind, IcalPropName},
42 recur::{IcalRecurDateTime, IcalRecurRule, expand::IcalRecurExpand},
43 tz::IcalTz,
44 value::IcalValue,
45};
46
47/// One occurrence of a recurrence set.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub struct IcalRecurOccurrence {
50 /// The instance identity: the time the rules place this occurrence at, and
51 /// the value a `RECURRENCE-ID` would carry to name it.
52 pub id: IcalRecurDateTime,
53 /// When the occurrence actually starts. The same as
54 /// [`id`](Self::id) unless an override moved it.
55 pub start: IcalRecurDateTime,
56 /// The index into [`IcalRecurSet::overrides`] of the override that replaced
57 /// this instance, if one did.
58 pub over: Option<usize>,
59}
60
61/// A component that replaces one instance of a set, keyed by the identity its
62/// `RECURRENCE-ID` names.
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub struct IcalRecurOverride {
65 /// The identity this override replaces, from its `RECURRENCE-ID`.
66 pub id: IcalRecurDateTime,
67 /// The overriding start, from the override component's own `DTSTART`.
68 pub start: IcalRecurDateTime,
69 /// Whether the override carried `RANGE=THISANDFUTURE`, which shifts this
70 /// instance and every later one by the same offset.
71 pub this_and_future: bool,
72}
73
74/// The recurrence set of one component: what adds, subtracts and overrides.
75///
76/// Build one from a decoded component with
77/// [`of_component`](Self::of_component), or by hand, and walk it with
78/// [`expand`](Self::expand).
79#[derive(Clone, Debug, Default, PartialEq, Eq)]
80pub struct IcalRecurSet {
81 /// The `DTSTART`, always the first instance of the set (RFC 5545 3.8.2.4).
82 pub start: Option<IcalRecurDateTime>,
83 /// Every `RRULE`.
84 pub rules: Vec<IcalRecurRule>,
85 /// Every date named by an `RDATE`, in order. A period item contributes its
86 /// start.
87 pub dates: Vec<IcalRecurDateTime>,
88 /// Every `EXRULE`, deprecated by RFC 5545 but still on the wire.
89 pub exrules: Vec<IcalRecurRule>,
90 /// Every date named by an `EXDATE`, in order.
91 pub exdates: Vec<IcalRecurDateTime>,
92 /// The overrides that replace instances, in order of identity.
93 pub overrides: Vec<IcalRecurOverride>,
94}
95
96impl IcalRecurSet {
97 /// Read the set a decoded component denotes, its overrides aside.
98 ///
99 /// A component with no `DTSTART` and no `RDATE` denotes nothing and comes
100 /// back empty rather than as an error: this is the liberal side of the
101 /// crate. A malformed date or rule is skipped for the same reason.
102 pub fn of_component(component: &IcalComponent<'_>) -> Self {
103 let mut set = Self::default();
104
105 for prop in &component.props {
106 let IcalPropName::Kind(kind) = prop.name else {
107 continue;
108 };
109
110 match kind {
111 IcalPropKind::DtStart => set.start = date_of(&prop.value),
112 IcalPropKind::RRule => set.rules.extend(rule_of(&prop.value)),
113 IcalPropKind::ExRule => set.exrules.extend(rule_of(&prop.value)),
114 IcalPropKind::RDate => set.dates.extend(dates_of(&prop.value)),
115 IcalPropKind::ExDate => set.exdates.extend(dates_of(&prop.value)),
116 _ => {}
117 }
118 }
119
120 set.dates.sort_unstable();
121 set.dates.dedup();
122 set.exdates.sort_unstable();
123 set.exdates.dedup();
124
125 set
126 }
127
128 /// Add the override a sibling component carrying a `RECURRENCE-ID` states.
129 ///
130 /// A component with no `RECURRENCE-ID`, or with no `DTSTART` to move the
131 /// instance to, is not an override and is ignored.
132 pub fn with_override(&mut self, component: &IcalComponent<'_>) -> &mut Self {
133 let mut id = None;
134 let mut start = None;
135 let mut this_and_future = false;
136
137 for prop in &component.props {
138 let IcalPropName::Kind(kind) = prop.name else {
139 continue;
140 };
141
142 match kind {
143 IcalPropKind::RecurrenceId => {
144 id = date_of(&prop.value);
145 this_and_future = prop.params.iter().any(|param| {
146 matches!(param, IcalParam::Range(range) if range.eq_ignore_ascii_case("THISANDFUTURE"))
147 });
148 }
149 IcalPropKind::DtStart => start = date_of(&prop.value),
150 _ => {}
151 }
152 }
153
154 if let (Some(id), Some(start)) = (id, start) {
155 self.overrides.push(IcalRecurOverride {
156 id,
157 start,
158 this_and_future,
159 });
160 self.overrides.sort_unstable_by_key(|over| over.id);
161 }
162
163 self
164 }
165
166 /// The set a whole calendar denotes for one `UID`: the series component,
167 /// plus every sibling that overrides an instance of it.
168 ///
169 /// The series is the component carrying that `UID` with no
170 /// `RECURRENCE-ID`; every other one carrying it is an override.
171 pub fn of_uid(components: &[IcalComponent<'_>], uid: &str) -> Self {
172 let mut set = Self::default();
173
174 for component in components {
175 if uid_of(component) != Some(uid) {
176 continue;
177 }
178
179 if has(component, IcalPropKind::RecurrenceId) {
180 set.with_override(component);
181 } else {
182 let series = Self::of_component(component);
183 set.start = series.start;
184 set.rules = series.rules;
185 set.dates = series.dates;
186 set.exrules = series.exrules;
187 set.exdates = series.exdates;
188 }
189 }
190
191 set
192 }
193
194 /// Walk the set, lazily, in identity order.
195 pub fn expand(&self) -> IcalRecurSetExpand<'_> {
196 self.walk(None)
197 }
198
199 /// Walk the set against a time zone, dropping the instances its rules
200 /// generate at local times the zone jumps over (RFC 5545 3.3.10).
201 ///
202 /// The filter sits on the rule streams alone. An `RDATE` does not generate
203 /// an instance, it names one, so a date written into one is as deliberate
204 /// as a lone `DTSTART` and is kept whatever the zone says of it.
205 pub fn expand_in_zone(&self, zone: &IcalTz) -> IcalRecurSetExpand<'_> {
206 self.walk(Some(zone))
207 }
208
209 /// The walk both expansions are, with and without a zone to filter by.
210 fn walk(&self, zone: Option<&IcalTz>) -> IcalRecurSetExpand<'_> {
211 let start = self.start;
212
213 let stream = |rule: &IcalRecurRule| {
214 let expand = IcalRecurExpand::new(rule.clone(), start?);
215
216 Some(match zone {
217 Some(zone) => expand.in_zone(zone.clone()),
218 None => expand,
219 })
220 };
221
222 IcalRecurSetExpand {
223 set: self,
224 streams: self.rules.iter().filter_map(stream).collect(),
225 heads: vec![None; self.rules.len()],
226 primed: false,
227 // NOTE: The literal sources: DTSTART, the RDATEs, and the identity
228 // of every override, which is an instance whether or not a rule
229 // generates it.
230 literals: {
231 let mut literals: Vec<IcalRecurDateTime> = start.into_iter().collect();
232 literals.extend(self.dates.iter().copied());
233 literals.extend(self.overrides.iter().map(|over| over.id));
234 literals.sort_unstable();
235 literals.dedup();
236 literals
237 },
238 literal: 0,
239 exrules: self.exrules.iter().filter_map(stream).collect(),
240 exheads: vec![None; self.exrules.len()],
241 last: None,
242 }
243 }
244}
245
246/// The lazy walk of an [`IcalRecurSet`], in identity order.
247///
248/// A k-way merge over the rule expansions and the literal dates, exclusions
249/// applied as it goes: an `EXDATE` is a membership test, an `EXRULE` another
250/// lazy stream in step. Only the wire-literal lists are ever materialised.
251pub struct IcalRecurSetExpand<'a> {
252 set: &'a IcalRecurSet,
253 streams: Vec<IcalRecurExpand>,
254 heads: Vec<Option<IcalRecurDateTime>>,
255 primed: bool,
256 literals: Vec<IcalRecurDateTime>,
257 literal: usize,
258 exrules: Vec<IcalRecurExpand>,
259 exheads: Vec<Option<IcalRecurDateTime>>,
260 last: Option<IcalRecurDateTime>,
261}
262
263impl Iterator for IcalRecurSetExpand<'_> {
264 type Item = IcalRecurOccurrence;
265
266 fn next(&mut self) -> Option<Self::Item> {
267 loop {
268 let id = self.next_id()?;
269
270 if self.excluded(id) {
271 continue;
272 }
273
274 let over = self.set.overrides.iter().position(|over| over.id == id);
275
276 let start = match over {
277 Some(index) => self.set.overrides[index].start,
278 None => IcalRecurDateTime::from_seconds(id.seconds() + self.shift(id)),
279 };
280
281 return Some(IcalRecurOccurrence { id, start, over });
282 }
283 }
284}
285
286impl IcalRecurSetExpand<'_> {
287 /// The next identity in chronological order, deduplicated across sources.
288 fn next_id(&mut self) -> Option<IcalRecurDateTime> {
289 loop {
290 if !self.primed {
291 for (index, stream) in self.streams.iter_mut().enumerate() {
292 self.heads[index] = stream.next();
293 }
294 self.primed = true;
295 }
296
297 let from_rules = self.heads.iter().flatten().min().copied();
298 let from_literals = self.literals.get(self.literal).copied();
299
300 let next = match (from_rules, from_literals) {
301 (Some(rule), Some(literal)) => rule.min(literal),
302 (Some(rule), None) => rule,
303 (None, Some(literal)) => literal,
304 (None, None) => return None,
305 };
306
307 // NOTE: Consume every source sitting on it, so an instance a rule
308 // and an RDATE both name is yielded once.
309 for (index, head) in self.heads.iter_mut().enumerate() {
310 if *head == Some(next) {
311 *head = self.streams[index].next();
312 }
313 }
314 if from_literals == Some(next) {
315 self.literal += 1;
316 }
317
318 // NOTE: A rule and a literal can still collide across iterations
319 // when a stream repeats a value; the last-yielded guard makes the
320 // walk strictly increasing whatever the sources do.
321 if self.last == Some(next) {
322 continue;
323 }
324
325 self.last = Some(next);
326 return Some(next);
327 }
328 }
329
330 /// Whether an identity is excluded, by an `EXDATE` or an `EXRULE`.
331 fn excluded(&mut self, id: IcalRecurDateTime) -> bool {
332 if self.set.exdates.binary_search(&id).is_ok() {
333 return true;
334 }
335
336 for (index, stream) in self.exrules.iter_mut().enumerate() {
337 // NOTE: Advance this exception stream up to the candidate: it is
338 // sorted, so anything it has already passed can never match again.
339 while self.exheads[index].is_none_or(|head| head < id) {
340 match stream.next() {
341 Some(next) => self.exheads[index] = Some(next),
342 None => break,
343 }
344 }
345
346 if self.exheads[index] == Some(id) {
347 return true;
348 }
349 }
350
351 false
352 }
353
354 /// The offset every `RANGE=THISANDFUTURE` override in force at `id`
355 /// applies, in seconds. The latest one wins, as a later override restates
356 /// the shift rather than compounding it.
357 fn shift(&self, id: IcalRecurDateTime) -> i64 {
358 self.set
359 .overrides
360 .iter()
361 .rfind(|over| over.this_and_future && over.id <= id)
362 .map(|over| over.start.seconds() - over.id.seconds())
363 .unwrap_or(0)
364 }
365}
366
367/// The civil date a date-ish value names, when it names one.
368fn date_of(value: &IcalValue<'_>) -> Option<IcalRecurDateTime> {
369 let text = match value {
370 IcalValue::Date(date) => &date.0,
371 IcalValue::DateTime(date) => &date.0,
372 IcalValue::DateTimeList(dates) => dates.0.first()?,
373 _ => return None,
374 };
375
376 IcalRecurDateTime::parse(text).ok()
377}
378
379/// Every civil date a list value names. A period item (`start/end` or
380/// `start/duration`, which `RDATE` admits) contributes its start.
381fn dates_of(value: &IcalValue<'_>) -> Vec<IcalRecurDateTime> {
382 let items: &[_] = match value {
383 IcalValue::DateTimeList(dates) => &dates.0,
384 // NOTE: A single-valued RDATE or EXDATE, however it was built.
385 other => return date_of(other).into_iter().collect(),
386 };
387
388 items
389 .iter()
390 .filter_map(|item| {
391 let start = item.split('/').next().unwrap_or(item);
392 IcalRecurDateTime::parse(start).ok()
393 })
394 .collect()
395}
396
397/// The rule a recurrence value states, when it states a readable one.
398fn rule_of(value: &IcalValue<'_>) -> Option<IcalRecurRule> {
399 let IcalValue::Recur(recur) = value else {
400 return None;
401 };
402
403 IcalRecurRule::parse(&recur.0).ok()
404}
405
406/// The `UID` of a component, if it carries one.
407fn uid_of<'a>(component: &'a IcalComponent<'_>) -> Option<&'a str> {
408 component.props.iter().find_map(|prop| {
409 if !matches!(prop.name, IcalPropName::Kind(IcalPropKind::Uid)) {
410 return None;
411 }
412
413 match &prop.value {
414 IcalValue::Text(text) => Some(&*text.0),
415 _ => None,
416 }
417 })
418}
419
420/// Whether a component carries a property of the given kind.
421fn has(component: &IcalComponent<'_>, kind: IcalPropKind) -> bool {
422 component
423 .props
424 .iter()
425 .any(|prop| matches!(prop.name, IcalPropName::Kind(k) if k == kind))
426}