Skip to main content

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}