expandRecurrence function
Lazily yields the occurrences of rule at and after start, in ascending
order. limit caps the number emitted (in addition to any count/until
on the rule); the first limit reached wins.
Example:
final rule = parseRrule('FREQ=WEEKLY;BYDAY=MO,WE;COUNT=4');
expandRecurrence(rule, DateTime(2026, 1, 5)).toList();
// Mon Jan 5, Wed Jan 7, Mon Jan 12, Wed Jan 14
Audited: 2026-06-12 11:26 EDT
Implementation
Iterable<DateTime> expandRecurrence(
RecurrenceRule rule,
DateTime start, {
int? limit,
}) sync* {
int emitted = 0;
// Counts periods that produced nothing since the last yield. An IMPOSSIBLE
// rule (e.g. BYMONTHDAY=30;BYMONTH=2 — Feb 30 never exists) yields forever-
// nothing, so without this bound even `.take(n)` would hang. A long empty run
// with nothing emitted means no occurrence will ever appear. Valid infinite
// rules yield regularly, resetting the counter, so they still iterate forever.
int emptyPeriods = 0;
DateTime anchor = start;
// Walk one FREQ×INTERVAL period at a time; each period yields its sorted
// candidate dates. Periods advance monotonically forward, so the first
// candidate past `until` ends the whole sequence.
while (true) {
bool yieldedThisPeriod = false;
for (final DateTime occurrence in _candidatesFor(rule, start, anchor)) {
if (occurrence.isBefore(start)) {
continue;
}
final DateTime? until = rule.until;
if (until != null && occurrence.isAfter(until)) {
return;
}
yield occurrence;
emitted++;
yieldedThisPeriod = true;
if (_reachedLimit(rule, limit, emitted)) {
return;
}
}
anchor = _advanceAnchor(rule, anchor);
// Stop once the period itself is past `until` (an impossible rule never
// emits, so the per-occurrence `until` check above can never fire for it).
final DateTime? until = rule.until;
if (until != null && anchor.isAfter(until)) {
return;
}
if (yieldedThisPeriod) {
emptyPeriods = 0;
} else if (++emptyPeriods >= _maxEmptyPeriods) {
return;
}
}
}