Skip to content

Commit 2ef5d97

Browse files
authored
Properly adjust invalid dates in RecurrenceRule enumeration (#1077)
In recurrence rules without a strict matching policy, we'd sometimes come across dates which don't exist (such as February 29, 2009). When adjusting these dates, we were passing unadjusted date components. That resulted in adjusted dates with the highest unspecified component different than the original date. For example, adjusting February 29, 2009 to match components {month: 2, day: 29} would result February 29, 2012, instead of Match 1 or February 28 of the same year. Adjusting the date components (that is, setting the year to 2009 for the example above) fixes this. This is what we already do in Calendar.dates(byMatching: ...)
1 parent 7aadc41 commit 2ef5d97

File tree

2 files changed

+26
-2
lines changed

2 files changed

+26
-2
lines changed

Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -909,7 +909,8 @@ extension Calendar {
909909
}
910910

911911
let results = try unadjustedMatchDates.map { date, components in
912-
(try _adjustedDate(date, startingAfter: start, allowStartDate: true, matching: components, adjustedMatchingComponents: components , matchingPolicy: matchingPolicy, repeatedTimePolicy: repeatedTimePolicy, direction: .forward, inSearchingDate: start, previouslyReturnedMatchDate: nil), components)
912+
let adjustedComponents = _adjustedComponents(components, date: start, direction: .forward)
913+
return (try _adjustedDate(date, startingAfter: start, allowStartDate: true, matching: components, adjustedMatchingComponents: adjustedComponents, matchingPolicy: matchingPolicy, repeatedTimePolicy: repeatedTimePolicy, direction: .forward, inSearchingDate: start, previouslyReturnedMatchDate: nil), components)
913914
}
914915

915916
var foundDates: [Date] = []

Tests/FoundationEssentialsTests/GregorianCalendarRecurrenceRuleTests.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,29 @@ final class GregorianCalendarRecurrenceRuleTests: XCTestCase {
426426
]
427427
XCTAssertEqual(results, expectedResults)
428428
}
429+
430+
func testYearlyRecurrenceMovingToFeb29() {
431+
/// Rule for an event that repeats on February 29th of each year, or closest date after
432+
var rule = Calendar.RecurrenceRule(calendar: gregorian, frequency: .yearly, matchingPolicy: .nextTimePreservingSmallerComponents)
433+
rule.months = [2]
434+
rule.daysOfTheMonth = [29]
435+
436+
let rangeStart = Date(timeIntervalSince1970: 946684800.0) // 2000-01-01T00:00:00-0000
437+
let rangeEnd = Date(timeIntervalSince1970: 1262304000.0) // 2010-01-01T00:00:00-0000
438+
var birthdays = rule.recurrences(of: rangeStart, in: rangeStart..<rangeEnd).makeIterator()
439+
// ^ Since the rule will change the month and day, we only borrow the time of day from the initial date
440+
441+
XCTAssertEqual(birthdays.next(), Date(timeIntervalSince1970: 951782400.0)) // 2000-02-29T00:00:00-0000
442+
XCTAssertEqual(birthdays.next(), Date(timeIntervalSince1970: 983404800.0)) // 2001-03-01T00:00:00-0000
443+
XCTAssertEqual(birthdays.next(), Date(timeIntervalSince1970: 1014940800.0)) // 2002-03-01T00:00:00-0000
444+
XCTAssertEqual(birthdays.next(), Date(timeIntervalSince1970: 1046476800.0)) // 2003-03-01T00:00:00-0000
445+
XCTAssertEqual(birthdays.next(), Date(timeIntervalSince1970: 1078012800.0)) // 2004-02-29T00:00:00-0000
446+
XCTAssertEqual(birthdays.next(), Date(timeIntervalSince1970: 1109635200.0)) // 2005-03-01T00:00:00-0000
447+
XCTAssertEqual(birthdays.next(), Date(timeIntervalSince1970: 1141171200.0)) // 2006-03-01T00:00:00-0000
448+
XCTAssertEqual(birthdays.next(), Date(timeIntervalSince1970: 1172707200.0)) // 2007-03-01T00:00:00-0000
449+
XCTAssertEqual(birthdays.next(), Date(timeIntervalSince1970: 1204243200.0)) // 2008-02-29T00:00:00-0000
450+
XCTAssertEqual(birthdays.next(), Date(timeIntervalSince1970: 1235865600.0)) // 2009-03-01T00:00:00-0000
451+
}
429452

430453
func testYearlyRecurrenceWithMonthExpansion() {
431454
let start = Date(timeIntervalSince1970: 1285027200.0) // 2010-09-21T00:00:00-0000
@@ -780,5 +803,5 @@ final class GregorianCalendarRecurrenceRuleTests: XCTestCase {
780803
Date(timeIntervalSince1970: 1695304800.0), // 2023-09-21T14:00:00-0000
781804
]
782805
XCTAssertEqual(results, expectedResults)
783-
}
806+
}
784807
}

0 commit comments

Comments
 (0)