From 8646af1f4f09c0a39eb73d5d5c0bc5209d8138bd Mon Sep 17 00:00:00 2001 From: Jonas Wanke Date: Tue, 4 Jan 2022 13:01:03 +0100 Subject: [PATCH] fix: handle yearly frequency with byMonths and byWeekDays Closes: #29 --- lib/src/iteration/date_set_filtering.dart | 97 ++++++++++++++--------- lib/src/utils.dart | 4 + test/recurrence_rule_test.dart | 19 +++++ 3 files changed, 81 insertions(+), 39 deletions(-) diff --git a/lib/src/iteration/date_set_filtering.dart b/lib/src/iteration/date_set_filtering.dart index e5be6fe..4a093c9 100644 --- a/lib/src/iteration/date_set_filtering.dart +++ b/lib/src/iteration/date_set_filtering.dart @@ -70,16 +70,19 @@ bool _isFilteredByWeekDays(RecurrenceRule rrule, DateTime date) { final dayOfWeek = date.weekday; final relevantByWeekDays = rrule.byWeekDays.where((e) => e.day == dayOfWeek); - final genericByWeekDays = relevantByWeekDays.where((e) => e.hasNoOccurrence); + // MO, TU, etc. match + final genericByWeekDays = relevantByWeekDays.where((e) => e.hasNoOccurrence); if (genericByWeekDays.isNotEmpty) return false; // +3TU, -51TH, etc. match - final specificByWeekDays = - relevantByWeekDays.where((e) => e.hasOccurrence).map((e) => e.occurrence); + final specificByWeekDays = relevantByWeekDays + .where((e) => e.hasOccurrence) + .map((e) => e.occurrence!) + .toSet(); if (specificByWeekDays.isEmpty) return true; - if (rrule.frequency == Frequency.yearly && rrule.byMonths.isEmpty) { + if (rrule.frequency == Frequency.yearly) { assert( rrule.byWeeks.isEmpty, '"[…], the BYDAY rule part MUST NOT be specified with a numeric ' @@ -88,43 +91,28 @@ bool _isFilteredByWeekDays(RecurrenceRule rrule, DateTime date) { '— https://tools.ietf.org/html/rfc5545#section-3.3.10', ); - var current = DateTimeRrule.date(date.year, 1, 1).nextOrSame(dayOfWeek); - var occurrence = 1; - while (current != date) { - current = current + 1.weeks; - occurrence++; - } - - var totalOccurrences = occurrence - 1; - while (current.year == date.year) { - current = current + 1.weeks; - totalOccurrences++; - } - final negativeOccurrence = occurrence - 1 - totalOccurrences; - - if (!specificByWeekDays.contains(occurrence) && - !specificByWeekDays.contains(negativeOccurrence)) { - return true; + if (rrule.byMonths.isEmpty) { + return _doesOccurrenceMatch( + specificByWeekDays, + date.firstDayOfYear, + date.lastDayOfYear, + date, + ); + } else { + return _doesOccurrenceMatch( + specificByWeekDays, + date.firstDayOfMonth, + date.lastDayOfMonth, + date, + ); } } else if (rrule.frequency == Frequency.monthly) { - var current = date.firstDayOfMonth.nextOrSame(dayOfWeek); - var occurrence = 1; - while (current != date) { - current = current + 1.weeks; - occurrence++; - } - - var totalOccurrences = occurrence - 1; - while (current.month == date.month) { - current = current + 1.weeks; - totalOccurrences++; - } - final negativeOccurrence = occurrence - 1 - totalOccurrences; - - if (!specificByWeekDays.contains(occurrence) && - !specificByWeekDays.contains(negativeOccurrence)) { - return true; - } + return _doesOccurrenceMatch( + specificByWeekDays, + date.firstDayOfMonth, + date.lastDayOfMonth, + date, + ); } else { assert( false, @@ -137,6 +125,37 @@ bool _isFilteredByWeekDays(RecurrenceRule rrule, DateTime date) { return false; } +bool _doesOccurrenceMatch( + Set occurrences, + DateTime firstDateWithinPeriod, + DateTime lastDateWithinPeriod, + DateTime date, +) { + assert(firstDateWithinPeriod <= date); + assert(lastDateWithinPeriod >= date); + + var current = firstDateWithinPeriod.nextOrSame(date.weekday); + var occurrence = 1; + while (current < date) { + current = current + 1.weeks; + occurrence++; + } + assert(current == date); + + var totalOccurrences = occurrence - 1; + while (current <= lastDateWithinPeriod) { + current = current + 1.weeks; + totalOccurrences++; + } + final negativeOccurrence = occurrence - 1 - totalOccurrences; + + if (!occurrences.contains(occurrence) && + !occurrences.contains(negativeOccurrence)) { + return true; + } + return false; +} + bool _isFilteredByMonthDays(RecurrenceRule rrule, DateTime date) { assert(date.isValidRruleDate); diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 2b23a9b..0d9015f 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,7 +1,9 @@ import 'dart:math' as math; + import 'package:supercharged_dart/supercharged_dart.dart'; import 'utils/week.dart'; + export 'utils/week.dart'; /// Combines the [Object.hashCode] values of an arbitrary number of objects @@ -106,6 +108,8 @@ extension DateTimeRrule on DateTime { DateTime plusYears(int years) => plusYearsAndMonths(years: years); DateTime plusMonths(int months) => plusYearsAndMonths(months: months); + DateTime get firstDayOfYear => atStartOfDay.copyWith(month: 1, day: 1); + DateTime get lastDayOfYear => atStartOfDay.copyWith(month: 12, day: 31); DateTime get firstDayOfMonth => atStartOfDay.copyWith(day: 1); DateTime get lastDayOfMonth => plusMonths(1).firstDayOfMonth - 1.days; int get daysInMonth { diff --git a/test/recurrence_rule_test.dart b/test/recurrence_rule_test.dart index 4dc56e1..d72f9ce 100644 --- a/test/recurrence_rule_test.dart +++ b/test/recurrence_rule_test.dart @@ -1,5 +1,6 @@ import 'package:rrule/rrule.dart'; import 'package:rrule/src/cache.dart'; +import 'package:rrule/src/utils.dart'; import 'package:test/test.dart'; void main() { @@ -131,4 +132,22 @@ void main() { throwsA(isA()), ); }); + test( + "#29: getting instances for rrule yearly, 'every 2nd tuesday of January' fails", + () { + const rruleString = + 'RRULE:FREQ=YEARLY;COUNT=4;INTERVAL=1;BYDAY=2TU;BYMONTH=1'; + final rrule = RecurrenceRule( + frequency: Frequency.yearly, + count: 4, + interval: 1, + byMonths: {1}, + byWeekDays: {ByWeekDayEntry(DateTime.tuesday, 2)}, + ); + expect(RecurrenceRule.fromString(rruleString), rrule); + + final instances = + rrule.getAllInstances(start: DateTimeRrule.date(2022, 1, 1)); + expect(instances.length, 4); + }); }