diff --git a/Ical.Net.Tests/contrib/libical/icalrecur_test.out b/Ical.Net.Tests/contrib/libical/icalrecur_test.out
index 87e3cbc73..8d957baef 100644
--- a/Ical.Net.Tests/contrib/libical/icalrecur_test.out
+++ b/Ical.Net.Tests/contrib/libical/icalrecur_test.out
@@ -340,11 +340,10 @@ DTSTART:20190101T100000
INSTANCES:20190102T100000,20190104T100000,20190116T100000,20190118T100000
PREV-INSTANCES:20190116T100000,20190104T100000,20190102T100000
-# TODO: FIX (see https://github.com/ical-org/ical.net/issues/618)
-# RRULE:FREQ=YEARLY;BYWEEKNO=1,2,-1,-2;BYDAY=TU;UNTIL=20170101T000000Z
-# DTSTART:20130101T000000
-# INSTANCES:20130101T000000,20130108T000000,20131217T000000,20131224T000000,20131231T000000,20140107T000000,20141216T000000,20141223T000000,20141230T000000,20150106T000000,20151222T000000,20151229T000000,20160105T000000,20160112T000000,20161220T000000,20161227T000000
-# PREV-INSTANCES:20161227T000000,20161220T000000,20160112T000000,20160105T000000,20151229T000000,20151222T000000,20150106T000000,20141230T000000,20141223T000000,20141216T000000,20140107T000000,20131231T000000,20131224T000000,20131217T000000,20130108T000000,20130101T000000
+RRULE:FREQ=YEARLY;BYWEEKNO=1,2,-1,-2;BYDAY=TU;UNTIL=20170101T000000Z
+DTSTART:20130101T000000
+INSTANCES:20130101T000000,20130108T000000,20131217T000000,20131224T000000,20131231T000000,20140107T000000,20141216T000000,20141223T000000,20141230T000000,20150106T000000,20151222T000000,20151229T000000,20160105T000000,20160112T000000,20161220T000000,20161227T000000
+PREV-INSTANCES:20161227T000000,20161220T000000,20160112T000000,20160105T000000,20151229T000000,20151222T000000,20150106T000000,20141230T000000,20141223T000000,20141216T000000,20140107T000000,20131231T000000,20131224T000000,20131217T000000,20130108T000000,20130101T000000
RRULE:FREQ=YEARLY;BYWEEKNO=53;BYDAY=TU,SA;UNTIL=20170101T000000Z
DTSTART:20130101T000000
diff --git a/Ical.Net/CalendarExtensions.cs b/Ical.Net/CalendarExtensions.cs
index 6848ef293..8b0007f47 100644
--- a/Ical.Net/CalendarExtensions.cs
+++ b/Ical.Net/CalendarExtensions.cs
@@ -39,4 +39,34 @@ private static DateTime GetStartOfWeek(this DateTime t, DayOfWeek firstDayOfWeek
var tn = ((int) t.DayOfWeek) % 7;
return t.AddDays(-((tn + 7 - t0) % 7));
}
+
+ ///
+ /// Calculate the year, the given date's week belongs to according to ISO 8601, as required by RFC 5545.
+ ///
+ ///
+ /// A date's nominal year may be different from the year, the week belongs to that the date is in.
+ /// I.e. the first and last week of the year may belong to a different year than the date's year.
+ /// E.g. for `2019-12-31` with first day of the week being Monday, the method will return 2020,
+ /// because the week that contains `2019-12-31` is the first week of 2020.
+ ///
+ public static int GetIso8601YearOfWeek(this System.Globalization.Calendar calendar, DateTime time, DayOfWeek firstDayOfWeek)
+ {
+ var year = time.Year;
+ if ((time.Month >= 12) && (calendar.GetIso8601WeekOfYear(time, firstDayOfWeek) == 1))
+ year++;
+ else if ((time.Month == 1) && (calendar.GetIso8601WeekOfYear(time, firstDayOfWeek) >= 52))
+ year--;
+
+ return year;
+ }
+
+ ///
+ /// Calculate the number of weeks in the given year according to ISO 8601, as required by RFC 5545.
+ ///
+ public static int GetIso8601WeeksInYear(this System.Globalization.Calendar calendar, int year, DayOfWeek firstDayOfWeek)
+ {
+ // 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).
+ var testTime = new DateTime(year + 1, 1, 1, 0, 0, 0, DateTimeKind.Unspecified).AddDays(-4);
+ return calendar.GetIso8601WeekOfYear(testTime, firstDayOfWeek);
+ }
}
diff --git a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs
index 118bdbc0d..376332467 100644
--- a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs
+++ b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs
@@ -395,7 +395,7 @@ private List GetWeekNoVariants(List dates, RecurrencePattern
var weekNoDates = new List();
foreach (var t in dates)
{
- foreach (var weekNo in pattern.ByWeekNo)
+ foreach (var weekNo in GetByWeekNoForYearNormalized(pattern, t.Year))
{
var date = t;
// Determine our current week number
@@ -429,6 +429,17 @@ private List GetWeekNoVariants(List dates, RecurrencePattern
return weekNoDates;
}
+ ///
+ /// Normalize the BYWEEKNO values to be positive integers.
+ ///
+ private List GetByWeekNoForYearNormalized(RecurrencePattern pattern, int year)
+ {
+ var weeksInYear = new Lazy(() => Calendar.GetIso8601WeeksInYear(year, pattern.FirstDayOfWeek));
+ return pattern.ByWeekNo
+ .Select(weekNo => weekNo >= 0 ? weekNo : weeksInYear.Value + weekNo + 1)
+ .ToList();
+ }
+
///
/// Applies BYYEARDAY rules specified in this Recur instance to the specified date list.
/// If no BYYEARDAY rules are specified, the date list is returned unmodified.
@@ -641,13 +652,14 @@ private List GetAbsWeekDays(DateTime date, WeekDay weekDay, Recurrence
var nextWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek);
var currentWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek);
+ var byWeekNoNormalized = GetByWeekNoForYearNormalized(pattern, Calendar.GetIso8601YearOfWeek(date, pattern.FirstDayOfWeek));
//When we manage weekly recurring pattern and we have boundary case:
//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.
//So we need an EXRULE for this situation, but only for weekly events
while (currentWeekNo == weekNo || (nextWeekNo < weekNo && currentWeekNo == nextWeekNo && pattern.Frequency == FrequencyType.Weekly))
{
- if ((pattern.ByWeekNo.Count == 0 || pattern.ByWeekNo.Contains(currentWeekNo))
+ if ((byWeekNoNormalized.Count == 0 || byWeekNoNormalized.Contains(currentWeekNo))
&& (pattern.ByMonth.Count == 0 || pattern.ByMonth.Contains(date.Month)))
{
days.Add(date);
@@ -668,11 +680,12 @@ private List GetAbsWeekDays(DateTime date, WeekDay weekDay, Recurrence
date = date.AddDays(1);
}
+ var byWeekNoNormalized = GetByWeekNoForYearNormalized(pattern, Calendar.GetIso8601YearOfWeek(date, pattern.FirstDayOfWeek));
while (date.Month == month)
{
var currentWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek);
- if ((pattern.ByWeekNo.Count == 0 || pattern.ByWeekNo.Contains(currentWeekNo))
+ if ((byWeekNoNormalized.Count == 0 || byWeekNoNormalized.Contains(currentWeekNo))
&& (pattern.ByMonth.Count == 0 || pattern.ByMonth.Contains(date.Month)))
{
days.Add(date);