File: 0009-calendar-recurrence-rule.md

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (519 lines) | stat: -rw-r--r-- 25,409 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
# Recurrence rules in `Calendar`

* Proposal: SF-0009
* Author: Hristo Staykov <https://github.com/hristost>
* Review Manager: [Tina Liu](https://github.com/itingliu)
* Status: **Accepted**
* Implementation: [apple/swift-foundation#464](https://github.com/apple/swift-foundation/pull/464)
* Bugs: <rdar://120559017>

## Revision history

* **v1** Initial version
* **v2** Make filters on `RecurrenceRule` not optional, and use `[]` to signify no filter is applied
* **v3** Use `Calendar.RepeatedTimePolicy` instead of a new type, make `RecurrenceRule` conform to `Equatable`. Mark `Calendar.MatchingPolicy` as `Codable`.

## Introduction

This proposal introduces API for enumerating dates matching a specific recurrence rule, and a structure to represent recurrence rules.


## Motivation

In a calendar, a recurrence rule is a set of rules describing how often a repeating event should occur. E.g. _"Yearly"_, _"Every 1st Saturday of the month"_, etc.

There are two existing APIs in the Apple ecosystem that allow for enumerating repeating events:

- `EKRecurrenceRule` in EventKit [^1]
- `INRecurrenceRule` in SiriKit [^2]

Both APIs are subsets of RFC-5545[^rfc-5545].

Foundation owns the `Calendar` type, and as such it makes sense for recurrence rules to be implement alongside Calendar.
## Proposed solution and example

We introduce `struct Calendar.RecurrenceRule` that describes how often an event should repeat. The structure models a subset of `RRULE` specified in:

- RFC-5545 Internet Calendaring and Scheduling Core Object Specification (iCalendar), 3.3.10. Recurrence Rule [^rfc-5545]
- RFC-7529 Non-Gregorian Recurrence Rules in the Internet Calendaring and Scheduling Core Object Specification (iCalendar) [^rfc-7529]

There are slight differences from the iCalendar RFCs and Apple's existing APIs:

- The complete iCalendar RFC allows repetition by seconds, whereas `Calendar.RecurrenceRule` does not. This may be added at a later time to ensure full conformance.
- Compared to our existing APIs, `Calendar.RecurrenceRule` is designed with non-Gregorian calendars in mind.
- Any recurrence rule that can be represented using `Calendar.RecurrenceRule` can be represented in iCalendar.

## Detailed design

```swift
extension Calendar {
    /// A rule which specifies how often an event should repeat in the future
    @available(FoundationPreview 0.4, *)
    public struct RecurrenceRule: Equatable, Codable, Sendable {
        /// The calendar in which the recurrence occurs
        public var calendar: Calendar
        /// What to do when a recurrence is not a valid date
        ///
        /// An occurrence may not be a valid date if it falls on a leap day or a
        /// leap hour when there is not one. When that happens, we can choose to
        /// ignore the occurrence (`.strict`), choose a later time which has the
        /// same components (`.nextTimePreservingSmallerComponents`), or find an
        /// earlier time (`.previousTimePreservingSmallerComponents`).
        ///
        /// For example, consider an event happening every year, starting on the
        /// 29th of February 2020. When the matching policy is set to `.strict`,
        /// it yields the following recurrences:
        /// - 2020-02-29
        /// - 2024-02-29
        /// - 2028-02-29
        /// - ...
        ///
        /// With `matchingPolicy` of `.previousTimePreservingSmallerComponents`,
        /// we get a result for each year:
        /// - 2020-02-29
        /// - 2021-02-28
        /// - 2022-02-28
        /// - 2023-02-28
        /// - 2024-02-29
        ///
        /// Lastly, a `matchingPolicy` of `.nextTimePreservingSmallerComponents`
        /// moves invalid occurrences to the day after February 29:
        /// - 2020-02-29
        /// - 2021-03-01
        /// - 2022-03-01
        /// - 2023-03-01
        /// - 2024-02-29
        ///
        /// The same logic applies for missing leap hours during daylight saving
        /// time switches. For example, consider an event repeating daily, which
        /// starts at March 9 2024, 01:30 PST. With a `.strict` matching policy,
        /// the event repeats on the following dates, and skips a day:
        /// - 2024-03-09 01:30 PST (09:30 UTC)
        ///   (on 2024-03-10, there is a missing hour between 1am and 2am)
        /// - 2024-03-11 01:30 PDT (08:30 UTC)
        /// - 2024-03-12 01:30 PDT (08:30 UTC)
        /// With `matchingPolicy` of `.previousTimePreservingSmallerComponents`,
        /// we get a result for each day:
        /// - 2024-03-09 01:30 PST (09:30 UTC)
        /// - 2024-03-10 02:30 PST (10:30 UTC)
        ///   (on 2024-03-10, there is a missing hour between 1am and 2am)
        /// - 2024-03-11 01:30 PDT (08:30 UTC)
        /// - 2024-03-12 01:30 PDT (08:30 UTC)
        /// Lastly, a `matchingPolicy` of `.nextTimePreservingSmallerComponents`
        /// moves invalid occurrences an hour forward:
        /// - 2024-03-09 01:30 PST (09:30 UTC)
        /// - 2024-03-10 00:30 PST (08:30 UTC)
        ///   (on 2024-03-10, there is a missing hour between 1am and 2am)
        /// - 2024-03-11 01:30 PDT (08:30 UTC)
        /// - 2024-03-12 01:30 PDT (08:30 UTC)
        ///
        /// Default value is `.nextTimePreservingSmallerComponents`
        public var matchingPolicy: Calendar.MatchingPolicy
        /// What to do when there are multiple recurrences occurring at the same
        /// time of the day but in different time zones due to a daylight saving
        /// transition.
        ///
        /// For example, an event with daily recurrence rule that starts at 1 am
        /// on November 2 in PDT will repeat on:
        ///
        /// - 2024-11-02 01:00 PDT (08:00 UTC)
        /// - 2024-11-03 01:00 PDT (08:00 UTC)
        ///   (Time zone switches from PST to PDT - clock jumps back one hour at
        ///    02:00 PDT)
        /// - 2024-11-03 01:00 PST (09:00 UTC)
        /// - 2024-11-04 01:00 PST (09:00 UTC)
        ///
        /// Due to the time zone switch on November 3, there are different times
        /// when the event might repeat.
        ///
        /// Default value is `.onlyFirst`
        public var repeatedTimePolicy: Calendar.RepeatedTimePolicy
        /// How often a recurring event repeats
        public enum Frequency: Sendable, Codable {
            case minutely, hourly, daily, weekly, monthly, yearly
        }
        /// How often the event repeats
        public var frequency: Frequency
        /// At what interval to repeat
        ///
        /// Default value is `1`
        public var interval: Int
        /// When a recurring event stops recurring
        public struct End: Sendable, Codable {
            /// The event stops repeating after a given number of times
            /// - Parameter count: how many times to repeat the event, including
            ///                    the first occurrence. `count` must be greater
            ///                    than `0`
            public static func afterOccurrences(_ count: Int) -> Self
            /// The event stops repeating after a given date
            /// - Parameter date: the date on which the event may last occur. No
            ///                   further occurrences will be found after that
            public static func afterDate(_ date: Date) -> Self
            /// The event repeats indefinitely
            public static var never: Self
        }
        /// For how long the event repeats
        ///
        /// Default value is `.never`
        public var end: End
        
        public enum Weekday: Sendable, Codable, Equatable {
            /// Repeat on every weekday
            case every(Locale.Weekday)
            /// Repeat on the n-th instance of the specified weekday in a month,
            /// if the recurrence has a monthly frequency. If the recurrence has
            /// a yearly frequency, repeat on the n-th week of the year.
            /// 
            /// If n is negative, repeat on the n-to-last of the given weekday.
            case nth(Int, Locale.Weekday)
        }
        
        /// Uniquely identifies a month in any calendar system
        public struct Month: Equatable, Codable, Sendable, ExpressibleByIntegerLiteral {
            public var index: Int
            public var isLeap: Bool

            public init(_ index: Int, isLeap: Bool = false)
        }
        
        /// On which seconds of the minute the event should repeat. Valid values
        /// between 0 and 60
        public var seconds: [Int]
        /// On which minutes of the hour the event should repeat. Accepts values
        /// between 0 and 59
        public var minutes: [Int]
        /// On which hours of a 24-hour day the event should repeat.
        public var hours: [Int]
        /// On which days of the week the event should occur
        public var weekdays: [Weekday]
        /// On which days in the month the event should occur
        /// - 1 signifies the first day of the month.
        /// - Negative values point to a day counted backwards from the last day
        ///   of the month
        /// This field is unused when `frequency` is `.weekly`.
        public var daysOfTheMonth: [Int]
        /// On which days of the year the event may occur.
        /// - 1 signifies the first day of the year.
        /// - Negative values point to a day counted backwards from the last day
        ///   of the year
        /// This field is unused when `frequency` is any of `.daily`, `.weekly`,
        /// or `.monthly`.
        public var daysOfTheYear: [Int]
        /// On which months the event should occur.
        /// - 1 is the first month of the year (January in Gregorian calendars)
        public var months: [Month]
        /// On which weeks of the year the event should occur.
        /// - 1 is the first week of the year. `calendar.minimumDaysInFirstWeek`
        ///   defines which week is considered first.
        /// - Negative values refer to weeks if counting backwards from the last
        ///   week of the year. -1 is the last week of the year.
        /// This field is unused when `frequency` is other than `.yearly`.
        public var weeks: [Int]
        /// Which occurrences within every interval should be returned
        public var setPositions: [Int]

        public init(calendar: Calendar,
                    frequency: Frequency,
                    interval: Int = 1,
                    end: End = .never,
                    matchingPolicy: Calendar.MatchingPolicy = .nextTimePreservingSmallerComponents,
                    repeatedTimePolicy: RepeatedTimePolicy = .onlyFirst,
                    months: [Month] = [],
                    daysOfTheYear: [Int] = [],
                    daysOfTheMonth: [Int] = [],
                    weeks: [Int] = [],
                    weekdays: [Weekday] = [],
                    hours: [Int] = [],
                    minutes: [Int] = [],
                    seconds: [Int] = [],
                    setPositions: [Int] = []) -> Self

        /// 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.
        ///   If `nil`, return all recurrences of the event.
        /// - 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.
        public func recurrences(of start: Date,
                                in range: Range<Date>? = nil
                                ) -> some (Sequence<Date> & Sendable)

        /// A recurrence that repeats every `interval` minutes
        public static func minutely(calendar: Calendar, interval: Int = 1, end: End = .never, matchingPolicy: Calendar.MatchingPolicy = .nextTimePreservingSmallerComponents, repeatedTimePolicy: RepeatedTimePolicy = .onlyFirst, months: [Month] = [], daysOfTheYear: [Int] = [], daysOfTheMonth: [Int] = [], weekdays: [Weekday] = [], hours: [Int] = [], minutes: [Int] = [], seconds: [Int] = [] setPositions: [Int] = []) -> Self
        /// A recurrence that repeats every `interval` hours
        public static func hourly(calendar: Calendar, interval: Int = 1, end: End = .never, matchingPolicy: Calendar.MatchingPolicy = .nextTimePreservingSmallerComponents, repeatedTimePolicy: RepeatedTimePolicy = .onlyFirst, months: [Month] = [], daysOfTheYear: [Int] = [], daysOfTheMonth: [Int] = [], weekdays: [Weekday] = [], hours: [Int] = [], minutes: [Int] = [], seconds: [Int] = [] setPositions: [Int] = []) -> Self
        /// A recurrence that repeats every `interval` days
        public static func daily(calendar: Calendar, interval: Int = 1, end: End = .never, matchingPolicy: Calendar.MatchingPolicy = .nextTimePreservingSmallerComponents, repeatedTimePolicy: RepeatedTimePolicy = .onlyFirst, months: [Month] = [], daysOfTheMonth: [Int] = [], weekdays: [Weekday] = [], hours: [Int] = [], minutes: [Int] = [], seconds: [Int] = [] setPositions: [Int] = []) -> Self
        /// A recurrence that repeats every `interval` weeks
        public static func weekly(calendar: Calendar, interval: Int = 1, end: End = .never, matchingPolicy: Calendar.MatchingPolicy = .nextTimePreservingSmallerComponents, repeatedTimePolicy: RepeatedTimePolicy = .onlyFirst, months: [Month] = [], weekdays: [Weekday] = [], hours: [Int] = [], minutes: [Int] = [], seconds: [Int] = [] setPositions: [Int] = []) -> Self
        /// A recurrence that repeats every `interval` months
        public static func monthly(calendar: Calendar, interval: Int = 1, end: End = .never, matchingPolicy: Calendar.MatchingPolicy = .nextTimePreservingSmallerComponents, repeatedTimePolicy: RepeatedTimePolicy = .onlyFirst, months: [Month] = [], daysOfTheMonth: [Int] = [], weekdays: [Weekday] = [], hours: [Int] = [], minutes: [Int] = [], seconds: [Int] = [] setPositions: [Int] = []) -> Self
        /// A recurrence that repeats every `interval` years
        public static func yearly(calendar: Calendar, interval: Int = 1, end: End = .never, matchingPolicy: Calendar.MatchingPolicy = .nextTimePreservingSmallerComponents, repeatedTimePolicy: RepeatedTimePolicy = .onlyFirst, months: [Month] = [], daysOfTheYear: [Int] = [], daysOfTheMonth: [Int] = [], weeks: [Int] = [], weekdays: [Weekday] = [], hours: [Int] = [], minutes: [Int] = [], seconds: [Int] = [] setPositions: [Int] = []) -> Self
    }
}

@available(FoundationPreview 0.4, *)
extension Calendar.MatchingPolicy: Codable {}
```



There is no Objective-C interface to this API.

### Usage
A recurrence rule of a given frequency repeats the start date with the interval of that frequency. For example, assume that now it is February 09 2024, 13:43. Creating a daily recurrence would yield a result for each following date at the same time:

```swift
var recurrence = Calendar.RecurrenceRule(calendar: .current, frequency: .daily)
for date in recurrence.recurrences(of: .now) {
    // 2024-02-09, 13:43
    // 2024-02-10, 13:43
    // 2024-02-11, 13:43
    // 2024-02-12, 13:43
    // ...
}
```

A recurrence can be limited by a given end date:
```swift
let until: Date // = 01 March 2024, 00:00
var recurrence = Calendar.RecurrenceRule(calendar: .current, frequency: .daily, until: until)
for date in recurrence.recurrences(of: .now) {
    // 2024-02-09, 13:43
    // 2024-02-10, 13:43
    // ...
    // 2024-02-28, 13:43
    // 2024-02-29, 13:43
}
```

or by a given count:
```swift
var recurrence = Calendar.RecurrenceRule(calendar: .current, frequency: .daily, count: 3)
for date in recurrence.recurrences(of: .now) {
    // 2024-02-09, 13:43
    // 2024-02-10, 13:43
    // 2024-02-11, 13:43
}
```

An internal can be specified so we don't repeat at every unit of the frequency. For example, a weekly recurrence with a frequency of 2 means to repeat every other week, at the same time:
```swift
var recurrence = Calendar.RecurrenceRule(calendar: .current, frequency: .weekly)
recurrence.interval = 2
for date in recurrence.recurrences(of: .now) {
    // 2024-02-09, 13:43
    // 2024-02-23, 13:43
    // 2024-03-08, 13:43
}
```

The `minutes`, `hours`, `weekdays`, `daysOfTheMonth`, `months`, and `daysOfTheYear` can be used to limit or expand the search so it returns multiple results per unit of repetitions. A value of `[]` is unused and does not affect the recurrence. For example, if we want to repeat an event every Tuesday, Wednesday, and Thursday:
```swift
var recurrence = Calendar.RecurrenceRule(calendar: .current, frequency: .weekly)
recurrence.weekdays = [.every(.tuesday), .every(.wednesday), .every(.thursday)]
for date in recurrence.recurrences(of: .now) {
    // 2024-02-13, 13:43 (Tuesday)
    // 2024-02-14, 13:43 (Wednesday)
    // 2024-02-15, 13:43 (Thursday)
    // 2024-02-20, 13:43 (Tuesday)
    // 2024-02-21, 13:43 (Wednesday)
    // ...
}
```

Note that the start date is not part of the sequence, for it is on a Friday. Smaller components of the start date like hour and minute are preserved, unless overwritten with the recurrence rule.

The same fields can be used to limit the search. For example, if we want all the Fridays in February from now on:
```swift
var recurrence = Calendar.RecurrenceRule(calendar: .current, frequency: .weekly)
recurrence.weekdays = [.every(.friday)]
for date in recurrence.recurrences(of: .now) {
    // 2024-02-09, 13:43
    // 2024-02-16, 13:43
    // 2024-02-19, 13:43
    // 2024-02-23, 13:43
    // 2025-02-07, 13:43
    // ...
}
```

Lastly, the "set position" can be used to filter results by their position in the repetition interval. For example, if we want the first weekend day of the month, we filter by Saturdays and Sundays in every month, and return only the first match in the repetition interval (a month):

```swift
var recurrence = Calendar.RecurrenceRule(calendar: .current, frequency: .monthly)
recurrence.weekdays = [.every(.saturday), .every(.sunday)]
recurrence.setPositions = [1]
for date in recurrence.recurrences(of: .now) {
    // 2024-03-01, 13:43
    // 2024-04-06, 13:43
    // ...
}
```

The precise semantics of whether a field limits or expands the search vary by the specified frequency. In practice, we adhere to RFC-5545 for calculating recurrences. Please consult that document for further reference.

<details>
  <summary>Valid combinations of frequencies and fields</summary>
RFC-5545 specifies the following valid combinations of frequencies and fields. _"Limit"_ indicates that specifying the field filters results, whereas _"Expand"_ results in more matches per frequency interval. _"N/A"_ indicates that the field will not be used with the given frequency.

| Frequency        |`.minutely`|`.hourly` |`.daily`  |`.weekly`|`.monthly`|`.yearly`|
|------------------|-----------|----------|----------|---------|----------|---------|
|`.months`         |Limit      |Limit     |Limit     |Limit    |Limit     |Expand   |
|`.weeks`          |N/A        |N/A       |N/A       |N/A      |N/A       |Expand   |
|`.daysOfTheYear`  |Limit      |Limit     |N/A       |N/A      |N/A       |Expand   |
|`.daysOfTheMonth` |Limit      |Limit     |Limit     |N/A      |Expand    |Expand   |
|`.weekdays`       |Limit      |Limit     |Limit     |Expand   |Note 1    |Note 2   |
|`.hours`          |Limit      |Limit     |Expand    |Expand   |Expand    |Expand   |
|`.minutes`        |Limit      |Expand    |Expand    |Expand   |Expand    |Expand   |
|`.setPositions`   |Limit      |Limit     |Limit     |Limit    |Limit     |Limit    |

- Note 1:  Limit if `daysOfTheMonth` is present; otherwise expand
- Note 2:  Limit if `daysOfTheYear` or `daysOfTheMonth` is present; otherwise expand
</details>

#### Matching policy

Some dates created by a recurrence may not exist in a given calendar. For example, consider a person born on the 29th of February 1996. Their birthdays would be enumerated by repeating that date yearly:
```swift
let birthday: Date // = 29 February 1996 14:00
var recurrence = Calendar.RecurrenceRule(calendar.current, frequency: .yearly)
```

If we want to choose February 28 to observe the birthday on non-leap years, we can use `.previousTimePreservingSmallerComponents`:
```swift
recurrence.matchingPolicy = .previousTimePreservingSmallerComponents
let birthdays = recurrence.recurrences(of: birthday)
// 1996-02-29 14:00
// 1997-02-28 14:00
// 1998-02-28 14:00
// 1999-02-28 14:00
// 2000-02-29 14:00
// ...
```

If we would like to use March 1st, we may use `.nextTimePreservingSmallerComponents`:
```swift
recurrence.matchingPolicy = .nextTimePreservingSmallerComponents
let birthdays = recurrence.recurrences(of: birthday)
// 1996-02-29 14:00
// 1997-03-01 14:00
// 1998-03-01 14:00
// 1999-03-01 14:00
// 2000-02-29 14:00
// ...
```

Alternatively, if we only care about exact matches, we can use `.strict`:
```swift
recurrence.matchingPolicy = .strict
let birthdays = recurrence.recurrences(of: birthday)
// 1996-02-29 14:00
// 2000-02-29 14:00
// 2004-02-29 14:00
// ...
```

### Examples

- Every 21st of September in the future:
  ```swift
  var recurrence = Calendar.RecurrenceRule(calendar: .current, frequency: .yearly)
  recurrence.months = [9]
  recurrence.daysOfTheMonth = [21]

  for date in recurrence.recurrences(of: .now) {
      // 21 Sept 2024, 21 Sept 2025, and so on.
  }
  ```
- Start of the Lunar New Year:
  ```swift
  let lunarCalendar = Calendar(identifier: .chinese)
  var recurrence = Calendar.RecurrenceRule(calendar: lunarCalendar, frequency: .yearly, count: 5)
  recurrence.daysOfTheMonth = [1]
  recurrence.months = [1]

  for date in recurrence.recurrences(of: .now) {
      // First day of the Lunar New Year
  }
  ```
- Every last Friday of the month at 6PM, from now until 5 bike parties later:
  ```swift
  var recurrence = Calendar.RecurrenceRule(calendar: .current, frequency: .monthly, count: 5)
  recurrence.weekdays = [.nth(-1, .friday)]
  recurrence.hours = [18]
  recurrence.minutes = [0]

  for date in recurrence.recurrences(of: .now) {
      // Critical mass bike ride
  }
  ```
- The first weekend day of each month:
  ```swift
  var recurrence = Calendar.RecurrenceRule(calendar: .current, frequency: .monthly, count: 5)
  recurrence.weekdays = [.nth(1, .saturday), .nth(1, .sunday)]
  recurrence.setPositions = [1]
  for date in recurrence.recurrences(of: .now) {
      // The first Saturday or Sunday in the month, whichever comes first
  }
  ```
- Every weekday at 8:05, 8:35, 9:05, and 9:35am:
  ```swift
  var recurrence = Calendar.RecurrenceRule(calendar: .current, frequency: .monthly, count: 5)
  recurrence.weekdays = [.every(.monday), .every(.tuesday), .every(.wednesday), .every(.thursday), .every(.friday)]
  recurrence.hours = [8, 9]
  recurrence.minutes = [5, 35]
  ```


## Impact on existing code

None. This is an additive change.


## Alternatives considered

### Making the start time part of the recurrence rule

The starting date could also be made of the recurrence rule itself. Then, API usage could look like:
```swift
var recurrence = Calendar.Recurrence(calendar: .current, startingAt: .now, frequency: .monthly, count: 5)
for date in recurrence {

}
```

However, this would likely result in developers persisting the starting date twice. Imagine the following model for describing an event in a calendar:
```swift
struct Event {
    var start: Date
    var duration: Duration
    var recurrence: Calendar.RecurrenceRule?
}
```

The recurrence rule only describes how often an event should occur, not all of the event itself. Requiring the start time be part of the rule results in duplication thus and the possibility of mismatches in the above model.

Furthermore, moving the start date into the recurrence rule changes the semantics of what this API represents -- it would be no longer a recurrence rule, but a set of recurring dates.

### Enforcing valid states

Some combinations of fields in a recurrence rule are invalid. For example, it is not allowed to set `weeks` on a recurrence rule which has an hourly frequency. In such cases, we have to ignore invalid values. The static methods on `RecurrenceRule` allow it to be initialized with only valid fields for the given frequency. 

If we deem that enforcing valid states is important at compile time, we could remove the generic `init()` initializer, and make every property read-only so it may only be initialized with the appropriate static method for the desired frequency. This does not amount to full validation, as the there may still be invalid values such as months and weeks that are out of range for the given calendar. Furthermore, if this model ends up being used for iCalendar events, we want to make sure that round-trip encoding is possible even with an invalid state.

On the other hand, we can simply remove the static methods and let the user initialize each field separately, with the caveat that some values may be unused. Such is already the case with `DateComponents`.

## Future directions


[^1]: <https://developer.apple.com/documentation/eventkit/ekrecurrencerule>
[^2]: <https://developer.apple.com/documentation/sirikit/inrecurrencerule>
[^rfc-5545]: <https://www.rfc-editor.org/info/rfc5545>
[^rfc-7529]: <https://www.rfc-editor.org/info/rfc7529>