Skip to content

Commit 2419348

Browse files
authored
Recurrence evaluation: Implement support for negative BYWEEKNO values. (ical-org#654)
* Test: Restore test `RRULE:FREQ=YEARLY;BYWEEKNO=1,2,-1,-2;BYDAY=TU;UNTIL=20170101T000000Z` and associated test failure. * Fix BYWEEKNO recurrence evaluation; consider negative _BY_ values.
1 parent 7cb2fa3 commit 2419348

File tree

3 files changed

+50
-8
lines changed

3 files changed

+50
-8
lines changed

Ical.Net.Tests/contrib/libical/icalrecur_test.out

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -340,11 +340,10 @@ DTSTART:20190101T100000
340340
INSTANCES:20190102T100000,20190104T100000,20190116T100000,20190118T100000
341341
PREV-INSTANCES:20190116T100000,20190104T100000,20190102T100000
342342

343-
# TODO: FIX (see https://github.com/ical-org/ical.net/issues/618)
344-
# RRULE:FREQ=YEARLY;BYWEEKNO=1,2,-1,-2;BYDAY=TU;UNTIL=20170101T000000Z
345-
# DTSTART:20130101T000000
346-
# INSTANCES:20130101T000000,20130108T000000,20131217T000000,20131224T000000,20131231T000000,20140107T000000,20141216T000000,20141223T000000,20141230T000000,20150106T000000,20151222T000000,20151229T000000,20160105T000000,20160112T000000,20161220T000000,20161227T000000
347-
# PREV-INSTANCES:20161227T000000,20161220T000000,20160112T000000,20160105T000000,20151229T000000,20151222T000000,20150106T000000,20141230T000000,20141223T000000,20141216T000000,20140107T000000,20131231T000000,20131224T000000,20131217T000000,20130108T000000,20130101T000000
343+
RRULE:FREQ=YEARLY;BYWEEKNO=1,2,-1,-2;BYDAY=TU;UNTIL=20170101T000000Z
344+
DTSTART:20130101T000000
345+
INSTANCES:20130101T000000,20130108T000000,20131217T000000,20131224T000000,20131231T000000,20140107T000000,20141216T000000,20141223T000000,20141230T000000,20150106T000000,20151222T000000,20151229T000000,20160105T000000,20160112T000000,20161220T000000,20161227T000000
346+
PREV-INSTANCES:20161227T000000,20161220T000000,20160112T000000,20160105T000000,20151229T000000,20151222T000000,20150106T000000,20141230T000000,20141223T000000,20141216T000000,20140107T000000,20131231T000000,20131224T000000,20131217T000000,20130108T000000,20130101T000000
348347

349348
RRULE:FREQ=YEARLY;BYWEEKNO=53;BYDAY=TU,SA;UNTIL=20170101T000000Z
350349
DTSTART:20130101T000000

Ical.Net/CalendarExtensions.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,34 @@ private static DateTime GetStartOfWeek(this DateTime t, DayOfWeek firstDayOfWeek
3939
var tn = ((int) t.DayOfWeek) % 7;
4040
return t.AddDays(-((tn + 7 - t0) % 7));
4141
}
42+
43+
/// <summary>
44+
/// Calculate the year, the given date's week belongs to according to ISO 8601, as required by RFC 5545.
45+
/// </summary>
46+
/// <remarks>
47+
/// A date's nominal year may be different from the year, the week belongs to that the date is in.
48+
/// I.e. the first and last week of the year may belong to a different year than the date's year.
49+
/// E.g. for `2019-12-31` with first day of the week being Monday, the method will return 2020,
50+
/// because the week that contains `2019-12-31` is the first week of 2020.
51+
/// </remarks>
52+
public static int GetIso8601YearOfWeek(this System.Globalization.Calendar calendar, DateTime time, DayOfWeek firstDayOfWeek)
53+
{
54+
var year = time.Year;
55+
if ((time.Month >= 12) && (calendar.GetIso8601WeekOfYear(time, firstDayOfWeek) == 1))
56+
year++;
57+
else if ((time.Month == 1) && (calendar.GetIso8601WeekOfYear(time, firstDayOfWeek) >= 52))
58+
year--;
59+
60+
return year;
61+
}
62+
63+
/// <summary>
64+
/// Calculate the number of weeks in the given year according to ISO 8601, as required by RFC 5545.
65+
/// </summary>
66+
public static int GetIso8601WeeksInYear(this System.Globalization.Calendar calendar, int year, DayOfWeek firstDayOfWeek)
67+
{
68+
// The last week of the year is the week that contains the 4th-last day of the year (which is the 28th of December in Gregorian Calendar).
69+
var testTime = new DateTime(year + 1, 1, 1, 0, 0, 0, DateTimeKind.Unspecified).AddDays(-4);
70+
return calendar.GetIso8601WeekOfYear(testTime, firstDayOfWeek);
71+
}
4272
}

