From 4013137c0ccdde01c66d0ff71058b445e48fda0f Mon Sep 17 00:00:00 2001 From: Lukas Kollmer Date: Thu, 8 May 2025 16:00:50 +0200 Subject: [PATCH] fix `Calendar.RecurrenceRule` --- .../Calendar/Calendar_Recurrence.swift | 12 ++++++++++++ .../GregorianCalendarRecurrenceRuleTests.swift | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift b/Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift index 1a1fac7e9..173558761 100644 --- a/Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift +++ b/Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift @@ -93,6 +93,9 @@ extension Calendar { /// value is used as a lower bound for ``nextBaseRecurrenceDate()``. let rangeLowerBound: Date? + /// The start date's nanoseconds component + let startDateNanoseconds: TimeInterval + /// How many occurrences have been found so far var resultsFound = 0 @@ -232,6 +235,8 @@ extension Calendar { } var componentsForEnumerating = recurrence.calendar._dateComponents(components, from: start) + startDateNanoseconds = start.timeIntervalSince1970.remainder(dividingBy: 1) + let expansionChangesDay = dayOfYearAction == .expand || dayOfMonthAction == .expand || weekAction == .expand || weekdayAction == .expand let expansionChangesMonth = dayOfYearAction == .expand || monthAction == .expand || weekAction == .expand @@ -422,6 +427,13 @@ extension Calendar { recurrence._limitTimeComponent(.second, dates: &dates, anchor: anchor) } + if startDateNanoseconds > 0 { + // `_dates(startingAfter:)` above returns whole-second dates, + // so we need to restore the nanoseconds value present in the original start date. + for idx in dates.indices { + dates[idx] += startDateNanoseconds + } + } dates = dates.filter { $0 >= self.start } if let limit = recurrence.end.date { diff --git a/Tests/FoundationEssentialsTests/GregorianCalendarRecurrenceRuleTests.swift b/Tests/FoundationEssentialsTests/GregorianCalendarRecurrenceRuleTests.swift index f95cd37cc..112a4c45e 100644 --- a/Tests/FoundationEssentialsTests/GregorianCalendarRecurrenceRuleTests.swift +++ b/Tests/FoundationEssentialsTests/GregorianCalendarRecurrenceRuleTests.swift @@ -803,4 +803,16 @@ final class GregorianCalendarRecurrenceRuleTests: XCTestCase { ] XCTAssertEqual(results, expectedResults) } + + func testDailyRecurrenceRuleWithNonzeroNanosecondComponent() { + let start = Date(timeIntervalSince1970: 1746627600.5) // 2025-05-07T07:20:00.500-07:00 + let rule = Calendar.RecurrenceRule.daily(calendar: gregorian, end: .afterOccurrences(2)) + let results = Array(rule.recurrences(of: start)) + + let expectedResults: [Date] = [ + start, + Date(timeIntervalSince1970: 1746714000.5), // 2025-05-08T07:20:00.500-07:00 + ] + XCTAssertEqual(results, expectedResults) + } }