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