Skip to content

Commit 5714ec3

Browse files
committed
Partial ranges in Calendar.RecurrenceRule.recurrences()
Introduce new variants for `Calendar.RecurrenceRule.recurrences()`, which accept partial ranges as proposed in SE-NNNN.
1 parent abfcc28 commit 5714ec3

File tree

3 files changed

+239
-21
lines changed

3 files changed

+239
-21
lines changed

Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -71,27 +71,63 @@ extension Calendar {
7171
let start: Date
7272
/// The recurrenece rule
7373
let recurrence: RecurrenceRule
74-
/// Range in which the search should occur. If `nil`, return all results
75-
let range: Range<Date>?
74+
/// The lower end of the search range. If `nil`, the search is unbounded
75+
/// in the past.
76+
let lowerBound: Date?
77+
/// The upper end of the search range. If `nil`, the search is unbounded
78+
/// in the future. If `inclusive` is true, `bound` is a valid result
79+
let upperBound: (bound: Date, inclusive: Bool)?
7680

7781
init(start: Date, recurrence: RecurrenceRule, range: Range<Date>?) {
7882
self.start = start
7983
self.recurrence = recurrence
80-
self.range = range
84+
if let range {
85+
self.lowerBound = range.lowerBound
86+
self.upperBound = (range.upperBound, false)
87+
} else {
88+
self.lowerBound = nil
89+
self.upperBound = nil
90+
}
91+
}
92+
93+
init(start: Date, recurrence: RecurrenceRule, range: ClosedRange<Date>) {
94+
self.start = start
95+
self.recurrence = recurrence
96+
self.lowerBound = range.lowerBound
97+
self.upperBound = (range.upperBound, true)
98+
}
99+
100+
init(start: Date, recurrence: RecurrenceRule, range: PartialRangeFrom<Date>) {
101+
self.start = start
102+
self.recurrence = recurrence
103+
self.lowerBound = range.lowerBound
104+
self.upperBound = nil
105+
}
106+
107+
init(start: Date, recurrence: RecurrenceRule, range: PartialRangeThrough<Date>) {
108+
self.start = start
109+
self.recurrence = recurrence
110+
self.lowerBound = nil
111+
self.upperBound = (range.upperBound, true)
112+
}
113+
114+
init(start: Date, recurrence: RecurrenceRule, range: PartialRangeUpTo<Date>) {
115+
self.start = start
116+
self.recurrence = recurrence
117+
self.lowerBound = nil
118+
self.upperBound = (range.upperBound, false)
81119
}
82120

83121
struct Iterator: Sendable, IteratorProtocol {
84122
/// The starting date for the recurrence
85123
let start: Date
86124
/// The recurrence rule that should be used for enumeration
87125
let recurrence: RecurrenceRule
88-
/// The range in which the sequence should produce results
89-
let range: Range<Date>?
90-
91-
/// The lower bound of `range`, adjusted so that date expansions may
92-
/// still fit in range even if this value is outside the range. This
93-
/// value is used as a lower bound for ``nextBaseRecurrenceDate()``.
94-
let rangeLowerBound: Date?
126+
127+
/// The lower bound for iteration results, inclusive
128+
let lowerBound: Date?
129+
/// The upper bound for iteration results and whether it's inclusive
130+
let upperBound: (bound: Date, inclusive: Bool)?
95131

96132
/// The start date's nanoseconds component
97133
let startDateNanoseconds: TimeInterval
@@ -105,6 +141,10 @@ extension Calendar {
105141
/// date, by the interval specified by the recurrence rule frequency
106142
/// This does not include the start date itself.
107143
var baseRecurrence: Calendar.DatesByMatching.Iterator
144+
/// The lower bound for `baseRecurrence`. Note that this date can be
145+
/// lower than `lowerBound`
146+
let baseRecurrenceLowerBound: Date?
147+
108148

109149
/// How many elements we have consumed from `baseRecurrence`
110150
var iterations: Int = 0
@@ -123,7 +163,8 @@ extension Calendar {
123163

124164
internal init(start: Date,
125165
matching recurrence: RecurrenceRule,
126-
range: Range<Date>?) {
166+
lowerBound: Date?,
167+
upperBound: (bound: Date, inclusive: Bool)?) {
127168
// Copy the calendar if it's autoupdating
128169
var recurrence = recurrence
129170
if recurrence.calendar == .autoupdatingCurrent {
@@ -132,7 +173,6 @@ extension Calendar {
132173
self.recurrence = recurrence
133174

134175
self.start = start
135-
self.range = range
136176

137177
let frequency = recurrence.frequency
138178

@@ -215,10 +255,12 @@ extension Calendar {
215255
secondAction = .expand
216256
}
217257

218-
if let range {
219-
rangeLowerBound = recurrence.calendar.dateInterval(of: frequency.component, for: range.lowerBound)?.start
258+
self.lowerBound = lowerBound
259+
self.upperBound = upperBound
260+
if let lowerBound {
261+
baseRecurrenceLowerBound = recurrence.calendar.dateInterval(of: frequency.component, for: lowerBound)?.start
220262
} else {
221-
rangeLowerBound = nil
263+
baseRecurrenceLowerBound = nil
222264
}
223265

224266
// Create date components that enumerate recurrences without any
@@ -330,7 +372,7 @@ extension Calendar {
330372
}
331373
// If a range has been specified, we should skip a few extra
332374
// occurrences until we reach the start date
333-
if let rangeLowerBound, nextDate < rangeLowerBound {
375+
if let baseRecurrenceLowerBound, nextDate < baseRecurrenceLowerBound {
334376
continue
335377
}
336378
anchor = nextDate
@@ -476,11 +518,18 @@ extension Calendar {
476518
finished = true
477519
return nil
478520
}
479-
if let range = self.range {
480-
if date >= range.upperBound {
521+
if let upperBound = self.upperBound {
522+
let outOfRange = switch upperBound.inclusive {
523+
case true: date > upperBound.bound
524+
case false: date >= upperBound.bound
525+
}
526+
if outOfRange {
481527
finished = true
482528
return nil
483-
} else if date < range.lowerBound {
529+
}
530+
}
531+
if let lowerBound = self.lowerBound {
532+
if date < lowerBound {
484533
continue
485534
}
486535
}
@@ -503,7 +552,7 @@ extension Calendar {
503552
}
504553

505554
public func makeIterator() -> Iterator {
506-
return Iterator(start: start, matching: recurrence, range: range)
555+
return Iterator(start: start, matching: recurrence, lowerBound: lowerBound, upperBound: upperBound)
507556
}
508557
}
509558
}

Sources/FoundationEssentials/Calendar/RecurrenceRule.swift

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,12 +270,79 @@ extension Calendar {
270270
/// - Returns: a sequence of dates conforming to the recurrence rule, in
271271
/// the given `range`. An empty sequence if the rule doesn't match any
272272
/// dates.
273-
/// A recurrence that repeats every `interval` minutes
274273
public func recurrences(of start: Date,
275274
in range: Range<Date>? = nil
276275
) -> some (Sequence<Date> & Sendable) {
277276
DatesByRecurring(start: start, recurrence: self, range: range)
278277
}
278+
279+
/// Find recurrences of the given date
280+
///
281+
/// The calculations are implemented according to RFC-5545 and RFC-7529.
282+
///
283+
/// - Parameter start: the date which defines the starting point for the
284+
/// recurrence rule.
285+
/// - Parameter range: a range of dates which to search for recurrences.
286+
/// - Returns: a sequence of dates conforming to the recurrence rule, in
287+
/// the given `range`. An empty sequence if the rule doesn't match any
288+
/// dates.
289+
@available(FoundationPreview 6.3, *)
290+
public func recurrences(of start: Date,
291+
in range: PartialRangeThrough<Date>
292+
) -> some (Sequence<Date> & Sendable) {
293+
DatesByRecurring(start: start, recurrence: self, range: range)
294+
}
295+
296+
/// Find recurrences of the given date
297+
///
298+
/// The calculations are implemented according to RFC-5545 and RFC-7529.
299+
///
300+
/// - Parameter start: the date which defines the starting point for the
301+
/// recurrence rule.
302+
/// - Parameter range: a range of dates which to search for recurrences.
303+
/// - Returns: a sequence of dates conforming to the recurrence rule, in
304+
/// the given `range`. An empty sequence if the rule doesn't match any
305+
/// dates.
306+
@available(FoundationPreview 6.3, *)
307+
public func recurrences(of start: Date,
308+
in range: PartialRangeUpTo<Date>
309+
) -> some (Sequence<Date> & Sendable) {
310+
DatesByRecurring(start: start, recurrence: self, range: range)
311+
}
312+
313+
/// Find recurrences of the given date
314+
///
315+
/// The calculations are implemented according to RFC-5545 and RFC-7529.
316+
///
317+
/// - Parameter start: the date which defines the starting point for the
318+
/// recurrence rule.
319+
/// - Parameter range: a range of dates which to search for recurrences.
320+
/// - Returns: a sequence of dates conforming to the recurrence rule, in
321+
/// the given `range`. An empty sequence if the rule doesn't match any
322+
/// dates.
323+
@available(FoundationPreview 6.3, *)
324+
public func recurrences(of start: Date,
325+
in range: PartialRangeFrom<Date>
326+
) -> some (Sequence<Date> & Sendable) {
327+
DatesByRecurring(start: start, recurrence: self, range: range)
328+
}
329+
330+
/// Find recurrences of the given date
331+
///
332+
/// The calculations are implemented according to RFC-5545 and RFC-7529.
333+
///
334+
/// - Parameter start: the date which defines the starting point for the
335+
/// recurrence rule.
336+
/// - Parameter range: a range of dates which to search for recurrences.
337+
/// - Returns: a sequence of dates conforming to the recurrence rule, in
338+
/// the given `range`. An empty sequence if the rule doesn't match any
339+
/// dates.
340+
@available(FoundationPreview 6.3, *)
341+
public func recurrences(of start: Date,
342+
in range: ClosedRange<Date>
343+
) -> some (Sequence<Date> & Sendable) {
344+
DatesByRecurring(start: start, recurrence: self, range: range)
345+
}
279346

280347
/// A recurrence that repeats every `interval` minutes
281348
public static func minutely(calendar: Calendar, interval: Int = 1, end: End = .never, matchingPolicy: Calendar.MatchingPolicy = .nextTimePreservingSmallerComponents, repeatedTimePolicy: Calendar.RepeatedTimePolicy = .first, months: [Month] = [], daysOfTheYear: [Int] = [], daysOfTheMonth: [Int] = [], weekdays: [Weekday] = [], hours: [Int] = [], minutes: [Int] = [], seconds: [Int] = [], setPositions: [Int] = []) -> Self {

Tests/FoundationEssentialsTests/GregorianCalendarRecurrenceRuleTests.swift

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,4 +828,106 @@ private struct GregorianCalendarRecurrenceRuleTests {
828828
#expect(results == expectedResults, "Failed for nanoseconds \(nsec)")
829829
}
830830
}
831+
832+
@available(FoundationPreview 6.3, *)
833+
@Test func closedRange() {
834+
let rule = Calendar.RecurrenceRule(calendar: gregorian, frequency: .daily, end: .never)
835+
836+
let eventStart = Date(timeIntervalSince1970: 1285077600.0) // 2010-09-21T14:00:00-0000
837+
let sept28 = Date(timeIntervalSince1970: 1285682400.0) // 2010-09-28T14:00:00-0000
838+
let oct3 = Date(timeIntervalSince1970: 1286114400.0) // 2010-10-03T14:00:00-0000
839+
840+
let results = Array(rule.recurrences(of: eventStart, in: sept28...oct3))
841+
842+
let expectedResults = [
843+
Date(timeIntervalSince1970: 1285682400.0), // 2010-09-28T14:00:00-0000
844+
Date(timeIntervalSince1970: 1285768800.0), // 2010-09-29T14:00:00-0000
845+
Date(timeIntervalSince1970: 1285855200.0), // 2010-09-30T14:00:00-0000
846+
Date(timeIntervalSince1970: 1285941600.0), // 2010-10-01T14:00:00-0000
847+
Date(timeIntervalSince1970: 1286028000.0), // 2010-10-02T14:00:00-0000
848+
Date(timeIntervalSince1970: 1286114400.0), // 2010-10-03T14:00:00-0000
849+
]
850+
851+
#expect(results == expectedResults)
852+
}
853+
854+
@available(FoundationPreview 6.3, *)
855+
@Test func partialRangeUpTo() {
856+
let rule = Calendar.RecurrenceRule(calendar: gregorian, frequency: .daily, end: .never)
857+
858+
let eventStart = Date(timeIntervalSince1970: 1285077600.0) // 2010-09-21T14:00:00-0000
859+
let oct3 = Date(timeIntervalSince1970: 1286114400.0) // 2010-10-03T14:00:00-0000
860+
861+
let results = Array(rule.recurrences(of: eventStart, in: ..<oct3))
862+
863+
let expectedResults = [
864+
Date(timeIntervalSince1970: 1285077600.0), // 2010-09-21T14:00:00-0000
865+
Date(timeIntervalSince1970: 1285164000.0), // 2010-09-22T14:00:00-0000
866+
Date(timeIntervalSince1970: 1285250400.0), // 2010-09-23T14:00:00-0000
867+
Date(timeIntervalSince1970: 1285336800.0), // 2010-09-24T14:00:00-0000
868+
Date(timeIntervalSince1970: 1285423200.0), // 2010-09-25T14:00:00-0000
869+
Date(timeIntervalSince1970: 1285509600.0), // 2010-09-26T14:00:00-0000
870+
Date(timeIntervalSince1970: 1285596000.0), // 2010-09-27T14:00:00-0000
871+
Date(timeIntervalSince1970: 1285682400.0), // 2010-09-28T14:00:00-0000
872+
Date(timeIntervalSince1970: 1285768800.0), // 2010-09-29T14:00:00-0000
873+
Date(timeIntervalSince1970: 1285855200.0), // 2010-09-30T14:00:00-0000
874+
Date(timeIntervalSince1970: 1285941600.0), // 2010-10-01T14:00:00-0000
875+
Date(timeIntervalSince1970: 1286028000.0), // 2010-10-02T14:00:00-0000
876+
]
877+
878+
#expect(results == expectedResults)
879+
}
880+
881+
@available(FoundationPreview 6.3, *)
882+
@Test func partialRangeThrough() {
883+
let rule = Calendar.RecurrenceRule(calendar: gregorian, frequency: .daily, end: .never)
884+
885+
let eventStart = Date(timeIntervalSince1970: 1285077600.0) // 2010-09-21T14:00:00-0000
886+
let oct3 = Date(timeIntervalSince1970: 1286114400.0) // 2010-10-03T14:00:00-0000
887+
888+
let results = Array(rule.recurrences(of: eventStart, in: ...oct3))
889+
890+
let expectedResults = [
891+
Date(timeIntervalSince1970: 1285077600.0), // 2010-09-21T14:00:00-0000
892+
Date(timeIntervalSince1970: 1285164000.0), // 2010-09-22T14:00:00-0000
893+
Date(timeIntervalSince1970: 1285250400.0), // 2010-09-23T14:00:00-0000
894+
Date(timeIntervalSince1970: 1285336800.0), // 2010-09-24T14:00:00-0000
895+
Date(timeIntervalSince1970: 1285423200.0), // 2010-09-25T14:00:00-0000
896+
Date(timeIntervalSince1970: 1285509600.0), // 2010-09-26T14:00:00-0000
897+
Date(timeIntervalSince1970: 1285596000.0), // 2010-09-27T14:00:00-0000
898+
Date(timeIntervalSince1970: 1285682400.0), // 2010-09-28T14:00:00-0000
899+
Date(timeIntervalSince1970: 1285768800.0), // 2010-09-29T14:00:00-0000
900+
Date(timeIntervalSince1970: 1285855200.0), // 2010-09-30T14:00:00-0000
901+
Date(timeIntervalSince1970: 1285941600.0), // 2010-10-01T14:00:00-0000
902+
Date(timeIntervalSince1970: 1286028000.0), // 2010-10-02T14:00:00-0000
903+
Date(timeIntervalSince1970: 1286114400.0), // 2010-10-03T14:00:00-0000
904+
]
905+
906+
#expect(results == expectedResults)
907+
}
908+
909+
@available(FoundationPreview 6.3, *)
910+
@Test func partialRangeFrom() {
911+
let rule = Calendar.RecurrenceRule(calendar: gregorian, frequency: .daily, end: .never)
912+
913+
let eventStart = Date(timeIntervalSince1970: 1285077600.0) // 2010-09-21T14:00:00-0000
914+
let oct3 = Date(timeIntervalSince1970: 1286114400.0) // 2010-10-03T14:00:00-0000
915+
916+
var results = rule.recurrences(of: eventStart, in: oct3...).makeIterator()
917+
918+
#expect(results.next() == Date(timeIntervalSince1970: 1286114400.0)) // 2010-10-03T14:00:00-0000
919+
#expect(results.next() == Date(timeIntervalSince1970: 1286200800.0)) // 2010-10-04T14:00:00-0000
920+
#expect(results.next() == Date(timeIntervalSince1970: 1286287200.0)) // 2010-10-05T14:00:00-0000
921+
#expect(results.next() == Date(timeIntervalSince1970: 1286373600.0)) // 2010-10-06T14:00:00-0000
922+
#expect(results.next() == Date(timeIntervalSince1970: 1286460000.0)) // 2010-10-07T14:00:00-0000
923+
#expect(results.next() == Date(timeIntervalSince1970: 1286546400.0)) // 2010-10-08T14:00:00-0000
924+
#expect(results.next() == Date(timeIntervalSince1970: 1286632800.0)) // 2010-10-09T14:00:00-0000
925+
#expect(results.next() == Date(timeIntervalSince1970: 1286719200.0)) // 2010-10-10T14:00:00-0000
926+
#expect(results.next() == Date(timeIntervalSince1970: 1286805600.0)) // 2010-10-11T14:00:00-0000
927+
#expect(results.next() == Date(timeIntervalSince1970: 1286892000.0)) // 2010-10-12T14:00:00-0000
928+
#expect(results.next() == Date(timeIntervalSince1970: 1286978400.0)) // 2010-10-13T14:00:00-0000
929+
#expect(results.next() == Date(timeIntervalSince1970: 1287064800.0)) // 2010-10-14T14:00:00-0000
930+
#expect(results.next() == Date(timeIntervalSince1970: 1287151200.0)) // 2010-10-15T14:00:00-0000
931+
// No upper bound
932+
}
831933
}

0 commit comments

Comments
 (0)