Skip to content

Commit

Permalink
fix: handle yearly frequency with byMonths and byWeekDays
Browse files Browse the repository at this point in the history
Closes: #29
  • Loading branch information
JonasWanke committed Jan 4, 2022
1 parent 40a314f commit 8646af1
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 39 deletions.
97 changes: 58 additions & 39 deletions lib/src/iteration/date_set_filtering.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 '
Expand All @@ -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,
Expand All @@ -137,6 +125,37 @@ bool _isFilteredByWeekDays(RecurrenceRule rrule, DateTime date) {
return false;
}

bool _doesOccurrenceMatch(
Set<int> 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);

Expand Down
4 changes: 4 additions & 0 deletions lib/src/utils.dart
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 {
Expand Down
19 changes: 19 additions & 0 deletions test/recurrence_rule_test.dart
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -131,4 +132,22 @@ void main() {
throwsA(isA<AssertionError>()),
);
});
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);
});
}

0 comments on commit 8646af1

Please sign in to comment.