Skip to content

Commit

Permalink
Recurrence evaluation: Fix calculation of week numbering. (#652)
Browse files Browse the repository at this point in the history
* Test: Restore tes tcase from `icalrecur_test.out` and fix its `DTSTART` according to libical/libical#823.

* Recurrence evaluation: Fix calculation of week numbering.
  • Loading branch information
minichma authored Nov 25, 2024
1 parent dad1c27 commit 7cb2fa3
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 26 deletions.
9 changes: 4 additions & 5 deletions Ical.Net.Tests/contrib/libical/icalrecur_test.out
Original file line number Diff line number Diff line change
Expand Up @@ -357,11 +357,10 @@ START-AT:20210302T100000
INSTANCES:20210302T102000,20210302T112000,20210302T122000,20210302T132000,20210302T142000,20210302T152000,20210302T162000,20210302T172000,20210302T182000,20210302T192000,20210302T202000,20210302T212000,20210302T222000,20210302T232000
PREV-INSTANCES:20210302T092000,20210302T082000,20210302T072000,20210302T062000,20210302T052000,20210302T042000,20210302T032000,20210302T022000,20210302T012000,20210302T002000,20210301T232000,20210301T222000,20210301T212000,20210301T202000,20210301T192000,20210301T182000,20210301T172000,20210301T162000,20210301T152000,20210301T142000

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

RRULE:FREQ=DAILY;BYMINUTE=1,2,3,4;INTERVAL=2;COUNT=3
DTSTART:20241018
Expand Down
38 changes: 25 additions & 13 deletions Ical.Net/CalendarExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,32 @@ namespace Ical.Net;
public static class CalendarExtensions
{
/// <summary>
/// https://blogs.msdn.microsoft.com/shawnste/2006/01/24/iso-8601-week-of-year-format-in-microsoft-net/
/// Calculate the week number according to ISO.8601, as required by RFC 5545.
/// </summary>
public static int GetIso8601WeekOfYear(this System.Globalization.Calendar calendar, DateTime time, CalendarWeekRule rule, DayOfWeek firstDayOfWeek)
public static int GetIso8601WeekOfYear(this System.Globalization.Calendar calendar, DateTime time, DayOfWeek firstDayOfWeek)
{
// Seriously cheat. If its Monday, Tuesday or Wednesday, then it'll
// be the same week# as whatever Thursday, Friday or Saturday are,
// and we always get those right
var day = calendar.GetDayOfWeek(time);
if (day >= DayOfWeek.Monday && day <= DayOfWeek.Wednesday)
{
time = time.AddDays(3);
}
// A week is defined as a
// seven day period, starting on the day of the week defined to be
// the week start(see WKST). Week number one of the calendar year
// is the first week that contains at least four (4) days in that
// calendar year.

// Return the week of our adjusted day
return calendar.GetWeekOfYear(time, rule, firstDayOfWeek);
// We add 3 to make sure the test date is in the 'right' year, because
// otherwise we might end up with week 53 in a year that only has 52.
var tTest = GetStartOfWeek(time, firstDayOfWeek).AddDays(3);
var res = calendar.GetWeekOfYear(tTest, CalendarWeekRule.FirstFourDayWeek, firstDayOfWeek);

return res;
}

/// <summary>
/// Calculate and return the date that represents the first day of the week the given date is
/// in, according to the week numbering required by RFC 5545.
/// </summary>
private static DateTime GetStartOfWeek(this DateTime t, DayOfWeek firstDayOfWeek)
{
var t0 = ((int) firstDayOfWeek) % 7;
var tn = ((int) t.DayOfWeek) % 7;
return t.AddDays(-((tn + 7 - t0) % 7));
}
}
}
15 changes: 7 additions & 8 deletions Ical.Net/Evaluation/RecurrencePatternEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Ical.Net.DataTypes;
using Ical.Net.Utility;
Expand Down Expand Up @@ -400,15 +399,15 @@ private List<DateTime> GetWeekNoVariants(List<DateTime> dates, RecurrencePattern
{
var date = t;
// Determine our current week number
var currWeekNo = Calendar.GetIso8601WeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, pattern.FirstDayOfWeek);
var currWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek);
while (currWeekNo > weekNo)
{
// If currWeekNo > weekNo, then we're likely at the start of a year
// where currWeekNo could be 52 or 53. If we simply step ahead 7 days
// we should be back to week 1, where we can easily make the calculation
// to move to weekNo.
date = date.AddDays(7);
currWeekNo = Calendar.GetIso8601WeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, pattern.FirstDayOfWeek);
currWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek);
}

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

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

var nextWeekNo = Calendar.GetIso8601WeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, pattern.FirstDayOfWeek);
var currentWeekNo = Calendar.GetIso8601WeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, pattern.FirstDayOfWeek);
var nextWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek);
var currentWeekNo = Calendar.GetIso8601WeekOfYear(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.
Expand All @@ -655,7 +654,7 @@ private List<DateTime> GetAbsWeekDays(DateTime date, WeekDay weekDay, Recurrence
}

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

while (date.Month == month)
{
var currentWeekNo = Calendar.GetIso8601WeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, pattern.FirstDayOfWeek);
var currentWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek);

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

0 comments on commit 7cb2fa3

Please sign in to comment.