Files
Client/lib/utils/recurrence_occurrences.dart
T

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