expandRecurrence function

Iterable<DateTime> expandRecurrence(
  1. RecurrenceRule rule,
  2. DateTime start, {
  3. int? limit,
})

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;
    }
  }
}