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