76 lines
2.3 KiB
Dart
76 lines
2.3 KiB
Dart
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<DateTime> 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<DateTime> _iterator;
|
|
final List<DateTime> _found = [];
|
|
bool _exhausted = false;
|
|
|
|
_Series(RecurrenceRule rule, DateTime anchorUtc)
|
|
: _iterator = rule.getInstances(start: anchorUtc).iterator;
|
|
|
|
List<DateTime> 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;
|
|
}
|
|
}
|