diff --git a/lib/src/cache.dart b/lib/src/cache.dart new file mode 100644 index 0000000..1bd3d6e --- /dev/null +++ b/lib/src/cache.dart @@ -0,0 +1,58 @@ +import 'package:meta/meta.dart'; + +import 'utils.dart'; + +@immutable +class CacheKey { + const CacheKey({ + required this.start, + this.after, + this.includeAfter = false, + this.before, + this.includeBefore = false, + }); + + final DateTime start; + final DateTime? after; + final bool includeAfter; + final DateTime? before; + final bool includeBefore; + + @override + int get hashCode { + return hashList([ + start, + after, + includeAfter, + before, + includeBefore, + ]); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + + return other is CacheKey && + other.after == after && + other.includeAfter == includeAfter && + other.before == before && + other.includeBefore == includeBefore; + } +} + +class Cache { + final Map> _results = {}; + + @visibleForTesting + Map> get results => _results; + + void add(CacheKey key, List data) { + _results[key]= data; + } + + List? get(CacheKey key) { + return _results[key]; + } +} diff --git a/lib/src/iteration/iteration.dart b/lib/src/iteration/iteration.dart index f07fa05..47ca737 100644 --- a/lib/src/iteration/iteration.dart +++ b/lib/src/iteration/iteration.dart @@ -17,8 +17,16 @@ import 'time_set.dart'; Iterable getRecurrenceRuleInstances( RecurrenceRule rrule, { required DateTime start, + DateTime? after, + bool includeAfter = false, + DateTime? before, + bool includeBefore = false, }) sync* { assert(start.isValidRruleDateTime); + assert(after.isValidRruleDateTime); + assert(before.isValidRruleDateTime); + if (after != null) assert(after >= start); + if (before != null) assert(before >= start); rrule = _prepare(rrule, start); @@ -44,7 +52,16 @@ Iterable getRecurrenceRuleInstances( for (final result in results) { if (rrule.until != null && result > rrule.until!) return; + if (before != null) { + if (!includeBefore && result >= before) return; + if (includeBefore && result > before) return; + } + if (result < start) continue; + if (after != null) { + if (!includeAfter && result <= after) continue; + if (includeAfter && result < after) continue; + } yield result; if (count != null) { diff --git a/lib/src/recurrence_rule.dart b/lib/src/recurrence_rule.dart index ed4cffd..0896bfd 100644 --- a/lib/src/recurrence_rule.dart +++ b/lib/src/recurrence_rule.dart @@ -1,9 +1,11 @@ import 'dart:collection'; +import 'dart:math'; import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'by_week_day_entry.dart'; +import 'cache.dart'; import 'codecs/string/decoder.dart'; import 'codecs/string/encoder.dart'; import 'codecs/string/string.dart'; @@ -31,6 +33,7 @@ class RecurrenceRule { Set byMonths = const {}, Set bySetPositions = const {}, this.weekStart, + this.shouldCacheResults = false, }) : assert(count == null || count >= 1), assert(until.isValidRruleDateTime), assert(until == null || count == null), @@ -167,9 +170,65 @@ class RecurrenceRule { /// Returns [weekStart] or [DateTime.monday] if that is not set. int get actualWeekStart => weekStart ?? DateTime.monday; - Iterable getInstances({required DateTime start}) { + final bool shouldCacheResults; + + final Cache _cache = Cache(); + + @visibleForTesting + Cache get cache => _cache; + + Iterable getInstances({ + required DateTime start, + DateTime? after, + bool includeAfter = false, + DateTime? before, + bool includeBefore = false, + }) { assert(start.isValidRruleDateTime); - return getRecurrenceRuleInstances(this, start: start); + assert(after.isValidRruleDateTime); + assert(before.isValidRruleDateTime); + + return getRecurrenceRuleInstances( + this, + start: start, + after: after, + includeAfter: includeAfter, + before: before, + includeBefore: includeBefore, + ); + } + + List getAllInstances({ + required DateTime start, + DateTime? after, + bool includeAfter = false, + DateTime? before, + bool includeBefore = false, + }) { + final key = CacheKey( + start: start, + after: after, + includeAfter: includeAfter, + before: before, + includeBefore: includeBefore, + ); + + final fromCache = _cache.get(key); + if (fromCache != null) return fromCache; + + final results = getInstances( + start: start, + after: after, + includeAfter: includeAfter, + before: before, + includeBefore: includeBefore, + ).toList(growable: false); + + if (shouldCacheResults) { + _cache.add(key, results); + } + + return results; } @override diff --git a/test/cache_test.dart b/test/cache_test.dart new file mode 100644 index 0000000..b7e49f8 --- /dev/null +++ b/test/cache_test.dart @@ -0,0 +1,22 @@ +import 'package:rrule/src/cache.dart'; +import 'package:test/test.dart'; + +void main() { + test('should add data to the cache', () { + final cache = Cache(); + final key = CacheKey(start: DateTime.now()); + final results = []; + cache.add(key, results); + + expect(cache.results, containsPair(key, results)); + }); + + test('should return data from the cache by key', () { + final cache = Cache(); + final key = CacheKey(start: DateTime.now()); + final results = []; + cache.add(key, results); + + expect(cache.get(key), results); + }); +} diff --git a/test/recurrence_rule_test.dart b/test/recurrence_rule_test.dart index c08da34..4dc56e1 100644 --- a/test/recurrence_rule_test.dart +++ b/test/recurrence_rule_test.dart @@ -1,4 +1,5 @@ import 'package:rrule/rrule.dart'; +import 'package:rrule/src/cache.dart'; import 'package:test/test.dart'; void main() { @@ -49,6 +50,79 @@ void main() { }); }); + group('cache', () { + test('should store instances in the cache', () { + final rrule = RecurrenceRule( + frequency: Frequency.monthly, + until: DateTime.utc(2021), + shouldCacheResults: true, + ); + final instances = rrule.getAllInstances(start: DateTime.utc(2020)); + + expect(rrule.cache.get(CacheKey(start: DateTime.utc(2020))), instances); + }); + }); + + group('getAllInstances', () { + test('should support date after inclusive', () { + final rrule = RecurrenceRule( + frequency: Frequency.monthly, + until: DateTime.utc(2021), + ); + + final instances = rrule.getAllInstances( + start: DateTime.utc(2020), + after: DateTime.utc(2020, 5), + includeAfter: true, + ); + + expect(instances.first, DateTime.utc(2020, 5)); + }); + + test('should support date after exclusive', () { + final rrule = RecurrenceRule( + frequency: Frequency.monthly, + until: DateTime.utc(2021), + ); + + final instances = rrule.getAllInstances( + start: DateTime.utc(2020), + after: DateTime.utc(2020, 5), + ); + + expect(instances.first, DateTime.utc(2020, 6)); + }); + + test('should support date before inclusive', () { + final rrule = RecurrenceRule( + frequency: Frequency.monthly, + until: DateTime.utc(2021), + ); + + final instances = rrule.getAllInstances( + start: DateTime.utc(2020), + before: DateTime.utc(2020, 5), + includeBefore: true, + ); + + expect(instances.last, DateTime.utc(2020, 5)); + }); + + test('should support date before exclusive', () { + final rrule = RecurrenceRule( + frequency: Frequency.monthly, + until: DateTime.utc(2021), + ); + + final instances = rrule.getAllInstances( + start: DateTime.utc(2020), + before: DateTime.utc(2020, 5), + ); + + expect(instances.last, DateTime.utc(2020, 4)); + }); + }); + test( '#19: No warning for creating a RRULE with BYWEEKNO, but with non-YEARLY frequency', () {