Ical.Net/Evaluation/RecurrencePatternEvaluator.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ private List<DateTime> GetWeekNoVariants(List<DateTime> dates, RecurrencePattern
395395
var weekNoDates = new List<DateTime>();
396396
foreach (var t in dates)
397397
{
398-
foreach (var weekNo in pattern.ByWeekNo)
398+
foreach (var weekNo in GetByWeekNoForYearNormalized(pattern, t.Year))
399399
{
400400
var date = t;
401401
// Determine our current week number
@@ -429,6 +429,17 @@ private List<DateTime> GetWeekNoVariants(List<DateTime> dates, RecurrencePattern
429429
return weekNoDates;
430430
}
431431

432+
/// <summary>
433+
/// Normalize the BYWEEKNO values to be positive integers.
434+
/// </summary>
435+
private List<int> GetByWeekNoForYearNormalized(RecurrencePattern pattern, int year)
436+
{
437+
var weeksInYear = new Lazy<int>(() => Calendar.GetIso8601WeeksInYear(year, pattern.FirstDayOfWeek));
438+
return pattern.ByWeekNo
439+
.Select(weekNo => weekNo >= 0 ? weekNo : weeksInYear.Value + weekNo + 1)
440+
.ToList();
441+
}
442+
432443
/// <summary>
433444
/// Applies BYYEARDAY rules specified in this Recur instance to the specified date list.
434445
/// If no BYYEARDAY rules are specified, the date list is returned unmodified.
@@ -641,13 +652,14 @@ private List<DateTime> GetAbsWeekDays(DateTime date, WeekDay weekDay, Recurrence
641652

642653
var nextWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek);
643654
var currentWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek);
655+
var byWeekNoNormalized = GetByWeekNoForYearNormalized(pattern, Calendar.GetIso8601YearOfWeek(date, pattern.FirstDayOfWeek));
644656

645657
//When we manage weekly recurring pattern and we have boundary case:
646658
//Weekdays: Dec 31, Jan 1, Feb 1, Mar 1, Apr 1, May 1, June 1, Dec 31 - It's the 53th week of the year, but all another are 1st week number.
647659
//So we need an EXRULE for this situation, but only for weekly events
648660
while (currentWeekNo == weekNo || (nextWeekNo < weekNo && currentWeekNo == nextWeekNo && pattern.Frequency == FrequencyType.Weekly))
649661
{
650-
if ((pattern.ByWeekNo.Count == 0 || pattern.ByWeekNo.Contains(currentWeekNo))
662+
if ((byWeekNoNormalized.Count == 0 || byWeekNoNormalized.Contains(currentWeekNo))
651663
&& (pattern.ByMonth.Count == 0 || pattern.ByMonth.Contains(date.Month)))
652664
{
653665
days.Add(date);
@@ -668,11 +680,12 @@ private List<DateTime> GetAbsWeekDays(DateTime date, WeekDay weekDay, Recurrence
668680
date = date.AddDays(1);
669681
}
670682

683+
var byWeekNoNormalized = GetByWeekNoForYearNormalized(pattern, Calendar.GetIso8601YearOfWeek(date, pattern.FirstDayOfWeek));
671684
while (date.Month == month)
672685
{
673686
var currentWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek);
674687

675-
if ((pattern.ByWeekNo.Count == 0 || pattern.ByWeekNo.Contains(currentWeekNo))
688+
if ((byWeekNoNormalized.Count == 0 || byWeekNoNormalized.Contains(currentWeekNo))
676689
&& (pattern.ByMonth.Count == 0 || pattern.ByMonth.Contains(date.Month)))
677690
{
678691
days.Add(date);

0 commit comments

Comments
 (0)