Skip to content

Commit

Permalink
Fix BYWEEKNO recurrence evaluation; consider negative _BY_ values.
Browse files Browse the repository at this point in the history
  • Loading branch information
minichma committed Nov 26, 2024
1 parent db18285 commit 2bd69c4
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 3 deletions.
30 changes: 30 additions & 0 deletions Ical.Net/CalendarExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/// <summary>
/// Calculate the year, the given date's week belongs to according to ISO 8601, as required by RFC 5545.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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;
}

/// <summary>
/// Calculate the number of weeks in the given year according to ISO 8601, as required by RFC 5545.
/// </summary>
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 28th of December (in Gregorian Calendar).
var testTime = new DateTime(year + 1, 1, 1).AddDays(-4);
return calendar.GetIso8601WeekOfYear(testTime, firstDayOfWeek);
}
}
19 changes: 16 additions & 3 deletions Ical.Net/Evaluation/RecurrencePatternEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ private List<DateTime> GetWeekNoVariants(List<DateTime> dates, RecurrencePattern
var weekNoDates = new List<DateTime>();
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
Expand Down Expand Up @@ -429,6 +429,17 @@ private List<DateTime> GetWeekNoVariants(List<DateTime> dates, RecurrencePattern
return weekNoDates;
}

/// <summary>
/// Normalize the BYWEEKNO values to be positive integers.
/// </summary>
private List<int> GetByWeekNoForYearNormalized(RecurrencePattern pattern, int year)
{
var weeksInYear = new Lazy<int>(() => Calendar.GetIso8601WeeksInYear(year, pattern.FirstDayOfWeek));
return pattern.ByWeekNo
.Select(weekNo => weekNo >= 0 ? weekNo : weeksInYear.Value + weekNo + 1)
.ToList();
}

/// <summary>
/// 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.
Expand Down Expand Up @@ -641,13 +652,14 @@ private List<DateTime> 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);
Expand All @@ -668,11 +680,12 @@ private List<DateTime> 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);
Expand Down

0 comments on commit 2bd69c4

Please sign in to comment.