expand.rs
⎇
Raw
1//! Recurrence expansion: DTSTART + RRULE + RDATE - EXRULE - EXDATE, with
2//! RECURRENCE-ID overrides applied.
3
4use std::collections::{BTreeMap, HashMap, HashSet};
5use std::ops::Range;
6
7use calcard::common::{CalendarScale, PartialDateTime};
8use calcard::icalendar::{
9 ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarDuration, ICalendarFrequency,
10 ICalendarParameterName, ICalendarPeriod, ICalendarProperty, ICalendarRecurrenceRule,
11 ICalendarSkip, ICalendarValue,
12};
13use chrono::{DateTime, NaiveDateTime, NaiveTime, TimeDelta, TimeZone, Timelike, Utc, Weekday};
14
15use crate::zone::{Zone, Zones, add, add_local};
16
17/// Occurrences the rules of one series may generate together before
18/// expansion gives up.
19// ponytail: COUNT, MONTHLY and YEARLY rules iterate from DTSTART, so a long
20// running one can hit this. Skip ahead for them if that matters.
21pub(crate) const MAX_OCCURRENCES: usize = 1_000_000;
22
23#[derive(Debug, Clone, PartialEq)]
24pub struct Instance {
25 pub start: DateTime<Utc>,
26 pub end: DateTime<Utc>,
27 /// The original start. `None` if the component does not recur.
28 pub recurrence_id: Option<DateTime<Utc>>,
29 /// Index into `ICalendar::components` of the master or override that
30 /// describes this instance.
31 pub component: usize,
32}
33
34#[derive(Debug, Default)]
35pub struct Expansion {
36 /// Sorted by start.
37 pub instances: Vec<Instance>,
38 /// A series reached `MAX_OCCURRENCES`, so later instances are missing.
39 pub truncated: bool,
40 /// Occurrences the rules produced, also those outside the window.
41 pub generated: usize,
42}
43
44/// The instances of all VEVENT, VTODO and VJOURNAL components that overlap
45/// `window`. A zero-length instance overlaps if it starts inside.
46///
47/// Components are grouped by type and UID into one master and its overrides.
48/// Components without DTSTART are skipped. `floating` interprets values
49/// without a zone.
50pub fn expand(cal: &ICalendar, window: Range<DateTime<Utc>>, floating: Zone) -> Expansion {
51 expand_in(cal, &Zones::new(cal, floating), window, MAX_OCCURRENCES)
52}
53
54/// `expand` with the zones built, and at most `cap` occurrences per series.
55pub(crate) fn expand_in(
56 cal: &ICalendar,
57 zones: &Zones,
58 window: Range<DateTime<Utc>>,
59 cap: usize,
60) -> Expansion {
61 let mut out = Expansion::default();
62 let mut groups: HashMap<(&ICalendarComponentType, &str), Vec<usize>> = HashMap::new();
63 for (i, c) in cal.components.iter().enumerate() {
64 if !matches!(
65 c.component_type,
66 ICalendarComponentType::VEvent
67 | ICalendarComponentType::VTodo
68 | ICalendarComponentType::VJournal
69 ) {
70 continue;
71 }
72 match c.uid() {
73 Some(uid) => groups.entry((&c.component_type, uid)).or_default().push(i),
74 None => expand_group(cal, zones, &[i], &window, cap, &mut out),
75 }
76 }
77 for group in groups.values() {
78 expand_group(cal, zones, group, &window, cap, &mut out);
79 }
80 out.instances.sort_by_key(|i| (i.start, i.component));
81 out
82}
83
84/// A date or date-time value and the zone it is in.
85#[derive(Debug, Clone)]
86pub(crate) struct Stamp {
87 pub(crate) local: NaiveDateTime,
88 zone: Zone,
89 pub(crate) date: bool,
90 /// A date-time without TZID or UTC offset.
91 floating: bool,
92}
93
94impl Stamp {
95 pub(crate) fn utc(&self) -> DateTime<Utc> {
96 self.zone.to_utc(self.local)
97 }
98}
99
100#[derive(Debug, Clone)]
101enum Length {
102 Exact(TimeDelta),
103 /// Whole days count in wall-clock time, so a day can last 23 or 25 hours.
104 Nominal {
105 days: i64,
106 exact: TimeDelta,
107 },
108}
109
110impl Length {
111 fn end(&self, zone: &Zone, local: NaiveDateTime, utc: DateTime<Utc>) -> DateTime<Utc> {
112 match self {
113 Length::Exact(d) => add(utc, *d),
114 Length::Nominal { days, exact } => add(
115 zone.to_utc(add_local(local, TimeDelta::days(*days))),
116 *exact,
117 ),
118 }
119 }
120
121 /// An upper bound, for widening the generation window.
122 fn max(&self) -> TimeDelta {
123 match self {
124 Length::Exact(d) => *d,
125 Length::Nominal { days, exact } => TimeDelta::days(days + 1) + *exact,
126 }
127 }
128}
129
130/// DTSTART and length of one component.
131#[derive(Debug, Clone)]
132struct Timing {
133 start: Stamp,
134 length: Length,
135 component: usize,
136}
137
138impl Timing {
139 fn of(cal: &ICalendar, zones: &Zones, component: usize) -> Option<Self> {
140 let c = &cal.components[component];
141 let start = prop_stamp(zones, c, &ICalendarProperty::Dtstart)?;
142 let end = match c.component_type {
143 ICalendarComponentType::VTodo => ICalendarProperty::Due,
144 _ => ICalendarProperty::Dtend,
145 };
146 let zero = TimeDelta::zero();
147 let length = if let Some(end) = prop_stamp(zones, c, &end) {
148 if start.date && end.date {
149 Length::Nominal {
150 days: (end.local - start.local).num_days().max(0),
151 exact: zero,
152 }
153 } else {
154 Length::Exact((end.utc() - start.utc()).max(zero))
155 }
156 } else if let Some(ICalendarValue::Duration(d)) = c
157 .property(&ICalendarProperty::Duration)
158 .and_then(|e| e.values.first())
159 {
160 nominal(d)
161 } else if start.date {
162 Length::Nominal {
163 days: 1,
164 exact: zero,
165 }
166 } else {
167 Length::Exact(zero)
168 };
169 Some(Timing {
170 start,
171 length,
172 component,
173 })
174 }
175
176 fn end_at(&self, local: NaiveDateTime, utc: DateTime<Utc>) -> DateTime<Utc> {
177 self.length.end(&self.start.zone, local, utc)
178 }
179
180 /// A RECURRENCE-ID, RDATE or EXDATE value. Dates and floating times are
181 /// read in the zone of the series: clients write them that way.
182 fn utc_of(&self, s: &Stamp) -> DateTime<Utc> {
183 if s.date || s.floating {
184 self.start.zone.to_utc(s.local)
185 } else {
186 s.utc()
187 }
188 }
189
190 /// The identity of a recurrence instance. An all-day series matches by
191 /// date alone, whatever zone the other value names.
192 fn key(&self, s: &Stamp) -> i64 {
193 if self.start.date {
194 day_key(s.local)
195 } else {
196 self.utc_of(s).timestamp()
197 }
198 }
199
200 fn member_key(&self, local: NaiveDateTime, utc: DateTime<Utc>) -> i64 {
201 if self.start.date {
202 day_key(local)
203 } else {
204 utc.timestamp()
205 }
206 }
207}
208
209struct Member {
210 local: NaiveDateTime,
211 utc: DateTime<Utc>,
212 /// From an RDATE period.
213 length: Option<Length>,
214}
215
216struct Override {
217 sequence: i64,
218 recurrence_id: DateTime<Utc>,
219 timing: Timing,
220}
221
222fn expand_group(
223 cal: &ICalendar,
224 zones: &Zones,
225 group: &[usize],
226 window: &Range<DateTime<Utc>>,
227 max: usize,
228 out: &mut Expansion,
229) {
230 let comp = |i: usize| &cal.components[i];
231 let sequence = |i: usize| {
232 comp(i)
233 .property(&ICalendarProperty::Sequence)
234 .and_then(|e| e.values.first()?.as_integer())
235 .unwrap_or(0)
236 };
237 let (overrides, masters): (Vec<usize>, Vec<usize>) = group
238 .iter()
239 .partition(|&&i| comp(i).has_property(&ICalendarProperty::RecurrenceId));
240 // Several masters for one UID are invalid. Take the latest revision,
241 // or the first one on a tie.
242 let master = masters
243 .into_iter()
244 .rev()
245 .max_by_key(|&i| sequence(i))
246 .and_then(|i| Timing::of(cal, zones, i));
247
248 // Matched by RECURRENCE-ID only. SEQUENCE picks between two overrides of
249 // one instance and never decides whether an override applies.
250 let mut exact: HashMap<i64, Override> = HashMap::new();
251 let mut future: Vec<(i64, DateTime<Utc>, Timing)> = Vec::new();
252 for i in overrides {
253 let Some(e) = comp(i).property(&ICalendarProperty::RecurrenceId) else {
254 continue;
255 };
256 let (Some(timing), Some(rid)) = (
257 Timing::of(cal, zones, i),
258 e.values
259 .first()
260 .and_then(|v| stamp(zones, v.as_partial_date_time()?, e.tz_id())),
261 ) else {
262 continue;
263 };
264 let (key, rid_utc) = match &master {
265 Some(m) => (m.key(&rid), m.utc_of(&rid)),
266 None => (rid.utc().timestamp(), rid.utc()),
267 };
268 if e.parameter(&ICalendarParameterName::Range).is_some() {
269 future.push((key, rid_utc, timing.clone()));
270 }
271 if exact.get(&key).is_none_or(|old| sequence(i) > old.sequence) {
272 let sequence = sequence(i);
273 exact.insert(
274 key,
275 Override {
276 sequence,
277 recurrence_id: rid_utc,
278 timing,
279 },
280 );
281 }
282 }
283 future.sort_by_key(|f| f.0);
284
285 let Some(m) = master else {
286 push_overrides(out, window, exact.values());
287 return;
288 };
289 let mc = comp(m.component);
290 let mz = &m.start.zone;
291
292 let rules: Vec<_> = mc
293 .properties(&ICalendarProperty::Rrule)
294 .filter_map(|e| rule(e.values.first()?))
295 .collect();
296 let mut set: BTreeMap<i64, Member> = BTreeMap::new();
297 let mut slack = m.length.max();
298 // DTSTART is an instance even when it does not match the RRULE, unless
299 // the series ended before it.
300 let ended = !rules.is_empty()
301 && rules.iter().all(|r| {
302 r.until
303 .as_ref()
304 .and_then(|u| until_local(u, &m.start))
305 .is_some_and(|u| u < m.start.local)
306 });
307 if !ended {
308 let utc = m.start.utc();
309 set.insert(
310 m.member_key(m.start.local, utc),
311 Member {
312 local: m.start.local,
313 utc,
314 length: None,
315 },
316 );
317 }
318 for e in mc.properties(&ICalendarProperty::Rdate) {
319 for v in &e.values {
320 let (s, length) = match v {
321 ICalendarValue::PartialDateTime(p) => {
322 let Some(s) = stamp(zones, p, e.tz_id()) else {
323 continue;
324 };
325 (s, None)
326 }
327 ICalendarValue::Period(ICalendarPeriod::Range { start, end }) => {
328 let (Some(s), Some(end)) =
329 (stamp(zones, start, e.tz_id()), stamp(zones, end, e.tz_id()))
330 else {
331 continue;
332 };
333 let d = (end.utc() - s.utc()).max(TimeDelta::zero());
334 (s, Some(Length::Exact(d)))
335 }
336 ICalendarValue::Period(ICalendarPeriod::Duration { start, duration }) => {
337 let Some(s) = stamp(zones, start, e.tz_id()) else {
338 continue;
339 };
340 (s, Some(nominal(duration)))
341 }
342 _ => continue,
343 };
344 let utc = m.utc_of(&s);
345 let local = mz.to_local(utc);
346 if let Some(l) = &length {
347 slack = slack.max(l.max());
348 }
349 // A period on DTSTART or a rule instance still sets its length.
350 let member = set.entry(m.key(&s)).or_insert(Member {
351 local,
352 utc,
353 length: None,
354 });
355 member.length = length.or(member.length.take());
356 }
357 }
358 for (_, rid, t) in &future {
359 slack = slack.max((t.start.utc() - *rid).abs() + t.length.max());
360 }
361 let wide = slack
362 .checked_add(&TimeDelta::days(1))
363 .unwrap_or(TimeDelta::MAX);
364 let reach = add(window.start, -wide);
365 // A huge DURATION would run an endless rule to the cap, so the rules
366 // iterate at most 1000 periods of the fastest rule around the window.
367 // ponytail: a rule instance longer than that is missed where it starts early.
368 let cap = mc
369 .properties(&ICalendarProperty::Rrule)
370 .chain(mc.properties(&ICalendarProperty::Exrule))
371 .filter_map(|e| rule(e.values.first()?))
372 .map(|r| period(r) * 1000)
373 .min();
374 let slack = cap.map_or(wide, |c| wide.min(c + TimeDelta::days(1)));
375 let (from, to) = (add(window.start, -slack), add(window.end, slack));
376 let (from_local, to_local) = (mz.to_local(from), mz.to_local(to));
377
378 let mut budget = max;
379 let mut excluded = HashSet::new();
380 for e in mc.properties(&ICalendarProperty::Exrule) {
381 let Some(rule) = e.values.first().and_then(rule) else {
382 continue;
383 };
384 for local in occurrences_capped(rule, &m, from_local, to_local, &mut budget, out) {
385 excluded.insert(m.member_key(local, mz.to_utc(local)));
386 }
387 }
388 // ponytail: sub-daily rules iterate in wall time. A DST fall-back skips the
389 // repeated hour, and a spring-forward collapses a shifted instance into the
390 // next (COUNT=5 gives 4). Iterate in elapsed time, BYHOUR in local, to fix.
391 for rule in rules {
392 for local in occurrences_capped(rule, &m, from_local, to_local, &mut budget, out) {
393 let utc = mz.to_utc(local);
394 if utc >= from {
395 set.entry(m.member_key(local, utc)).or_insert(Member {
396 local,
397 utc,
398 length: None,
399 });
400 }
401 }
402 }
403
404 // A DATE EXDATE on a DATE-TIME series removes every instance on that day.
405 let mut excluded_days = HashSet::new();
406 for e in mc.properties(&ICalendarProperty::Exdate) {
407 for s in e
408 .values
409 .iter()
410 .filter_map(|v| stamp(zones, v.as_partial_date_time()?, e.tz_id()))
411 {
412 if s.date && !m.start.date {
413 excluded_days.insert(s.local.date());
414 } else {
415 excluded.insert(m.key(&s));
416 }
417 }
418 }
419
420 // An EXDATE removes an override of that instance too. An override whose
421 // RECURRENCE-ID matches no instance still shows, as in clients.
422 push_overrides(
423 out,
424 window,
425 exact.iter().filter_map(|(key, o)| {
426 let day = mz.to_local(o.recurrence_id).date();
427 (!excluded.contains(key) && !excluded_days.contains(&day)).then_some(o)
428 }),
429 );
430
431 let recurs =
432 mc.has_property(&ICalendarProperty::Rrule) || mc.has_property(&ICalendarProperty::Rdate);
433 for (key, member) in &set {
434 if member.utc < reach
435 || excluded.contains(key)
436 || excluded_days.contains(&member.local.date())
437 || exact.contains_key(key)
438 {
439 continue;
440 }
441 let recurrence_id = recurs.then_some(member.utc);
442 // The latest THISANDFUTURE override before this instance moves it by
443 // the same offset and gives it the override's length. The offset is
444 // wall-clock time, so later instances keep their local time across DST.
445 let latest = future.partition_point(|f| f.0 <= *key).checked_sub(1);
446 let instance = match latest.map(|i| &future[i]) {
447 Some((_, rid, t)) => {
448 let shift = mz.to_local(t.start.utc()) - mz.to_local(*rid);
449 let start = mz.to_utc(add_local(member.local, shift));
450 Instance {
451 start,
452 end: t.end_at(t.start.zone.to_local(start), start),
453 recurrence_id,
454 component: t.component,
455 }
456 }
457 None => Instance {
458 start: member.utc,
459 end: member.length.as_ref().map_or_else(
460 || m.end_at(member.local, member.utc),
461 |l| l.end(mz, member.local, member.utc),
462 ),
463 recurrence_id,
464 component: m.component,
465 },
466 };
467 push(out, window, instance);
468 }
469}
470
471fn push_overrides<'a>(
472 out: &mut Expansion,
473 window: &Range<DateTime<Utc>>,
474 overrides: impl Iterator<Item = &'a Override>,
475) {
476 for o in overrides {
477 let t = &o.timing;
478 let start = t.start.utc();
479 let end = t.end_at(t.start.local, start);
480 push(
481 out,
482 window,
483 Instance {
484 start,
485 end,
486 recurrence_id: Some(o.recurrence_id),
487 component: t.component,
488 },
489 );
490 }
491}
492
493fn occurrences_capped(
494 rule: &ICalendarRecurrenceRule,
495 m: &Timing,
496 from_local: NaiveDateTime,
497 to_local: NaiveDateTime,
498 budget: &mut usize,
499 out: &mut Expansion,
500) -> Vec<NaiveDateTime> {
501 let until = match &rule.until {
502 Some(u) => match until_local(u, &m.start) {
503 Some(u) => Some(u),
504 None => return Vec::new(),
505 },
506 None => None,
507 };
508 // Whole steps keep the period grid, so INTERVAL and BYxxx still line up.
509 let unit = match rule.freq {
510 ICalendarFrequency::Weekly => 7 * 86400,
511 ICalendarFrequency::Daily => 86400,
512 ICalendarFrequency::Hourly => 3600,
513 ICalendarFrequency::Minutely => 60,
514 ICalendarFrequency::Secondly => 1,
515 _ => 0,
516 };
517 let mut first = m.start.local;
518 // BYSETPOS picks from a whole period, which a mid-period start would cut.
519 if unit > 0 && rule.count.is_none() && rule.bysetpos.is_empty() {
520 let step = unit * i64::from(rule.interval.unwrap_or(1).max(1));
521 let behind = (from_local - first).num_seconds();
522 if behind > 0 {
523 first += TimeDelta::seconds(behind / step * step);
524 }
525 }
526 let mut list: Vec<_> = occurrences(rule, first, until, to_local)
527 .take(*budget + 1)
528 .collect();
529 if list.len() > *budget {
530 list.pop();
531 out.truncated = true;
532 }
533 *budget -= list.len();
534 out.generated += list.len();
535 // DTSTART is the first of COUNT (RFC 5545, 3.3.10), also when the rule
536 // skips it.
537 if let Some(n) = rule.count.filter(|_| until.is_none()).map(|n| n as usize)
538 && list.first() != Some(&m.start.local)
539 && list.len() >= n
540 {
541 list.truncate(n.saturating_sub(1));
542 }
543 list
544}
545
546/// The longest time between two periods of `r`.
547fn period(r: &ICalendarRecurrenceRule) -> TimeDelta {
548 let days = match r.freq {
549 ICalendarFrequency::Yearly => 366,
550 ICalendarFrequency::Monthly => 31,
551 ICalendarFrequency::Weekly => 7,
552 _ => 1,
553 };
554 let unit = match r.freq {
555 ICalendarFrequency::Hourly => TimeDelta::hours(1),
556 ICalendarFrequency::Minutely => TimeDelta::minutes(1),
557 ICalendarFrequency::Secondly => TimeDelta::seconds(1),
558 _ => TimeDelta::days(days),
559 };
560 unit * i32::from(r.interval.unwrap_or(1).max(1))
561}
562
563fn push(out: &mut Expansion, window: &Range<DateTime<Utc>>, i: Instance) {
564 let overlaps = if i.start == i.end {
565 window.contains(&i.start)
566 } else {
567 i.start < window.end && i.end > window.start
568 };
569 if overlaps {
570 out.instances.push(i);
571 }
572}
573
574/// UNTIL as wall-clock time in the zone of DTSTART. The rule iterates in wall
575/// time, so a UTC UNTIL compared as-is cuts the last instance east of UTC.
576fn until_local(u: &PartialDateTime, start: &Stamp) -> Option<NaiveDateTime> {
577 let dt = u.to_date_time()?;
578 Some(match dt.offset {
579 Some(o) => {
580 let utc = dt.date_time - TimeDelta::seconds(o.local_minus_utc().into());
581 let local = start.zone.to_local(utc.and_utc());
582 match start.date {
583 // Clients write midnight in UTC or in their own zone. The
584 // later date is the last day either way.
585 true => utc.date().max(local.date()).and_hms_opt(23, 59, 59)?,
586 false => local,
587 }
588 }
589 // A DATE on a DATE-TIME series includes that whole day.
590 None if u.hour.is_none() && !start.date => dt.date_time.date().and_hms_opt(23, 59, 59)?,
591 // Floating, or a DATE on an all-day series: already wall time.
592 None => dt.date_time,
593 })
594}
595
596/// The last instant a rule with DTSTART `start` and UNTIL `u` starts at.
597pub(crate) fn until_utc(u: &PartialDateTime, start: &Stamp) -> Option<DateTime<Utc>> {
598 Some(start.zone.to_utc(until_local(u, start)?))
599}
600
601fn day_key(local: NaiveDateTime) -> i64 {
602 local.date().and_time(NaiveTime::MIN).and_utc().timestamp()
603}
604
605fn nominal(d: &ICalendarDuration) -> Length {
606 if d.neg {
607 return Length::Exact(TimeDelta::zero());
608 }
609 Length::Nominal {
610 days: i64::from(d.weeks) * 7 + i64::from(d.days),
611 exact: TimeDelta::seconds(
612 i64::from(d.hours) * 3600 + i64::from(d.minutes) * 60 + i64::from(d.seconds),
613 ),
614 }
615}
616
617/// The original start and end of the instance at `rid` of the series in
618/// component `master`.
619pub(crate) fn original(
620 cal: &ICalendar,
621 zones: &Zones,
622 master: usize,
623 rid: &Stamp,
624) -> Option<(DateTime<Utc>, DateTime<Utc>)> {
625 let t = Timing::of(cal, zones, master)?;
626 let start = t.utc_of(rid);
627 Some((start, t.end_at(t.start.zone.to_local(start), start)))
628}
629
630pub(crate) fn stamp(zones: &Zones, v: &PartialDateTime, tzid: Option<&str>) -> Option<Stamp> {
631 let dt = v.to_date_time()?;
632 let date = v.hour.is_none();
633 let zone = match dt.offset {
634 Some(o) if o.local_minus_utc() == 0 => Zone::Utc,
635 Some(o) => Zone::Fixed(o.local_minus_utc()),
636 None if date => zones.floating().clone(),
637 None => zones.get(tzid),
638 };
639 Some(Stamp {
640 local: dt.date_time,
641 zone,
642 date,
643 floating: !date && dt.offset.is_none() && tzid.is_none(),
644 })
645}
646
647fn prop_stamp(zones: &Zones, c: &ICalendarComponent, prop: &ICalendarProperty) -> Option<Stamp> {
648 let e = c.property(prop)?;
649 stamp(zones, e.values.first()?.as_partial_date_time()?, e.tz_id())
650}
651
652pub(crate) fn rule(v: &ICalendarValue) -> Option<&ICalendarRecurrenceRule> {
653 match v {
654 ICalendarValue::RecurrenceRule(r) => Some(r),
655 _ => None,
656 }
657}
658
659/// Wall-clock occurrences of `rule` from `start` up to `end`. An invalid or
660/// unsupported rule yields none.
661pub(crate) fn occurrences(
662 rule: &ICalendarRecurrenceRule,
663 start: NaiveDateTime,
664 until: Option<NaiveDateTime>,
665 end: NaiveDateTime,
666) -> impl Iterator<Item = NaiveDateTime> {
667 let wall = |t: NaiveDateTime| rrule::Tz::UTC.from_utc_datetime(&t);
668 let freq = match rule.freq {
669 ICalendarFrequency::Yearly => rrule::Frequency::Yearly,
670 ICalendarFrequency::Monthly => rrule::Frequency::Monthly,
671 ICalendarFrequency::Weekly => rrule::Frequency::Weekly,
672 ICalendarFrequency::Daily => rrule::Frequency::Daily,
673 ICalendarFrequency::Hourly => rrule::Frequency::Hourly,
674 ICalendarFrequency::Minutely => rrule::Frequency::Minutely,
675 ICalendarFrequency::Secondly => rrule::Frequency::Secondly,
676 };
677 // Other calendar scales and leap months (RFC 7529) are unsupported. RFC
678 // 7529 allows treating such a rule as absent.
679 let months: Option<Vec<chrono::Month>> = rule
680 .bymonth
681 .iter()
682 .map(|m| (!m.is_leap()).then(|| chrono::Month::try_from(m.month()).ok())?)
683 .collect();
684 let valid = months.is_some()
685 && rule
686 .rscale
687 .as_ref()
688 .is_none_or(|s| *s == CalendarScale::Gregorian)
689 && rule.skip.is_none_or(|s| s == ICalendarSkip::Omit);
690 // rrule intersects plain and numbered weekdays, RFC 5545 unites them.
691 // Numbering every occurrence of the plain ones gives the union.
692 let mixed = rule.byday.iter().any(|d| d.ordwk.is_some())
693 && rule.byday.iter().any(|d| d.ordwk.is_none());
694 let max_nth = if rule.freq == ICalendarFrequency::Monthly || !rule.bymonth.is_empty() {
695 5
696 } else {
697 53
698 };
699 let weekdays = rule
700 .byday
701 .iter()
702 .flat_map(|d| {
703 let wd = d.weekday.into();
704 match d.ordwk {
705 Some(n) => vec![rrule::NWeekday::Nth(n, wd)],
706 None if mixed => (1..=max_nth).map(|n| rrule::NWeekday::Nth(n, wd)).collect(),
707 None => vec![rrule::NWeekday::Every(wd)],
708 }
709 })
710 .collect();
711 let interval = rule.interval.unwrap_or(1).max(1);
712 // rrule loses the INTERVAL grid when it jumps to the next allowed hour or
713 // minute and the interval does not divide 60. Filter those limits here.
714 let (hours, minutes) = match rule.freq {
715 ICalendarFrequency::Minutely if 60 % interval != 0 => (rule.byhour.clone(), Vec::new()),
716 ICalendarFrequency::Secondly if 60 % interval != 0 => {
717 (rule.byhour.clone(), rule.byminute.clone())
718 }
719 _ => (Vec::new(), Vec::new()),
720 };
721 let filtered = !(hours.is_empty() && minutes.is_empty());
722 let mut r = rrule::RRule::new(freq)
723 .interval(interval)
724 .week_start(rule.wkst.map_or(Weekday::Mon, Into::into))
725 .by_set_pos(rule.bysetpos.clone())
726 .by_month(&months.unwrap_or_default())
727 .by_month_day(rule.bymonthday.clone())
728 .by_year_day(rule.byyearday.clone())
729 .by_week_no(rule.byweekno.clone())
730 .by_weekday(weekdays)
731 .by_hour(if hours.is_empty() {
732 rule.byhour.clone()
733 } else {
734 Vec::new()
735 })
736 .by_minute(if minutes.is_empty() {
737 rule.byminute.clone()
738 } else {
739 Vec::new()
740 })
741 .by_second(rule.bysecond.clone());
742 // COUNT and UNTIL together are invalid. UNTIL is the safer bound.
743 let count = rule.count.filter(|_| until.is_none());
744 if let Some(n) = count.filter(|_| !filtered) {
745 r = r.count(n);
746 }
747 if let Some(u) = until {
748 r = r.until(wall(u));
749 }
750 r.build(wall(start))
751 .ok()
752 .filter(|_| valid)
753 // Stops a rule that never matches after 100k periods, not at year 9999.
754 .map(|set| (&set.limit()).into_iter())
755 .into_iter()
756 .flatten()
757 .map(|t| t.naive_utc())
758 .filter(move |t| {
759 (hours.is_empty() || hours.contains(&(t.hour() as u8)))
760 && (minutes.is_empty() || minutes.contains(&(t.minute() as u8)))
761 })
762 .take(
763 count
764 .filter(|_| filtered)
765 .map_or(usize::MAX, |n| n as usize),
766 )
767 .take_while(move |t| *t <= end)
768}
769