Skip to content

Commit 7cb2fa3

Browse files
authored
Recurrence evaluation: Fix calculation of week numbering. (#652)
* Test: Restore tes tcase from `icalrecur_test.out` and fix its `DTSTART` according to libical/libical#823. * Recurrence evaluation: Fix calculation of week numbering.
1 parent dad1c27 commit 7cb2fa3

File tree

3 files changed

+36
-26
lines changed

3 files changed

+36
-26
lines changed

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -357,11 +357,10 @@ START-AT:20210302T100000
357357
INSTANCES:20210302T102000,20210302T112000,20210302T122000,20210302T132000,20210302T142000,20210302T152000,20210302T162000,20210302T172000,20210302T182000,20210302T192000,20210302T202000,20210302T212000,20210302T222000,20210302T232000
358358
PREV-INSTANCES:20210302T092000,20210302T082000,20210302T072000,20210302T062000,20210302T052000,20210302T042000,20210302T032000,20210302T022000,20210302T012000,20210302T002000,20210301T232000,20210301T222000,20210301T212000,20210301T202000,20210301T192000,20210301T182000,20210301T172000,20210301T162000,20210301T152000,20210301T142000
359359

360-
# TODO: FIX (see https://github.com/ical-org/ical.net/issues/618)
361-
# RRULE:FREQ=YEARLY;BYWEEKNO=6;BYDAY=TU;WKST=TH;UNTIL=20210612T000000Z
362-
# DTSTART:20180206T080001
363-
# INSTANCES:20180213T080001,20190212T080001,20200211T080001,20210209T080001
364-
# PREV-INSTANCES:20210209T080001,20200211T080001,20190212T080001,20180213T080001
360+
RRULE:FREQ=YEARLY;BYWEEKNO=6;BYDAY=TU;WKST=TH;UNTIL=20210612T000000Z
361+
DTSTART:20180213T080001
362+
INSTANCES:20180213T080001,20190212T080001,20200211T080001,20210209T080001
363+
PREV-INSTANCES:20210209T080001,20200211T080001,20190212T080001,20180213T080001
365364

366365
RRULE:FREQ=DAILY;BYMINUTE=1,2,3,4;INTERVAL=2;COUNT=3
367366
DTSTART:20241018

Ical.Net/CalendarExtensions.cs

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,32 @@ namespace Ical.Net;
1111
public static class CalendarExtensions
1212
{
1313
/// <summary>
14-
/// https://blogs.msdn.microsoft.com/shawnste/2006/01/24/iso-8601-week-of-year-format-in-microsoft-net/
14+
/// Calculate the week number according to ISO.8601, as required by RFC 5545.
1515
/// </summary>
16-
public static int GetIso8601WeekOfYear(this System.Globalization.Calendar calendar, DateTime time, CalendarWeekRule rule, DayOfWeek firstDayOfWeek)
16+
public static int GetIso8601WeekOfYear(this System.Globalization.Calendar calendar, DateTime time, DayOfWeek firstDayOfWeek)
1717
{
18-
// Seriously cheat. If its Monday, Tuesday or Wednesday, then it'll
19-
// be the same week# as whatever Thursday, Friday or Saturday are,
20-
// and we always get those right
21-
var day = calendar.GetDayOfWeek(time);
22-
if (day >= DayOfWeek.Monday && day <= DayOfWeek.Wednesday)
23-
{
24-
time = time.AddDays(3);
25-
}
18+
// A week is defined as a
19+
// seven day period, starting on the day of the week defined to be
20+
// the week start(see WKST). Week number one of the calendar year
21+
// is the first week that contains at least four (4) days in that
22+
// calendar year.
2623

27-
// Return the week of our adjusted day
28-
return calendar.GetWeekOfYear(time, rule, firstDayOfWeek);
24+
// We add 3 to make sure the test date is in the 'right' year, because
25+
// otherwise we might end up with week 53 in a year that only has 52.
26+
var tTest = GetStartOfWeek(time, firstDayOfWeek).AddDays(3);
27+
var res = calendar.GetWeekOfYear(tTest, CalendarWeekRule.FirstFourDayWeek, firstDayOfWeek);
28+
29+
return res;
30+
}
31+
32+
/// <summary>
33+
/// Calculate and return the date that represents the first day of the week the given date is
34+
/// in, according to the week numbering required by RFC 5545.
35+
/// </summary>
36+
private static DateTime GetStartOfWeek(this DateTime t, DayOfWeek firstDayOfWeek)
37+
{
38+
var t0 = ((int) firstDayOfWeek) % 7;
39+
var tn = ((int) t.DayOfWeek) % 7;
40+
return t.AddDays(-((tn + 7 - t0) % 7));
2941
}
30-
}
42+
}

