Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recurrence evaluation: Implement support for negative BYWEEKNO values. #654

Merged
merged 2 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

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
Loading