Skip to content

Partial ranges in Calendar.RecurrenceRule.recurrences() #1456

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 69 additions & 20 deletions Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,27 +71,63 @@ extension Calendar {
let start: Date
/// The recurrenece rule
let recurrence: RecurrenceRule
/// Range in which the search should occur. If `nil`, return all results
let range: Range<Date>?
/// The lower end of the search range. If `nil`, the search is unbounded
/// in the past.
let lowerBound: Date?
/// The upper end of the search range. If `nil`, the search is unbounded
/// in the future. If `inclusive` is true, `bound` is a valid result
let upperBound: (bound: Date, inclusive: Bool)?

init(start: Date, recurrence: RecurrenceRule, range: Range<Date>?) {
self.start = start
self.recurrence = recurrence
self.range = range
if let range {
self.lowerBound = range.lowerBound
self.upperBound = (range.upperBound, false)
} else {
self.lowerBound = nil
self.upperBound = nil
}
}

init(start: Date, recurrence: RecurrenceRule, range: ClosedRange<Date>) {
self.start = start
self.recurrence = recurrence
self.lowerBound = range.lowerBound
self.upperBound = (range.upperBound, true)
}

init(start: Date, recurrence: RecurrenceRule, range: PartialRangeFrom<Date>) {
self.start = start
self.recurrence = recurrence
self.lowerBound = range.lowerBound
self.upperBound = nil
}

init(start: Date, recurrence: RecurrenceRule, range: PartialRangeThrough<Date>) {
self.start = start
self.recurrence = recurrence
self.lowerBound = nil
self.upperBound = (range.upperBound, true)
}

init(start: Date, recurrence: RecurrenceRule, range: PartialRangeUpTo<Date>) {
self.start = start
self.recurrence = recurrence
self.lowerBound = nil
self.upperBound = (range.upperBound, false)
}

struct Iterator: Sendable, IteratorProtocol {
/// The starting date for the recurrence
let start: Date
/// The recurrence rule that should be used for enumeration
let recurrence: RecurrenceRule
/// The range in which the sequence should produce results
let range: Range<Date>?

/// The lower bound of `range`, adjusted so that date expansions may
/// still fit in range even if this value is outside the range. This
/// value is used as a lower bound for ``nextBaseRecurrenceDate()``.
let rangeLowerBound: Date?

/// The lower bound for iteration results, inclusive
let lowerBound: Date?
/// The upper bound for iteration results and whether it's inclusive
let upperBound: (bound: Date, inclusive: Bool)?

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


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

internal init(start: Date,
matching recurrence: RecurrenceRule,
range: Range<Date>?) {
lowerBound: Date?,
upperBound: (bound: Date, inclusive: Bool)?) {
// Copy the calendar if it's autoupdating
var recurrence = recurrence
if recurrence.calendar == .autoupdatingCurrent {
Expand All @@ -132,7 +173,6 @@ extension Calendar {
self.recurrence = recurrence

self.start = start
self.range = range

let frequency = recurrence.frequency

Expand Down Expand Up @@ -215,10 +255,12 @@ extension Calendar {
secondAction = .expand
}

if let range {
rangeLowerBound = recurrence.calendar.dateInterval(of: frequency.component, for: range.lowerBound)?.start
self.lowerBound = lowerBound
self.upperBound = upperBound
if let lowerBound {
baseRecurrenceLowerBound = recurrence.calendar.dateInterval(of: frequency.component, for: lowerBound)?.start
} else {
rangeLowerBound = nil
baseRecurrenceLowerBound = nil
}

// Create date components that enumerate recurrences without any
Expand Down Expand Up @@ -330,7 +372,7 @@ extension Calendar {
}
// If a range has been specified, we should skip a few extra
// occurrences until we reach the start date
if let rangeLowerBound, nextDate < rangeLowerBound {
if let baseRecurrenceLowerBound, nextDate < baseRecurrenceLowerBound {
continue
}
anchor = nextDate
Expand Down Expand Up @@ -476,11 +518,18 @@ extension Calendar {
finished = true
return nil
}
if let range = self.range {
if date >= range.upperBound {
if let upperBound = self.upperBound {
let outOfRange = switch upperBound.inclusive {
case true: date > upperBound.bound
case false: date >= upperBound.bound
}
if outOfRange {
finished = true
return nil
} else if date < range.lowerBound {
}
}
if let lowerBound = self.lowerBound {
if date < lowerBound {
continue
}
}
Expand All @@ -503,7 +552,7 @@ extension Calendar {
}

public func makeIterator() -> Iterator {
return Iterator(start: start, matching: recurrence, range: range)
return Iterator(start: start, matching: recurrence, lowerBound: lowerBound, upperBound: upperBound)
}
}
}
Expand Down
69 changes: 68 additions & 1 deletion Sources/FoundationEssentials/Calendar/RecurrenceRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -270,12 +270,79 @@ extension Calendar {
/// - Returns: a sequence of dates conforming to the recurrence rule, in
/// the given `range`. An empty sequence if the rule doesn't match any
/// dates.
/// A recurrence that repeats every `interval` minutes
public func recurrences(of start: Date,
in range: Range<Date>? = nil
) -> some (Sequence<Date> & Sendable) {
DatesByRecurring(start: start, recurrence: self, range: range)
}

/// Find recurrences of the given date
///
/// The calculations are implemented according to RFC-5545 and RFC-7529.
///
/// - Parameter start: the date which defines the starting point for the
/// recurrence rule.
/// - Parameter range: a range of dates which to search for recurrences.
/// - Returns: a sequence of dates conforming to the recurrence rule, in
/// the given `range`. An empty sequence if the rule doesn't match any
/// dates.
@available(FoundationPreview 6.3, *)
public func recurrences(of start: Date,
in range: PartialRangeThrough<Date>
) -> some (Sequence<Date> & Sendable) {
DatesByRecurring(start: start, recurrence: self, range: range)
}

/// Find recurrences of the given date
///
/// The calculations are implemented according to RFC-5545 and RFC-7529.
///
/// - Parameter start: the date which defines the starting point for the
/// recurrence rule.
/// - Parameter range: a range of dates which to search for recurrences.
/// - Returns: a sequence of dates conforming to the recurrence rule, in
/// the given `range`. An empty sequence if the rule doesn't match any
/// dates.
@available(FoundationPreview 6.3, *)
public func recurrences(of start: Date,
in range: PartialRangeUpTo<Date>
) -> some (Sequence<Date> & Sendable) {
DatesByRecurring(start: start, recurrence: self, range: range)
}

/// Find recurrences of the given date
///
/// The calculations are implemented according to RFC-5545 and RFC-7529.
///
/// - Parameter start: the date which defines the starting point for the
/// recurrence rule.
/// - Parameter range: a range of dates which to search for recurrences.
/// - Returns: a sequence of dates conforming to the recurrence rule, in
/// the given `range`. An empty sequence if the rule doesn't match any
/// dates.
@available(FoundationPreview 6.3, *)
public func recurrences(of start: Date,
in range: PartialRangeFrom<Date>
) -> some (Sequence<Date> & Sendable) {
DatesByRecurring(start: start, recurrence: self, range: range)
}

/// Find recurrences of the given date
///
/// The calculations are implemented according to RFC-5545 and RFC-7529.
///
/// - Parameter start: the date which defines the starting point for the
/// recurrence rule.
/// - Parameter range: a range of dates which to search for recurrences.
/// - Returns: a sequence of dates conforming to the recurrence rule, in
/// the given `range`. An empty sequence if the rule doesn't match any
/// dates.
@available(FoundationPreview 6.3, *)
public func recurrences(of start: Date,
in range: ClosedRange<Date>
) -> some (Sequence<Date> & Sendable) {
DatesByRecurring(start: start, recurrence: self, range: range)
}

/// A recurrence that repeats every `interval` minutes
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -828,4 +828,106 @@ private struct GregorianCalendarRecurrenceRuleTests {
#expect(results == expectedResults, "Failed for nanoseconds \(nsec)")
}
}

@available(FoundationPreview 6.3, *)
@Test func closedRange() {
let rule = Calendar.RecurrenceRule(calendar: gregorian, frequency: .daily, end: .never)

let eventStart = Date(timeIntervalSince1970: 1285077600.0) // 2010-09-21T14:00:00-0000
let sept28 = Date(timeIntervalSince1970: 1285682400.0) // 2010-09-28T14:00:00-0000
let oct3 = Date(timeIntervalSince1970: 1286114400.0) // 2010-10-03T14:00:00-0000

let results = Array(rule.recurrences(of: eventStart, in: sept28...oct3))

let expectedResults = [
Date(timeIntervalSince1970: 1285682400.0), // 2010-09-28T14:00:00-0000
Date(timeIntervalSince1970: 1285768800.0), // 2010-09-29T14:00:00-0000
Date(timeIntervalSince1970: 1285855200.0), // 2010-09-30T14:00:00-0000
Date(timeIntervalSince1970: 1285941600.0), // 2010-10-01T14:00:00-0000
Date(timeIntervalSince1970: 1286028000.0), // 2010-10-02T14:00:00-0000
Date(timeIntervalSince1970: 1286114400.0), // 2010-10-03T14:00:00-0000
]

#expect(results == expectedResults)
}

@available(FoundationPreview 6.3, *)
@Test func partialRangeUpTo() {
let rule = Calendar.RecurrenceRule(calendar: gregorian, frequency: .daily, end: .never)

let eventStart = Date(timeIntervalSince1970: 1285077600.0) // 2010-09-21T14:00:00-0000
let oct3 = Date(timeIntervalSince1970: 1286114400.0) // 2010-10-03T14:00:00-0000

let results = Array(rule.recurrences(of: eventStart, in: ..<oct3))

let expectedResults = [
Date(timeIntervalSince1970: 1285077600.0), // 2010-09-21T14:00:00-0000
Date(timeIntervalSince1970: 1285164000.0), // 2010-09-22T14:00:00-0000
Date(timeIntervalSince1970: 1285250400.0), // 2010-09-23T14:00:00-0000
Date(timeIntervalSince1970: 1285336800.0), // 2010-09-24T14:00:00-0000
Date(timeIntervalSince1970: 1285423200.0), // 2010-09-25T14:00:00-0000
Date(timeIntervalSince1970: 1285509600.0), // 2010-09-26T14:00:00-0000
Date(timeIntervalSince1970: 1285596000.0), // 2010-09-27T14:00:00-0000
Date(timeIntervalSince1970: 1285682400.0), // 2010-09-28T14:00:00-0000
Date(timeIntervalSince1970: 1285768800.0), // 2010-09-29T14:00:00-0000
Date(timeIntervalSince1970: 1285855200.0), // 2010-09-30T14:00:00-0000
Date(timeIntervalSince1970: 1285941600.0), // 2010-10-01T14:00:00-0000
Date(timeIntervalSince1970: 1286028000.0), // 2010-10-02T14:00:00-0000
]

#expect(results == expectedResults)
}

@available(FoundationPreview 6.3, *)
@Test func partialRangeThrough() {
let rule = Calendar.RecurrenceRule(calendar: gregorian, frequency: .daily, end: .never)

let eventStart = Date(timeIntervalSince1970: 1285077600.0) // 2010-09-21T14:00:00-0000
let oct3 = Date(timeIntervalSince1970: 1286114400.0) // 2010-10-03T14:00:00-0000

let results = Array(rule.recurrences(of: eventStart, in: ...oct3))

let expectedResults = [
Date(timeIntervalSince1970: 1285077600.0), // 2010-09-21T14:00:00-0000
Date(timeIntervalSince1970: 1285164000.0), // 2010-09-22T14:00:00-0000
Date(timeIntervalSince1970: 1285250400.0), // 2010-09-23T14:00:00-0000
Date(timeIntervalSince1970: 1285336800.0), // 2010-09-24T14:00:00-0000
Date(timeIntervalSince1970: 1285423200.0), // 2010-09-25T14:00:00-0000
Date(timeIntervalSince1970: 1285509600.0), // 2010-09-26T14:00:00-0000
Date(timeIntervalSince1970: 1285596000.0), // 2010-09-27T14:00:00-0000
Date(timeIntervalSince1970: 1285682400.0), // 2010-09-28T14:00:00-0000
Date(timeIntervalSince1970: 1285768800.0), // 2010-09-29T14:00:00-0000
Date(timeIntervalSince1970: 1285855200.0), // 2010-09-30T14:00:00-0000
Date(timeIntervalSince1970: 1285941600.0), // 2010-10-01T14:00:00-0000
Date(timeIntervalSince1970: 1286028000.0), // 2010-10-02T14:00:00-0000
Date(timeIntervalSince1970: 1286114400.0), // 2010-10-03T14:00:00-0000
]

#expect(results == expectedResults)
}

@available(FoundationPreview 6.3, *)
@Test func partialRangeFrom() {
let rule = Calendar.RecurrenceRule(calendar: gregorian, frequency: .daily, end: .never)

let eventStart = Date(timeIntervalSince1970: 1285077600.0) // 2010-09-21T14:00:00-0000
let oct3 = Date(timeIntervalSince1970: 1286114400.0) // 2010-10-03T14:00:00-0000

var results = rule.recurrences(of: eventStart, in: oct3...).makeIterator()

#expect(results.next() == Date(timeIntervalSince1970: 1286114400.0)) // 2010-10-03T14:00:00-0000
#expect(results.next() == Date(timeIntervalSince1970: 1286200800.0)) // 2010-10-04T14:00:00-0000
#expect(results.next() == Date(timeIntervalSince1970: 1286287200.0)) // 2010-10-05T14:00:00-0000
#expect(results.next() == Date(timeIntervalSince1970: 1286373600.0)) // 2010-10-06T14:00:00-0000
#expect(results.next() == Date(timeIntervalSince1970: 1286460000.0)) // 2010-10-07T14:00:00-0000
#expect(results.next() == Date(timeIntervalSince1970: 1286546400.0)) // 2010-10-08T14:00:00-0000
#expect(results.next() == Date(timeIntervalSince1970: 1286632800.0)) // 2010-10-09T14:00:00-0000
#expect(results.next() == Date(timeIntervalSince1970: 1286719200.0)) // 2010-10-10T14:00:00-0000
#expect(results.next() == Date(timeIntervalSince1970: 1286805600.0)) // 2010-10-11T14:00:00-0000
#expect(results.next() == Date(timeIntervalSince1970: 1286892000.0)) // 2010-10-12T14:00:00-0000
#expect(results.next() == Date(timeIntervalSince1970: 1286978400.0)) // 2010-10-13T14:00:00-0000
#expect(results.next() == Date(timeIntervalSince1970: 1287064800.0)) // 2010-10-14T14:00:00-0000
#expect(results.next() == Date(timeIntervalSince1970: 1287151200.0)) // 2010-10-15T14:00:00-0000
// No upper bound
}
}
Loading