swiftlang / swift-foundation

The Foundation project
Apache License 2.0
2.39k stars 155 forks source link

Unexpected results with Calendar.RecurrenceRule #881

Open theoks opened 1 month ago

theoks commented 1 month ago

I tried to use the new Calendar.Recurrence objects and functionality, and I've bumped into some surprising (wrong?) output.

Given the following date range: 2024-01-01...2024-12-31, I used the .monthly recurrence rule to find every 5th Friday in the given range, using the strict matching rule.

The result was the (correct) following:

2024-03-29
2024-05-31
2024-08-30
2024-11-29

Now if I change the lower bound (start date) of the range to 2024-01-31 the result is unexpectedly the following:

2024-03-29
2024-05-31
// Missing date here
2024-08-30

The 2024-11-29 is missing from the output.

Output becomes even stranger if the calendar's firstWeekday is changed. If firstWeekday == 2, and the lower bound (start day) is 2024-01-01 then the output is the following:

2024-03-29
2024-05-31
2024-08-30
2024-09-27 // Month with only 4 Fridays
2024-11-29
2024-12-27 // Month with only 4 Fridays

If the lower bound (start date) is 2024-01-31 then the result is the following.

2024-03-29
2024-05-31
2024-08-30
// Missing date here
2024-12-27 // Month with only 4 Fridays

I believe that this is a bug, unless I am missing something in the way the algorithm works.

Some code if someone wants to quickly run some tests:

var calendar = Calendar(identifier: .gregorian)
// calendar.firstWeekday = 2

// let startComponents = DateComponents(year: 2024, month: 1, day: 1)
let startComponents = DateComponents(year: 2024, month: 1, day: 31)
let startDate = calendar.date(from: startComponents)!

let endComponents = DateComponents(year: 2024, month: 12, day: 31)
let endDate = calendar.date(from: endComponents)!

let rule = Calendar.RecurrenceRule.monthly(
    calendar: calendar,
    interval: 1,
    matchingPolicy: .strict,
    weekdays: [.nth(5, .friday)]
)

let output = rule.recurrences(of: startDate, in: startDate..<endDate)
hristost commented 2 days ago

Thank you for finding and reporting this! What is happening is that we use a base recurrence to enumerate the monthly repetition, and then move each date to the fifth Friday of that month. Since we are matching strictly, the base recurrence can skip a few months if we start from the 31st.

I'll look into fixing this in #555, where I am optimizing the way we calculate recurrences.