Ical.Net/Evaluation/RecurrencePatternEvaluator.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
using System;
77
using System.Collections.Generic;
8-
using System.Globalization;
98
using System.Linq;
109
using Ical.Net.DataTypes;
1110
using Ical.Net.Utility;
@@ -400,15 +399,15 @@ private List<DateTime> GetWeekNoVariants(List<DateTime> dates, RecurrencePattern
400399
{
401400
var date = t;
402401
// Determine our current week number
403-
var currWeekNo = Calendar.GetIso8601WeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, pattern.FirstDayOfWeek);
402+
var currWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek);
404403
while (currWeekNo > weekNo)
405404
{
406405
// If currWeekNo > weekNo, then we're likely at the start of a year
407406
// where currWeekNo could be 52 or 53. If we simply step ahead 7 days
408407
// we should be back to week 1, where we can easily make the calculation
409408
// to move to weekNo.
410409
date = date.AddDays(7);
411-
currWeekNo = Calendar.GetIso8601WeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, pattern.FirstDayOfWeek);
410+
currWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek);
412411
}
413412

414413
// Move ahead to the correct week of the year
@@ -629,7 +628,7 @@ private List<DateTime> GetAbsWeekDays(DateTime date, WeekDay weekDay, Recurrence
629628
}
630629
else if (pattern.Frequency == FrequencyType.Weekly || pattern.ByWeekNo.Count > 0)
631630
{
632-
var weekNo = Calendar.GetIso8601WeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, pattern.FirstDayOfWeek);
631+
var weekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek);
633632

634633
// Go to the first day of the week
635634
date = date.AddDays(-GetWeekDayOffset(date, pattern.FirstDayOfWeek));
@@ -640,8 +639,8 @@ private List<DateTime> GetAbsWeekDays(DateTime date, WeekDay weekDay, Recurrence
640639
date = date.AddDays(1);
641640
}
642641

643-
var nextWeekNo = Calendar.GetIso8601WeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, pattern.FirstDayOfWeek);
644-
var currentWeekNo = Calendar.GetIso8601WeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, pattern.FirstDayOfWeek);
642+
var nextWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek);
643+
var currentWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek);
645644

646645
//When we manage weekly recurring pattern and we have boundary case:
647646
//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.
@@ -655,7 +654,7 @@ private List<DateTime> GetAbsWeekDays(DateTime date, WeekDay weekDay, Recurrence
655654
}
656655

657656
date = date.AddDays(7);
658-
currentWeekNo = Calendar.GetIso8601WeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, pattern.FirstDayOfWeek);
657+
currentWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek);
659658
}
660659
}
661660
else if (pattern.Frequency == FrequencyType.Monthly || pattern.ByMonth.Count > 0)
@@ -671,7 +670,7 @@ private List<DateTime> GetAbsWeekDays(DateTime date, WeekDay weekDay, Recurrence
671670

672671
while (date.Month == month)
673672
{
674-
var currentWeekNo = Calendar.GetIso8601WeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, pattern.FirstDayOfWeek);
673+
var currentWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek);
675674

676675
if ((pattern.ByWeekNo.Count == 0 || pattern.ByWeekNo.Contains(currentWeekNo))
677676
&& (pattern.ByMonth.Count == 0 || pattern.ByMonth.Contains(date.Month)))

0 commit comments

Comments
 (0)