Skip to content

Commit

Permalink
Recurrence evaluation: Implement support for negative BYWEEKNO valu…
Browse files Browse the repository at this point in the history
…es. (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.
  • Loading branch information
minichma authored Nov 27, 2024
1 parent 7cb2fa3 commit 2419348
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 8 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 @@ -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
Expand Down
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 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);
}
}
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 2419348

Please sign in to comment.