import 'package:rrule/rrule.dart'; /// Memoised RRULE expansion. /// /// `RecurrenceRule.getInstances` always starts at the series anchor, so asking /// for one week of a daily event created two years ago walks ~750 occurrences /// (tens of ms each time on older phones) — and the calendar asks once per /// week and event, the home widget twice per publish. Here each series keeps /// its parsed rule, the occurrences found so far and the live iterator, so /// later queries only extend the expansion instead of restarting it. class RecurrenceOccurrences { RecurrenceOccurrences._(); static const int _maxSeries = 64; static final Map<(String, DateTime), _Series> _cache = {}; /// UTC occurrences of [rule] anchored at [anchorUtc] in /// `[fromUtc, toUtc)`, in ascending order. Throws like /// [RecurrenceRule.fromString] for an invalid rule. static List between( String rule, DateTime anchorUtc, DateTime fromUtc, DateTime toUtc, ) { final key = (rule, anchorUtc); var series = _cache.remove(key); series ??= _Series(RecurrenceRule.fromString(rule), anchorUtc); // Re-insert to keep the map in least-recently-used order. _cache[key] = series; if (_cache.length > _maxSeries) _cache.remove(_cache.keys.first); return series.between(fromUtc, toUtc); } /// Drops all memoised series (tests). static void clear() => _cache.clear(); } class _Series { final Iterator _iterator; final List _found = []; bool _exhausted = false; _Series(RecurrenceRule rule, DateTime anchorUtc) : _iterator = rule.getInstances(start: anchorUtc).iterator; List between(DateTime fromUtc, DateTime toUtc) { while (!_exhausted && (_found.isEmpty || _found.last.isBefore(toUtc))) { if (_iterator.moveNext()) { _found.add(_iterator.current); } else { _exhausted = true; } } final start = _lowerBound(fromUtc); return [ for (var i = start; i < _found.length && _found[i].isBefore(toUtc); i++) _found[i], ]; } int _lowerBound(DateTime value) { var low = 0; var high = _found.length; while (low < high) { final mid = (low + high) >> 1; if (_found[mid].isBefore(value)) { low = mid + 1; } else { high = mid; } } return low; } }