Skip to content

Commit a92bc21

Browse files
authored
RecurrenceRule: Respect leap months (#1005)
This change makes a fix to `Calendar.RecurrenceRule` with regards to leap months and adds a few tests to verify correctness. When constructing the base sequence, we include `isLeapMonth` of the start date in the date components. Before, start dates falling on a leap month would have been treated the same as dates which do fall on the non-leap month before.
1 parent 09d9e34 commit a92bc21

File tree

2 files changed

+57
-5
lines changed

2 files changed

+57
-5
lines changed

Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ extension Calendar {
228228
case .daily: [.second, .minute, .hour]
229229
case .weekly: [.second, .minute, .hour, .weekday]
230230
case .monthly: [.second, .minute, .hour, .day]
231-
case .yearly: [.second, .minute, .hour, .day, .month]
231+
case .yearly: [.second, .minute, .hour, .day, .month, .isLeapMonth]
232232
}
233233
let componentsForEnumerating = recurrence.calendar._dateComponents(components, from: start)
234234

Tests/FoundationInternationalizationTests/CalendarRecurrenceRuleTests.swift

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,58 @@ final class CalendarRecurrenceRuleTests: XCTestCase {
5454
XCTAssertEqual(results, expectedResults)
5555
}
5656

57+
func testExpandToLeapMonths() {
58+
var lunarCalendar = Calendar(identifier: .chinese)
59+
lunarCalendar.timeZone = .gmt
60+
let locale = Locale(identifier: "zh-TW")
61+
62+
let start = Date(timeIntervalSince1970: 1729641600.0) // 2024-10-23T00:00:00-0000
63+
64+
var rule = Calendar.RecurrenceRule(calendar: lunarCalendar, frequency: .yearly)
65+
rule.months = [Calendar.RecurrenceRule.Month(6, isLeap: true)]
66+
rule.daysOfTheMonth = [1]
67+
var sequence = rule.recurrences(of: start).makeIterator()
68+
69+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 1753401600.0)) // 2025-07-25T00:00:00-0000 (Sixth leap month)
70+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 1786579200.0)) // 2026-08-13T00:00:00-0000 (Seventh month)
71+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 1817164800.0)) // 2027-08-02T00:00:00-0000 (Seventh month)
72+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 1850342400.0)) // 2028-08-20T00:00:00-0000 (Seventh month)
73+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 1881014400.0)) // 2029-08-10T00:00:00-0000 (Seventh month)
74+
}
75+
76+
func testStartFromLeapMonth() {
77+
var lunarCalendar = Calendar(identifier: .chinese)
78+
lunarCalendar.timeZone = .gmt
79+
80+
// Find recurrences of an event that happens on a leap month
81+
let start = Date(timeIntervalSince1970: 1753401600.0) // 2025-07-25T00:00:00-0000 (Leap month)
82+
83+
// A non-strict recurrence would match the month where the leap month would have been
84+
let rule = Calendar.RecurrenceRule(calendar: lunarCalendar, frequency: .yearly, matchingPolicy: .nextTimePreservingSmallerComponents)
85+
var sequence = rule.recurrences(of: start).makeIterator()
86+
87+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 1753401600.0)) // 2025-07-25T00:00:00-0000 (Sixth leap month)
88+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 1786579200.0)) // 2026-08-13T00:00:00-0000 (Seventh month)
89+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 1817164800.0)) // 2027-08-02T00:00:00-0000 (Seventh month)
90+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 1850342400.0)) // 2028-08-20T00:00:00-0000 (Seventh month)
91+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 1881014400.0)) // 2029-08-10T00:00:00-0000 (Seventh month)
92+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 1911600000.0)) // 2030-07-30T00:00:00-0000 (Seventh month)
93+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 1944777600.0)) // 2031-08-18T00:00:00-0000 (Seventh month)
94+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 1975363200.0)) // 2032-08-06T00:00:00-0000 (Seventh month)
95+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 2005948800.0)) // 2033-07-26T00:00:00-0000 (Seventh month)
96+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 2039126400.0)) // 2034-08-14T00:00:00-0000 (Seventh month)
97+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 2069798400.0)) // 2035-08-04T00:00:00-0000 (Seventh month)
98+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 2100384000.0)) // 2036-07-23T00:00:00-0000 (Sixth leap month)
99+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 2133561600.0)) // 2037-08-11T00:00:00-0000 (Seventh month)
100+
XCTAssertEqual(sequence.next(), Date(timeIntervalSince1970: 2164233600.0)) // 2038-08-01T00:00:00-0000 (Seventh month)
101+
102+
// A strict recurrence only matches in years with leap months
103+
let strictRule = Calendar.RecurrenceRule(calendar: lunarCalendar, frequency: .yearly, matchingPolicy: .strict)
104+
var strictSequence = strictRule.recurrences(of: start).makeIterator()
105+
XCTAssertEqual(strictSequence.next(), Date(timeIntervalSince1970: 1753401600.0)) // 2025-07-25T00:00:00-0000 (Sixth leap month)
106+
XCTAssertEqual(strictSequence.next(), Date(timeIntervalSince1970: 2100384000.0)) // 2036-07-23T00:00:00-0000 (Sixth leap month)
107+
}
108+
57109
func testDaylightSavingsRepeatedTimePolicyFirst() {
58110
let start = Date(timeIntervalSince1970: 1730535600.0) // 2024-11-02T01:20:00-0700
59111
var rule = Calendar.RecurrenceRule(calendar: gregorian, frequency: .daily)
@@ -68,9 +120,9 @@ final class CalendarRecurrenceRuleTests: XCTestCase {
68120
Date(timeIntervalSince1970: 1730712000.0), // 2024-11-04T01:20:00-0800
69121
]
70122
XCTAssertEqual(results, expectedResults)
71-
}
72-
73-
func testDaylightSavingsRepeatedTimePolicyLast() {
123+
}
124+
125+
func testDaylightSavingsRepeatedTimePolicyLast() {
74126
let start = Date(timeIntervalSince1970: 1730535600.0) // 2024-11-02T01:20:00-0700
75127
var rule = Calendar.RecurrenceRule(calendar: gregorian, frequency: .daily)
76128
rule.repeatedTimePolicy = .last
@@ -84,5 +136,5 @@ final class CalendarRecurrenceRuleTests: XCTestCase {
84136
Date(timeIntervalSince1970: 1730712000.0), // 2024-11-04T01:20:00-0800
85137
]
86138
XCTAssertEqual(results, expectedResults)
87-
}
139+
}
88140
}

0 commit comments

Comments
 (0)