├── .gitignore
├── Documentation
└── Resources
│ └── installation.png
├── LICENSE
├── README.md
├── RWMRecurrenceRule.playground
├── Contents.swift
└── contents.xcplayground
├── RWMRecurrenceRule.podspec
├── RWMRecurrenceRule.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ └── xcschemes
│ ├── RWMRecurrenceRule_iOS.xcscheme
│ ├── RWMRecurrenceRule_macOS.xcscheme
│ └── RWMRecurrenceRule_watchOS.xcscheme
├── RWMRecurrenceRule
├── Calendar+RWM.swift
├── EKEvent+RWM.swift
├── EKRecurrenceRule+RWM.swift
├── RWMRecurrenceRule.h
├── RWMRecurrenceRule.swift
├── RWMRuleParser.swift
└── RWMRuleScheduler.swift
├── RWMRecurrenceRuleTests
├── CalendarTests.swift
├── RWMDailyTests.swift
├── RWMEventKitTests.swift
├── RWMMonthlyTests.swift
├── RWMRecurrenceRuleBase.swift
├── RWMSpecTests.swift
├── RWMWeeklyTests.swift
└── RWMYearlyTests.swift
├── RWMRecurrenceRule_iOS
└── Info.plist
├── RWMRecurrenceRule_iOSTests
└── Info.plist
├── RWMRecurrenceRule_macOS
└── Info.plist
├── RWMRecurrenceRule_macOSTests
└── Info.plist
└── RWMRecurrenceRule_watchOS
└── Info.plist
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | xcuserdata/
4 |
--------------------------------------------------------------------------------
/Documentation/Resources/installation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rmaddy/RWMRecurrenceRule/79197ae3bee82574d74e9213f71d18c98d8b8aaf/Documentation/Resources/installation.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Rick Maddy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RWMRecurrenceRule
2 |
3 | Provides support for iCalendar RRULE expressions including an extension to [EventKit][] allowing you to create an [EKRecurrenceRule][] from an RRULE expression and to enumerate the dates of an [EKEvent][].
4 |
5 | For complete details about iCalendar RRULE expressions, see the [Format Definition][] and [examples][] at [iCalendar.org][].
6 |
7 | Note that you do not need to make any use of RRULE expressions to use this framework. If you just want to enumerate
8 | the dates of an EKEvent, you can use this framework to do so without any use or knowledge of RRULE syntax.
9 |
10 | RWMRecurrenceRule can be used with iOS 9.0 and later, macOS 10.12 and later, and watchOS 2.0 and later.
11 |
12 | [Format Definition]: https://icalendar.org/iCalendar-RFC-5545/3-3-10-recurrence-rule.html
13 | [examples]: https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html
14 | [iCalendar.org]: https://icalendar.org/
15 |
16 | ## Usage
17 |
18 | This first example shows how you can enumerate the dates of some events in your calendar. This
19 | `listDates` function would need to be called after gaining permission to access the event store.
20 |
21 | ```swift
22 | import EventKit
23 | import RWMRecurrenceRule
24 |
25 | func listDates(from store: EKEventStore) {
26 | let start = Date()
27 | let end = Calendar.current.date(byAdding: .month, value: 3, to: start)!
28 | // Get events from the next 3 months
29 | let pred = store.predicateForEvents(withStart: start, end: end, calendars: nil)
30 | // Iterate through the events
31 | store.enumerateEvents(matching: pred) { (event, stop) in
32 | print("Event: \(event.title)")
33 | var count = 0
34 | // Iterate through the first 20 occurrences of the event
35 | event.enumerateDates { (date, stop) in
36 | print(date)
37 | count += 1
38 | if count > 20 {
39 | stop = true
40 | }
41 | }
42 | }
43 | }
44 | ```
45 |
46 | This example shows how to create an `RWMRecurrenceRule` from an RRULE and then list its dates.
47 |
48 | ```swift
49 | import EventKit
50 | import RWMRecurrenceRule
51 |
52 | // Every 4 days, 10 occurrences
53 | let rule = "RRULE:FREQ=DAILY;INTERVAL=4;COUNT=10"
54 | let parser = RWMRuleParser()
55 | if let rules = parser.parse(rule: rule) {
56 | let scheduler = RWMRuleScheduler()
57 | let start = Date()
58 | scheduler.enumerateDates(with: rules, startingFrom: start, using: { (date, stop) in
59 | if let date = date {
60 | print(date)
61 | }
62 | })
63 | }
64 | ```
65 |
66 | Feel free to experiment from Xcode using the project's playground.
67 |
68 | ## Installation
69 |
70 | > _Note_: RWMRecurrenceRule requires Swift 4.1 and Xcode 9.3.
71 |
72 | ### CocoaPods
73 |
74 | [CocoaPods][] is a dependency manager for Cocoa projects. To install RWMRecurrenceRule with CocoaPods:
75 |
76 | 1. Make sure CocoaPods is [installed][CocoaPods Installation]. (RWMRecurrenceRule
77 | requires version 1.0.0 or greater.)
78 |
79 | ```sh
80 | # Using the default Ruby install will require you to use sudo when
81 | # installing and updating gems.
82 | [sudo] gem install cocoapods
83 | ```
84 |
85 | 2. Update your Podfile to include the following:
86 |
87 | ```ruby
88 | use_frameworks!
89 |
90 | target 'YourAppTargetName' do
91 | pod 'RWMRecurrenceRule', '~> 0.0.2'
92 | end
93 | ```
94 |
95 | 3. Run `pod install --repo-update`.
96 |
97 | [CocoaPods]: https://cocoapods.org
98 | [CocoaPods Installation]: https://guides.cocoapods.org/using/getting-started.html#getting-started
99 |
100 | ### Swift Package Manager
101 |
102 | The [Swift Package Manager][] is a tool for managing the distribution of
103 | Swift code.
104 |
105 | 1. Add the following to your `Package.swift` file:
106 |
107 | ```swift
108 | dependencies: [
109 | .package(url: "https://github.com/rmaddy/RWMRecurrenceRule.git", from: "0.0.2")
110 | ]
111 | ```
112 |
113 | 2. Build your project:
114 |
115 | ```sh
116 | $ swift build
117 | ```
118 |
119 | [Swift Package Manager]: https://swift.org/package-manager
120 |
121 | ### Manual
122 |
123 | To install RWNRecurrenceRule as an Xcode sub-project:
124 |
125 | 1. Drag the **RWMRecurrenceRule.xcodeproj** file into your own project.
126 | ([Submodule][], clone, or [download][] the project first.)
127 |
128 | 
129 |
130 | 2. In your target’s **General** tab, click the **+** button under **Linked
131 | Frameworks and Libraries**.
132 |
133 | 3. Select the appropriate **RWMRecurrenceRule.framework** for your platform.
134 |
135 | 4. **Add**.
136 |
137 | Some additional steps are required to install the application on an actual
138 | device:
139 |
140 | 5. In the **General** tab, click the **+** button under **Embedded
141 | Binaries**.
142 |
143 | 6. Select the appropriate **RWMRecurrenceRule.framework** for your platform.
144 |
145 | 7. **Add**.
146 |
147 |
148 | [Xcode]: https://developer.apple.com/xcode/downloads/
149 | [Submodule]: http://git-scm.com/book/en/Git-Tools-Submodules
150 | [download]: https://github.com/rmaddy/RWMRecurrenceRule/archive/master.zip
151 |
152 | ## Issues
153 |
154 | If you find any issues with RWMRecurrenceRule, please [open an issue][]
155 |
156 | [open an issue]: https://github.com/rmaddy/RWMRecurrenceRule/issues/new
157 |
158 | ## License
159 |
160 | RWMRecurrenceRule is available under the MIT license. See [the LICENSE
161 | file](./LICENSE.txt) for more information.
162 |
163 | [EventKit]: https://developer.apple.com/documentation/eventkit
164 | [EKRecurrenceRule]: https://developer.apple.com/documentation/eventkit/ekrecurrencerule
165 | [EKEvent]: https://developer.apple.com/documentation/eventkit/ekevent
166 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | // Note: Run this playground from within the RWMRecurrenceRule project.
2 |
3 | import Foundation
4 | import RWMRecurrenceRule
5 |
6 | // TODO - coming soon
7 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # Be sure to run `pod spec lint RWMRecurrenceRule.podspec' to ensure this is a
3 | # valid spec and to remove all comments including this before submitting the spec.
4 | #
5 |
6 | Pod::Spec.new do |s|
7 |
8 | s.name = "RWMRecurrenceRule"
9 | s.version = "0.0.2"
10 | s.summary = "A library allowing you to create recurrence rules from iCalendar RRULE statements and to iterate the dates of a recurrence rule."
11 |
12 | s.description = <<-DESC
13 | Includes an extension to EKEvent and EKRecurrenceRule as well as custom structures allowing you to iterate the dates of an EKEvent and its recurrence rule. It also allows you to create EKRecurrenceRule instance from a standard iCalendar RRULE.
14 | DESC
15 |
16 | s.homepage = "https://github.com/rmaddy/RWMRecurrenceRule"
17 | s.license = { :type => "MIT", :file => "LICENSE" }
18 | s.author = { "Rick Maddy" => "rick@maddyhome.com" }
19 |
20 | s.ios.deployment_target = "9.0"
21 | s.osx.deployment_target = "10.9"
22 | s.watchos.deployment_target = "2.0"
23 | s.pod_target_xcconfig = {
24 | 'SWIFT_VERSION' => '4.1',
25 | }
26 |
27 | s.source = { :git => "https://github.com/rmaddy/RWMRecurrenceRule.git", :tag => "#{s.version}" }
28 |
29 | s.source_files = "RWMRecurrenceRule/**/*.{swift,h}"
30 | #s.exclude_files = "Classes/Exclude"
31 |
32 | s.public_header_files = "RWMRecurrenceRule/**/*.h"
33 |
34 | s.framework = "Foundation"
35 |
36 | s.requires_arc = true
37 |
38 | end
39 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule.xcodeproj/xcshareddata/xcschemes/RWMRecurrenceRule_iOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
54 |
64 |
65 |
71 |
72 |
73 |
74 |
75 |
76 |
82 |
83 |
89 |
90 |
91 |
92 |
94 |
95 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule.xcodeproj/xcshareddata/xcschemes/RWMRecurrenceRule_macOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
54 |
64 |
65 |
71 |
72 |
73 |
74 |
75 |
76 |
82 |
83 |
89 |
90 |
91 |
92 |
94 |
95 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule.xcodeproj/xcshareddata/xcschemes/RWMRecurrenceRule_watchOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
45 |
46 |
52 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule/Calendar+RWM.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Calendar+RWM.swift
3 | // RWMRecurrenceRule
4 | //
5 | // Created by Richard W Maddy on 5/19/18.
6 | // Copyright © 2018 Maddysoft. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Calendar {
12 | /// Returns the range of the given weekday for the supplied year or month of year.
13 | ///
14 | /// Examples:
15 | /// - To find out how many Tuesdays there are in 2018, pass in `3` for the `weekday` and `2018` for the `year` and the default of `0` for the `month`.
16 | /// - To find out how many Saturdays there are in May of 2018, pass in `7` for the `weekday`, `2018` for the `year`, and `5` for the `month`.
17 | /// - To find out how many Mondays there are in the last month of 2018, pass in `2` for the `weekday`, `2018` for the `year`, and `-1` for the `month`.
18 | ///
19 | /// - Parameters:
20 | /// - weekday: The day of the week. Values range from 1 to 7, with Sunday being 1.
21 | /// - year: A calendar year.
22 | /// - month: A month within the calendar year. The value of `0` means the month is ignored. Negative values start from the last month of the year. `-1` is the last month. `-2` is the next-to-last month, etc.
23 | /// - Returns: A range from `1` through `n` where `n` is the number of times the given weekday appears in the year or month of the year. If `month` is out of range for the year, the result is `nil`.
24 | public func range(of weekday: Int, in year: Int, month: Int = 0) -> ClosedRange? {
25 | if month > 0 {
26 | let comps = DateComponents(year: year, month: month, weekday: weekday, weekdayOrdinal: -1)
27 | if let date = self.date(from: comps) {
28 | let count = self.component(.weekdayOrdinal, from: date)
29 |
30 | return 1...count
31 | }
32 | } else {
33 | // Get first day of year for the given weekday
34 | let startComps = DateComponents(year: year, month: 1, weekday: weekday, weekdayOrdinal: 1)
35 | // Get last day of year for the given weekday
36 | let finishComps = DateComponents(year: year, month: 12, weekday: weekday, weekdayOrdinal: -1)
37 | if let startDate = self.date(from: startComps), let finishDate = self.date(from: finishComps) {
38 | // Get the number of days between the two dates
39 | let days = self.dateComponents([.day], from: startDate, to: finishDate).day!
40 |
41 | return 1...(days / 7 + 1)
42 | }
43 | }
44 |
45 | return nil
46 | }
47 |
48 | /// Converts relative components to normalized components.
49 | ///
50 | /// The following relative components are normalized:
51 | /// - year set, month set, weekday set, weekday ordinal set to +/- instance of weekday within month
52 | /// - year set, no month, weekday set, weekday ordinal set to +/- instance of weekday within year
53 | /// - year set, month set, day set to +/- day of month
54 | /// - year set, no month, day set to +/- day of year
55 | ///
56 | /// All other components are returned as-is.
57 | ///
58 | /// - Parameter components: The relative date components.
59 | /// - Returns: The normalized date components.
60 | func relativeComponents(from components: DateComponents) -> DateComponents {
61 | var newComponents = components
62 |
63 | if let year = components.year {
64 | if let weekday = components.weekday, let ordinal = components.weekdayOrdinal {
65 | if ordinal < 0 {
66 | if let month = components.month {
67 | if let rng = self.range(of: weekday, in: year, month: month) {
68 | newComponents.weekdayOrdinal = rng.count + ordinal + 1
69 | }
70 | } else {
71 | if let rng = self.range(of: weekday, in: year) {
72 | newComponents.weekdayOrdinal = rng.count + ordinal + 1
73 | }
74 | }
75 | } else {
76 | // Calendar already handles positive weekdayOrdinal
77 | }
78 | } else if let day = components.day {
79 | if components.weekday == nil {
80 | if let month = components.month {
81 | if day < 0 {
82 | if let startOfMonth = self.date(from: DateComponents(year: year, month: month, day: 1)),
83 | let daysInMonth = self.range(of: .day, in: .month, for: startOfMonth)?.count {
84 | newComponents.day = daysInMonth + day + 1
85 | }
86 | } else {
87 | // Calendar already handles positive day
88 | }
89 | } else {
90 | if day < 0 {
91 | if let startOfYear = self.date(from: DateComponents(year: year, month: 1, day: 1)),
92 | let daysInYear = self.range(of: .day, in: .year, for: startOfYear)?.count {
93 | newComponents.day = daysInYear + day + 1
94 | }
95 | } else {
96 | // Calendar already handles positive day
97 | }
98 | }
99 | }
100 | }
101 | }
102 |
103 | return newComponents
104 | }
105 |
106 | public func date(fromRelative components: DateComponents) -> Date? {
107 | return self.date(from: self.relativeComponents(from: components))
108 | }
109 |
110 | public func date(_ date: Date, matchesRelativeComponents components: DateComponents) -> Bool {
111 | return self.date(date, matchesComponents: self.relativeComponents(from: components))
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule/EKEvent+RWM.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EKEvent+RWM.swift
3 | // RWMRecurrenceRule
4 | //
5 | // Created by Richard W Maddy on 5/21/18.
6 | // Copyright © 2018 Maddysoft. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import EventKit
11 |
12 | public extension EKEvent {
13 | /// Provides the sequence of dates provided by this event. If the event has no recurrence then only the event's
14 | /// start date is returned. If the event has a recurrence rule, that rule is used to generate the recurring dates.
15 | /// The closure is called once for each date until the either the last date is returned or the enumeration is
16 | /// stopped. If there are no dates from the recurrence rule, then the closed will not be called at all.
17 | ///
18 | /// If the event has a recurrence rule, the event's `startDate` is used as the basis for the resulting dates.
19 | ///
20 | /// The enumeration can be stopped before the event's last recurring date by setting `stop` to `true` in the closure
21 | /// and returning. It is not necessary to set `stop` to `false` to keep the enumeration going.
22 | ///
23 | /// **Note:** EventKit does not provide an API to get its determination of recurring dates from the event. These
24 | /// dates are calculated separately based on the event's recurrence rule.
25 | ///
26 | /// - Parameter block: A closure that is called with each event date
27 | public func enumerateDates(using block: (_ date: Date?, _ stop: inout Bool) -> Void) {
28 | // According to the EventKit documentation, an event can only have 0 or 1 recurrence rules. This logic follows
29 | // that assumption.
30 | if let rules = self.recurrenceRules, rules.count > 0 {
31 | if let rule = RWMRecurrenceRule(recurrenceWith: rules[0]) {
32 | let scheduler = RWMRuleScheduler()
33 | scheduler.mode = .eventKit
34 | scheduler.enumerateDates(with: rule, startingFrom: self.startDate) { (date, stop) in
35 | block(date, &stop)
36 | }
37 | }
38 | } else {
39 | // There is no recurrence rule so return just the event's start date.
40 | var stop = false
41 | block(self.startDate, &stop)
42 | }
43 | }
44 |
45 | /// Returns the next possible event date after the supplied date. If there are no recurrences after the date,
46 | /// the result is `nil`.
47 | ///
48 | /// - Parameter date: The date used to find the next occurrence.
49 | /// - Returns: The next recurrence date or `nil` if there are none after the supplied date.
50 | public func nextRecurrence(after date: Date = Date()) -> Date? {
51 | var result: Date? = nil
52 |
53 | self.enumerateDates { (rdate, stop) in
54 | if let rdate = rdate, rdate > date {
55 | result = rdate
56 | stop = true
57 | }
58 | }
59 |
60 | return result
61 | }
62 |
63 | /// Checks to see if the supplied date is among the recurring dates of the event.
64 | ///
65 | /// - Parameters:
66 | /// - date: The date to check for.
67 | /// - exact: `true` if the full date and time must match, `false` if the time is ignored.
68 | /// - Returns: `true` if the date is part of the event, `false` if not.
69 | public func includes(date: Date, exact: Bool = false) -> Bool {
70 | var result = false
71 |
72 | self.enumerateDates { (rdate, stop) in
73 | if let rdate = rdate {
74 | if (exact && rdate == date) || (!exact && Calendar.current.isDate(rdate, inSameDayAs: date)) {
75 | result = true
76 | stop = true
77 | } else if rdate > date {
78 | stop = true
79 | }
80 | }
81 | }
82 |
83 | return result
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule/EKRecurrenceRule+RWM.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EKRecurrenceRule+RWM.swift
3 | // RWMRecurrenceRule
4 | //
5 | // Created by Richard W Maddy on 5/13/18.
6 | // Copyright © 2018 Maddysoft. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import EventKit
11 |
12 | public extension EKRecurrenceRule {
13 | /// This convenience initializer allows you to create an EKRecurrenceRule from a standard iCalendar RRULE
14 | /// string. Please see https://icalendar.org/iCalendar-RFC-5545/3-3-10-recurrence-rule.html for a reference
15 | /// to the RRULE syntax.
16 | /// Only frequencies of DAILY, WEEKLY, MONTHLY, and YEARLY are supported. Also note that there are many valid
17 | /// RRULE strings that will parse but EventKit may not process correctly.
18 | ///
19 | /// If `rrule` is an invalid RRULE, the result is `nil`.
20 | ///
21 | /// See `RWMRecurrenceRule isEventKitSafe` for details about RRULE values safe to be used with Event Kit.
22 | ///
23 | /// - Parameter rrule: The RRULE string in the form RRULE:FREQUENCY=...
24 | public convenience init?(recurrenceWith rrule: String) {
25 | if let rule = RWMRuleParser().parse(rule: rrule) {
26 | self.init(recurrenceWith: rule)
27 | } else {
28 | return nil
29 | }
30 | }
31 |
32 | /// Creates a new EKRecurrenceRule from a RWMRecurrenceRule. If `rule` can't be converted, the result is `nil`.
33 | ///
34 | /// Note that Event Kit may not properly process some recurrence rules.
35 | ///
36 | /// - Parameter rule: The RWMRecurrenceRule.
37 | public convenience init?(recurrenceWith rule: RWMRecurrenceRule) {
38 | var daysOfTheWeek: [EKRecurrenceDayOfWeek]?
39 | if let dows = rule.daysOfTheWeek {
40 | daysOfTheWeek = []
41 | for dow in dows {
42 | if let ekwd = EKWeekday(rawValue: dow.dayOfTheWeek.rawValue) {
43 | daysOfTheWeek?.append(EKRecurrenceDayOfWeek(dayOfTheWeek: ekwd, weekNumber: dow.weekNumber))
44 | } else {
45 | return nil
46 | }
47 | }
48 | }
49 |
50 | let end: EKRecurrenceEnd?
51 | if let rend = rule.recurrenceEnd {
52 | if let date = rend.endDate {
53 | end = EKRecurrenceEnd(end: date)
54 | } else {
55 | end = EKRecurrenceEnd(occurrenceCount: rend.count)
56 | }
57 | } else {
58 | end = nil
59 | }
60 |
61 | if let frequency = EKRecurrenceFrequency(rawValue: rule.frequency.rawValue) {
62 | self.init(recurrenceWith: frequency, interval: rule.interval ?? 1, daysOfTheWeek: daysOfTheWeek, daysOfTheMonth: rule.daysOfTheMonth as [NSNumber]?, monthsOfTheYear: rule.monthsOfTheYear as [NSNumber]?, weeksOfTheYear: rule.weeksOfTheYear as [NSNumber]?, daysOfTheYear: rule.daysOfTheYear as [NSNumber]?, setPositions: rule.setPositions as [NSNumber]?, end: end)
63 | } else {
64 | return nil
65 | }
66 | }
67 |
68 | /// Returns the RRULE representation. If the sender can't be processed, the result is `nil`.
69 | public var rrule: String? {
70 | if let rule = RWMRecurrenceRule(recurrenceWith: self) {
71 | let parser = RWMRuleParser()
72 |
73 | return parser.rule(from: rule)
74 | } else {
75 | return nil
76 | }
77 | }
78 | }
79 |
80 | public extension RWMRecurrenceRule {
81 | /// Creates a new RWMRecurrenceRule from an EKRecurrenceRule. If `rule` can't be converted, the result is `nil`.
82 | ///
83 | /// - Parameter rule: The EKRecurrenceRule
84 | public init?(recurrenceWith rule: EKRecurrenceRule) {
85 | var daysOfTheWeek: [RWMRecurrenceDayOfWeek]?
86 | if let dows = rule.daysOfTheWeek {
87 | daysOfTheWeek = []
88 | for dow in dows {
89 | if let rwmwd = RWMWeekday(rawValue: dow.dayOfTheWeek.rawValue) {
90 | daysOfTheWeek?.append(RWMRecurrenceDayOfWeek(dayOfTheWeek: rwmwd, weekNumber: dow.weekNumber))
91 | } else {
92 | return nil
93 | }
94 | }
95 | }
96 |
97 | let end: RWMRecurrenceEnd?
98 | if let rend = rule.recurrenceEnd {
99 | if let date = rend.endDate {
100 | end = RWMRecurrenceEnd(end: date)
101 | } else {
102 | end = RWMRecurrenceEnd(occurrenceCount: rend.occurrenceCount)
103 | }
104 | } else {
105 | end = nil
106 | }
107 |
108 | if let frequency = RWMRecurrenceFrequency(rawValue: rule.frequency.rawValue) {
109 | // For weekly recurrence rules with days of the week set, set the rule's firstDay if the current calendar
110 | // starts its week on a day other than Monday.
111 | var firstDay: RWMWeekday? = nil
112 | if frequency == .weekly && daysOfTheWeek != nil && Calendar.current.firstWeekday != 2 {
113 | firstDay = RWMWeekday(rawValue: Calendar.current.firstWeekday)
114 | }
115 |
116 | self.init(recurrenceWith: frequency, interval: rule.interval == 1 ? nil : rule.interval, daysOfTheWeek: daysOfTheWeek, daysOfTheMonth: rule.daysOfTheMonth as! [Int]?, monthsOfTheYear: rule.monthsOfTheYear as! [Int]?, weeksOfTheYear: rule.weeksOfTheYear as! [Int]?, daysOfTheYear: rule.daysOfTheYear as! [Int]?, setPositions: rule.setPositions as! [Int]?, end: end, firstDay: firstDay)
117 | } else {
118 | return nil
119 | }
120 | }
121 |
122 | /*
123 | Sample events created through the iOS Calendar app
124 | A - RRULE FREQ=DAILY;INTERVAL=1;UNTIL=20180629T055959Z
125 | B - RRULE FREQ=WEEKLY;INTERVAL=1;UNTIL=20180822T055959Z
126 | C - RRULE FREQ=WEEKLY;INTERVAL=2;UNTIL=20180922T055959Z
127 | D - RRULE FREQ=MONTHLY;INTERVAL=1;UNTIL=20200522T055959Z
128 | E - RRULE FREQ=YEARLY;INTERVAL=1;UNTIL=20230521T180000Z
129 | F - RRULE FREQ=DAILY;INTERVAL=3;UNTIL=20180722T055959Z
130 | G - RRULE FREQ=WEEKLY;INTERVAL=2;UNTIL=20180822T055959Z;BYDAY=SU,WE,SA;WKST=SU
131 | H - RRULE FREQ=MONTHLY;INTERVAL=2;UNTIL=20190622T055959Z;BYMONTHDAY=10,15,20
132 | I - RRULE FREQ=MONTHLY;INTERVAL=3;UNTIL=20190622T055959Z;BYDAY=TU;BYSETPOS=2
133 | J - RRULE FREQ=MONTHLY;INTERVAL=1;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1
134 | K - RRULE FREQ=MONTHLY;INTERVAL=1;UNTIL=20190622T055959Z;BYDAY=SU,SA;BYSETPOS=3
135 | L - RRULE FREQ=YEARLY;INTERVAL=2;UNTIL=20230622T055959Z;BYMONTH=9,10,11
136 | M - RRULE FREQ=YEARLY;INTERVAL=1;UNTIL=20190622T055959Z;BYMONTH=5,7;BYDAY=1WE
137 | */
138 |
139 | /// Indicates whether the recurrence rule is safe to use with Event Kit and EKRecurrenceRule. Event Kit and the
140 | /// Calendar apps of iOS and macOS provide support for a subset of the possible iCalendar RRULE possibilties.
141 | /// A return value of `true` indicates that this recurrence rule is safe to use with Event Kit. A return value of
142 | /// `false` means the recurrence rule may or may not result in recurring events supported by Event Kit.
143 | ///
144 | /// Event Kit is known to support the following possible types of recurrence rules:
145 | /// Daily with an interval. Example: `RRULE:FREQ=DAILY;INTERVAL=1`
146 | /// Weekly with an interval. Example: `RRULE:FREQ=WEEKLY;INTERVAL=1`
147 | /// Weekly with an interval and specific days of the week. Example: `RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=SU,WE,SA`
148 | /// Monthly with an interval. Example: `RRULE:FREQ=MONTHLY;INTERVAL=1`
149 | /// Monthly with an interval and specific days of the month. Example: `RRULE:FREQ=MONTHLY;INTERVAL=2;BYMONTHDAY=10,15,20`
150 | /// Monthly with an interval and 1st, 2nd, 3rd, 4th, 5th, or last day of the week. Example: `RRULE:FREQ=MONTHLY;INTERVAL=3;BYDAY=TU;BYSETPOS=2`
151 | /// Monthly with an interval and 1st, 2nd, 3rd, 4th, 5th, or last day. Example: `RRULE:FREQ=MONTHLY;INTERVAL=3;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1`
152 | /// Monthly with an interval and 1st, 2nd, 3rd, 4th, 5th, or last weekday. Example: `RRULE:FREQ=MONTHLY;INTERVAL=3;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1`
153 | /// Monthly with an interval and 1st, 2nd, 3rd, 4th, 5th, or last weekend. Example: `RRULE:FREQ=MONTHLY;INTERVAL=3;BYDAY=SU,SA;BYSETPOS=3`
154 | /// Yearly with an interval. Example: `RRULE:FREQ=YEARLY;INTERVAL=1`
155 | /// Yearly with an interval and specific months. Example: `RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=9,10,11`
156 | /// Yearly with an interval, specific months, and 1st, 2nd, 3rd, 4th, 5th, or last day of the week. Example: `RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=9,10,11;BYDAY=1WE`
157 | /// Yearly with an interval, specific months, and 1st, 2nd, 3rd, 4th, 5th, or last day. Example: `RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=9,10,11;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=2`
158 | /// Yearly with an interval, specific months, and 1st, 2nd, 3rd, 4th, 5th, or last weekday. Example: `RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=9,10,11;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=2`
159 | /// Yearly with an interval, specific months, and 1st, 2nd, 3rd, 4th, 5th, or last weekend. Example: `RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=9,10,11;BYDAY=SU,SA;BYSETPOS=-1`
160 | public var isEventKitSafe: Bool {
161 | get {
162 | switch frequency {
163 | case .daily:
164 | // Only allow an interval
165 | if daysOfTheWeek != nil || daysOfTheMonth != nil || daysOfTheYear != nil || weeksOfTheYear != nil || monthsOfTheYear != nil || setPositions != nil {
166 | return false
167 | }
168 | case .weekly:
169 | // Only allow interval and daysOfTheWeek
170 | if daysOfTheMonth != nil || daysOfTheYear != nil || weeksOfTheYear != nil || monthsOfTheYear != nil || setPositions != nil {
171 | return false
172 | }
173 | case .monthly:
174 | // Allow interval, daysOfTheWeek, and daysOfTheMonth
175 | if daysOfTheYear != nil || weeksOfTheYear != nil || monthsOfTheYear != nil {
176 | return false
177 | }
178 | // If daysOfTheWeek is set, ensure no week numbers are set. Also ensure the days represent either a
179 | // single day, all days (SU-SA), all weekdays (MO-FR), or the weekend (SA and SU).
180 | if let days = daysOfTheWeek {
181 | var weekdays = Set()
182 | for day in days {
183 | if day.weekNumber != 0 {
184 | return false
185 | }
186 | weekdays.insert(day.dayOfTheWeek)
187 | }
188 |
189 | if weekdays.count == 5 {
190 | if weekdays.contains(.saturday) || weekdays.contains(.sunday) {
191 | return false
192 | }
193 | } else if weekdays.count == 2 {
194 | if !(weekdays.contains(.saturday) && weekdays.contains(.sunday)) {
195 | return false
196 | }
197 | } else if weekdays.count != 1 && weekdays.count != 7 {
198 | return false
199 | }
200 | }
201 | // If setPositions is set, only allow 1 value and make sure it is -1, 1, 2, 3, 4, or 5.
202 | // Only allow setPositions if there are days of the week.
203 | if let poss = setPositions {
204 | let daysCount = daysOfTheWeek?.count ?? 0
205 | if poss.count > 1 || daysCount == 0 {
206 | return false
207 | } else {
208 | if poss[0] < -1 || poss[0] > 5 {
209 | return false
210 | }
211 | }
212 | }
213 | case .yearly:
214 | // Allow interval, daysOfTheWeek, and monthsOfTheYear
215 | if daysOfTheMonth != nil || daysOfTheYear != nil || weeksOfTheYear != nil {
216 | return false
217 | }
218 | // If daysOfTheWeek is set, ensure no week numbers are set unless for a single weekday. Also ensure the
219 | // days represent either a single day, all days (SU-SA), all weekdays (MO-FR), or the weekend (SA and SU).
220 | if let days = daysOfTheWeek {
221 | var weekdays = Set()
222 | for day in days {
223 | if day.weekNumber != 0 && days.count != 1 {
224 | return false
225 | }
226 | weekdays.insert(day.dayOfTheWeek)
227 | }
228 |
229 | if weekdays.count == 5 {
230 | if weekdays.contains(.saturday) || weekdays.contains(.sunday) {
231 | return false
232 | }
233 | } else if weekdays.count == 2 {
234 | if !(weekdays.contains(.saturday) && weekdays.contains(.sunday)) {
235 | return false
236 | }
237 | } else if weekdays.count != 1 && weekdays.count != 7 {
238 | return false
239 | }
240 | }
241 | // If setPositions is set, only allow 1 value and make sure it is -1, 1, 2, 3, 4, or 5.
242 | // Only allow setPositions if there is more than one day of the week.
243 | if let poss = setPositions {
244 | let daysCount = daysOfTheWeek?.count ?? 0
245 | if poss.count > 1 || daysCount <= 1 {
246 | return false
247 | } else {
248 | if poss[0] < -1 || poss[0] > 5 {
249 | return false
250 | }
251 | }
252 | }
253 | }
254 |
255 | return true
256 | }
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule/RWMRecurrenceRule.h:
--------------------------------------------------------------------------------
1 | //
2 | // RWMRecurrenceRule.h
3 | // RWMRecurrenceRule
4 | //
5 | // Created by Richard W Maddy on 5/26/18.
6 | // Copyright © 2018 Maddysoft. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for RWMRecurrenceRule.
12 | FOUNDATION_EXPORT double RWMRecurrenceRuleVersionNumber;
13 |
14 | //! Project version string for RWMRecurrenceRule.
15 | FOUNDATION_EXPORT const unsigned char RWMRecurrenceRuleVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule/RWMRecurrenceRule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RWMRRule.swift
3 | // RWMRecurrenceRule
4 | //
5 | // Created by Richard W Maddy on 5/13/18.
6 | // Copyright © 2018 Maddysoft. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Defines frequencies for recurrence rules.
12 | ///
13 | /// - daily: Indicates a daily recurrence rule.
14 | /// - weekly: Indicates a weekly recurrence rule.
15 | /// - monthly: Indicates a monthly recurrence rule.
16 | /// - yearly: Indicates a yearly recurrence rule.
17 | public enum RWMRecurrenceFrequency: Int {
18 | case daily = 0
19 | case weekly = 1
20 | case monthly = 2
21 | case yearly = 3
22 | }
23 |
24 | /// The RWMRecurrenceEnd struct defines the end of a recurrence rule defined by an RWMRecurrenceRule object.
25 | /// The recurrence end can be specified by a date (date-based) or by a maximum count of occurrences (count-based).
26 | /// An event which is set to never end should have its RWMRecurrenceEnd set to nil.
27 | public struct RWMRecurrenceEnd: Equatable {
28 | /// The end date of the recurrence end, or `nil` if the recurrence end is count-based.
29 | public let endDate: Date?
30 | /// The occurrence count of the recurrence end, or `0` if the recurrence end is date-based.
31 | public let count: Int
32 |
33 | /// Initializes and returns a date-based recurrence end with a given end date.
34 | ///
35 | /// - Parameter end: The end date.
36 | public init(end: Date) {
37 | self.endDate = end
38 | self.count = 0
39 | }
40 |
41 | /// Initializes and returns a count-based recurrence end with a given maximum occurrence count.
42 | ///
43 | /// - Parameter occurrenceCount: The maximum occurrence count.
44 | public init(occurrenceCount: Int) {
45 | self.endDate = nil
46 | if occurrenceCount > 0 {
47 | self.count = occurrenceCount
48 | } else {
49 | fatalError("occurrenceCount must be > 0")
50 | }
51 | }
52 |
53 | public static func==(lhs: RWMRecurrenceEnd, rhs: RWMRecurrenceEnd) -> Bool {
54 | if let ldate = lhs.endDate {
55 | if let rdate = rhs.endDate {
56 | return ldate == rdate // both are dates
57 | } else {
58 | return false // one date and one count
59 | }
60 | } else {
61 | if rhs.endDate != nil {
62 | return false // one date and one count
63 | } else {
64 | return lhs.count == rhs.count // both are counts
65 | }
66 | }
67 | }
68 | }
69 |
70 | public enum RWMWeekday: Int {
71 | case sunday = 1
72 | case monday = 2
73 | case tuesday = 3
74 | case wednesday = 4
75 | case thursday = 5
76 | case friday = 6
77 | case saturday = 7
78 | }
79 |
80 | /// The `RWMRecurrenceDayOfWeek` struct represents a day of the week for use with an `RWMRecurrenceRule` object.
81 | /// A day of the week can optionally have a week number, indicating a specific day in the recurrence rule’s frequency.
82 | /// For example, a day of the week with a day value of `Tuesday` and a week number of `2` would represent the second
83 | /// Tuesday of every month in a monthly recurrence rule, and the second Tuesday of every year in a yearly recurrence
84 | /// rule. A day of the week with a week number of `0` ignores its week number.
85 | public struct RWMRecurrenceDayOfWeek: Equatable {
86 | /// The day of the week.
87 | public let dayOfTheWeek: RWMWeekday
88 | /// The week number of the day of the week.
89 | ///
90 | /// Values range from `-53` to `53`. A negative value indicates a value from the end of the range. `0` indicates the week number is irrelevant.
91 | public let weekNumber: Int
92 |
93 | /// Initializes and returns a day of the week with a given day and week number.
94 | ///
95 | /// - Parameters:
96 | /// - dayOfTheWeek: The day of the week.
97 | /// - weekNumber: The week number.
98 | public init(dayOfTheWeek: RWMWeekday, weekNumber: Int) {
99 | self.dayOfTheWeek = dayOfTheWeek
100 | if weekNumber < -53 || weekNumber > 53 {
101 | fatalError("weekNumber must be -53 to 53")
102 | } else {
103 | self.weekNumber = weekNumber
104 | }
105 | }
106 |
107 | /// Creates and returns a day of the week with a given day.
108 | ///
109 | /// - Parameter dayOfTheWeek: The day of the week.
110 | public init(_ dayOfTheWeek: RWMWeekday) {
111 | self.init(dayOfTheWeek: dayOfTheWeek, weekNumber: 0)
112 | }
113 |
114 | /// Creates and returns an autoreleased day of the week with a given day and week number.
115 | ///
116 | /// - Parameters:
117 | /// - dayOfTheWeek: The day of the week.
118 | /// - weekNumber: The week number.
119 | public init(_ dayOfTheWeek: RWMWeekday, weekNumber: Int) {
120 | self.init(dayOfTheWeek: dayOfTheWeek, weekNumber: weekNumber)
121 | }
122 |
123 | public static func==(lhs: RWMRecurrenceDayOfWeek, rhs: RWMRecurrenceDayOfWeek) -> Bool {
124 | return lhs.dayOfTheWeek == rhs.dayOfTheWeek && lhs.weekNumber == rhs.weekNumber
125 | }
126 | }
127 |
128 | /// The `RWMRecurrenceRule` class is used to describe the recurrence pattern for a recurring event.
129 | public struct RWMRecurrenceRule: Equatable {
130 | /// The frequency of the recurrence rule.
131 | let frequency: RWMRecurrenceFrequency
132 | /// Specifies how often the recurrence rule repeats over the unit of time indicated by its frequency. For example, a recurrence rule with a frequency type of `.weekly` and an interval of `2` repeats every two weeks.
133 | let interval: Int?
134 | /// Indicates which day of the week the recurrence rule treats as the first day of the week. No value indicates that this property is not set for the recurrence rule.
135 | let firstDayOfTheWeek: RWMWeekday?
136 | /// The days of the week associated with the recurrence rule, as an array of `RWMRecurrenceDayOfWeek` objects.
137 | let daysOfTheWeek: [RWMRecurrenceDayOfWeek]?
138 | /// The days of the month associated with the recurrence rule, as an array of `Int`. Values can be from 1 to 31 and from -1 to -31. This property value is invalid with a frequency type of `.weekly`.
139 | let daysOfTheMonth: [Int]?
140 | /// The days of the year associated with the recurrence rule, as an array of `Int`. Values can be from 1 to 366 and from -1 to -366. This property value is valid only for recurrence rules initialized with a frequency type of `.yearly`.
141 | let daysOfTheYear: [Int]?
142 | /// The weeks of the year associated with the recurrence rule, as an array of `Int` objects. Values can be from 1 to 53 and from -1 to -53. This property value is valid only for recurrence rules initialized with specific weeks of the year and a frequency type of `.yearly`.
143 | let weeksOfTheYear: [Int]?
144 | /// The months of the year associated with the recurrence rule, as an array of `Int` objects. Values can be from 1 to 12. This property value is valid only for recurrence rules initialized with specific months of the year and a frequency type of `.yearly`.
145 | let monthsOfTheYear: [Int]?
146 | /// An array of ordinal numbers that filters which recurrences to include in the recurrence rule’s frequency. For example, a yearly recurrence rule that has a daysOfTheWeek value that specifies Monday through Friday, and a setPositions array containing 2 and -1, occurs only on the second weekday and last weekday of every year.
147 | let setPositions: [Int]?
148 | /// Indicates when the recurrence rule ends. This can be represented by an end date or a number of occurrences.
149 | let recurrenceEnd: RWMRecurrenceEnd?
150 |
151 | /// Initializes and returns a simple recurrence rule with a given frequency, interval, and end.
152 | ///
153 | /// - Parameters:
154 | /// - type: Initializes and returns a simple recurrence rule with a given frequency, interval, and end.
155 | /// - interval: The interval between instances of this recurrence. For example, a weekly recurrence rule with an interval of `2` occurs every other week. Must be greater than `0`.
156 | /// - end: The end of the recurrence rule.
157 | public init?(recurrenceWith type: RWMRecurrenceFrequency, interval: Int?, end: RWMRecurrenceEnd?) {
158 | self.init(recurrenceWith: type, interval: interval, daysOfTheWeek: nil, daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: nil, end: end, firstDay: nil)
159 | }
160 |
161 | /// Initializes and returns a recurrence rule with a given frequency and additional scheduling information.
162 | ///
163 | /// Returns `nil` is any invalid parameters are provided.
164 | ///
165 | /// Negative value indicate counting backwards from the end of the recurrence rule's frequency.
166 | ///
167 | /// - Parameters:
168 | /// - type: The frequency of the recurrence rule. Can be daily, weekly, monthly, or yearly.
169 | /// - interval: The interval between instances of this recurrence. For example, a weekly recurrence rule with an interval of `2` occurs every other week. Must be greater than `0`.
170 | /// - days: The days of the week that the event occurs, as an array of `RWMRecurrenceDayOfWeek` objects.
171 | /// - monthDays: The days of the month that the event occurs, as an array of `Int`. Values can be from 1 to 31 and from -1 to -31. This parameter is not valid for recurrence rules of type `.weekly`.
172 | /// - months: The months of the year that the event occurs, as an array of `Int`. Values can be from 1 to 12.
173 | /// - weeksOfTheYear: The weeks of the year that the event occurs, as an array of `Int`. Values can be from 1 to 53 and from -1 to -53. This parameter is only valid for recurrence rules of type `.yearly`.
174 | /// - daysOfTheYear: The days of the year that the event occurs, as an array of `Int`. Values can be from 1 to 366 and from -1 to -366. This parameter is only valid for recurrence rules of type `.yearly`.
175 | /// - setPositions: An array of ordinal numbers that filters which recurrences to include in the recurrence rule’s frequency. See `setPositions` for more information.
176 | /// - end: The end of the recurrence rule.
177 | /// - firstDay: Indicates what day of the week to be used as the first day of a week. Defaults to Monday.
178 | public init?(recurrenceWith type: RWMRecurrenceFrequency, interval: Int?, daysOfTheWeek days: [RWMRecurrenceDayOfWeek]?, daysOfTheMonth monthDays: [Int]?, monthsOfTheYear months: [Int]?, weeksOfTheYear: [Int]?, daysOfTheYear: [Int]?, setPositions: [Int]?, end: RWMRecurrenceEnd?, firstDay: RWMWeekday?) {
179 | // NOTE - See https://icalendar.org/iCalendar-RFC-5545/3-3-10-recurrence-rule.html
180 |
181 | if let interval = interval, interval <= 0 { return nil } // If specified, INTERVAL must be 1 or more
182 | if let days = days {
183 | // In daily or weekly mode or in yearly mode with week numbers, the days should not have a week number.
184 | if (type != .monthly && type != .yearly) || (type == .yearly && weeksOfTheYear != nil) {
185 | for day in days {
186 | if day.weekNumber != 0 { return nil }
187 | }
188 | }
189 | }
190 | if let daysOfMonth = monthDays {
191 | guard type != .weekly else { return nil }
192 |
193 | for day in daysOfMonth {
194 | if day < -31 || day > 31 || day == 0 { return nil }
195 | }
196 | }
197 | if let monthsOfYear = months {
198 | for month in monthsOfYear {
199 | if month < 1 || month > 12 { return nil }
200 | }
201 | }
202 | if let weeksOfTheYear = weeksOfTheYear {
203 | guard type == .yearly else { return nil }
204 |
205 | for week in weeksOfTheYear {
206 | if week < -53 || week > 53 || week == 0 { return nil }
207 | }
208 | }
209 | if let daysOfTheYear = daysOfTheYear {
210 | // Also supported by secondly, minutely, and hourly
211 | guard type == .yearly else { return nil }
212 |
213 | for day in daysOfTheYear {
214 | if day < -366 || day > 366 || day == 0 { return nil }
215 | }
216 | }
217 | if let setPositions = setPositions {
218 | for pos in setPositions {
219 | if pos < -366 || pos > 366 || pos == 0 { return nil }
220 | }
221 | }
222 |
223 | self.frequency = type
224 | self.interval = interval
225 | self.firstDayOfTheWeek = firstDay
226 | self.daysOfTheWeek = days
227 | self.daysOfTheMonth = monthDays
228 | self.daysOfTheYear = daysOfTheYear
229 | self.weeksOfTheYear = weeksOfTheYear
230 | self.monthsOfTheYear = months
231 | self.setPositions = setPositions
232 | self.recurrenceEnd = end
233 | }
234 |
235 | public static func==(lhs: RWMRecurrenceRule, rhs: RWMRecurrenceRule) -> Bool {
236 | return
237 | lhs.frequency == rhs.frequency &&
238 | lhs.interval == rhs.interval &&
239 | lhs.firstDayOfTheWeek == rhs.firstDayOfTheWeek &&
240 | lhs.daysOfTheWeek == rhs.daysOfTheWeek &&
241 | lhs.daysOfTheMonth == rhs.daysOfTheMonth &&
242 | lhs.daysOfTheYear == rhs.daysOfTheYear &&
243 | lhs.weeksOfTheYear == rhs.weeksOfTheYear &&
244 | lhs.monthsOfTheYear == rhs.monthsOfTheYear &&
245 | lhs.setPositions == rhs.setPositions &&
246 | lhs.recurrenceEnd == rhs.recurrenceEnd
247 | }
248 | }
249 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule/RWMRuleParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RWMRRule.swift
3 | // RWMRecurrenceRule
4 | //
5 | // Created by Richard W Maddy on 5/13/18.
6 | // Copyright © 2018 Maddysoft. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class RWMRuleParser {
12 | private lazy var untilFormat: DateFormatter = {
13 | let df = DateFormatter()
14 | df.locale = Locale(identifier: "en_US_POSIX")
15 | df.timeZone = TimeZone(secondsFromGMT: 0)
16 | df.dateFormat = "yyyyMMdd'T'HHmmssX"
17 | return df
18 | }()
19 |
20 | public init() {
21 | }
22 |
23 | /// Compares two RRULE strings to see if they have the same components. The components do not need to be in the
24 | /// same order. Any `UNTIL` clause is ignored since the date can be in a different format.
25 | ///
26 | /// - Parameters:
27 | /// - left: The first RRULE string.
28 | /// - right: The second RRULE string.
29 | /// - Returns: `true` if the two rules have the same components, ignoring order and any `UNTIL` clause. `false` if different.
30 | public func compare(rule left: String, to right: String) -> Bool {
31 | var leftParts = split(rule: left).sorted()
32 | var rightParts = split(rule: right).sorted()
33 | if leftParts.first(where: { $0.hasPrefix("UNTIL") }) != nil && rightParts.first(where: { $0.hasPrefix("UNTIL")}) != nil {
34 | leftParts = leftParts.filter { !$0.hasPrefix("UNTIL") }
35 | rightParts = leftParts.filter { !$0.hasPrefix("UNTIL") }
36 | }
37 |
38 | return leftParts == rightParts
39 | }
40 |
41 | private func split(rule: String) -> [String] {
42 | var r = rule.uppercased()
43 | if r.hasPrefix("RRULE:") {
44 | r.removeFirst(6)
45 | }
46 |
47 | let parts = r.components(separatedBy: ";")
48 |
49 | return parts
50 | }
51 |
52 | /// Parses an RRULE string returning a `RWMRecurrenceRule`.
53 | ///
54 | /// Valid strings:
55 | /// - The RRULE string may optionally begin with `RRULE:`.
56 | /// - There must be 1 `FREQ=` followed by either `DAILY`, `WEEKLY`, `MONTHLY`, `YEARLY`.
57 | /// - There may be 1 `COUNT=` followed by a positive integer.
58 | /// - There may be 1 `UNTIL=` followed by a date. The date may be in one of these formats: "yyyyMMdd'T'HHmmssX", "yyyyMMdd'T'HHmmss", "'TZID'=VV:yyyyMMdd'T'HHmmss", "yyyyMMdd".
59 | /// - Only 1 of either `COUNT` or `UNTIL` is allowed, not both.
60 | /// - There may be 1 `INTERVAL=` followed by a positive integer.
61 | /// - There may be 1 `BYMONTH=` followed by a comma separated list of 1 or more month numbers in the range 1 to 12, or -12 to -1.
62 | /// - There may be 1 `BYDAY=` followed by a comma separated list of 1 or more days of the week, each optionally preceded by a week number. Days of the week must be `SU`, `MO`, `TU`, `WE`, `TH`, `FR`, or `SA`. Week numbers must be in the range 1 to 5 or -5 to -1.
63 | /// - There may be 1 `BYMONTHDAY=` followed by a comma separated list of days of the month in the range 1 to 31 or -31 to -1.
64 | /// - There may be 1 `BYYEARDAY=` followed by a comma separated list of days of the year in the range 1 to 366 or -366 to -1.
65 | /// - There may be 1 `BYWEEKNO=` followed by a comma separated list of week numbers in the range 1 to 53 or -53 to -1.
66 | /// - There may be 1 `WKST=` followed by a day of the week. Days of the week must be `SU`, `MO`, `TU`, `WE`, `TH`, `FR`, or `SA`.
67 | /// - There may be 1 `BYSETPOS=` following by a comma separated list of positive integers.
68 | /// - Each clause must be separated by a semicolon (`;`). No trailing semicolon should be used.
69 | ///
70 | /// - Parameter rule: The RRULE string.
71 | /// - Returns: The resulting recurrence rule. If the RRULE string is invalid in any way, the result is `nil`.
72 | public func parse(rule: String) -> RWMRecurrenceRule? {
73 | var frequency: RWMRecurrenceFrequency? = nil
74 | var interval: Int? = nil
75 | var firstDayOfTheWeek: RWMWeekday? = nil
76 | var daysOfTheWeek: [RWMRecurrenceDayOfWeek]? = nil
77 | var daysOfTheMonth: [Int]? = nil
78 | var daysOfTheYear: [Int]? = nil
79 | var weeksOfTheYear: [Int]? = nil
80 | var monthsOfTheYear: [Int]? = nil
81 | var setPositions: [Int]? = nil
82 | var recurrenceEnd: RWMRecurrenceEnd? = nil
83 |
84 | let parts = split(rule: rule)
85 | for part in parts {
86 | let varval = part.components(separatedBy: "=")
87 | guard varval.count == 2 else { return nil }
88 |
89 | switch varval[0] {
90 | case "FREQ":
91 | guard frequency == nil else { return nil } // only allowed one FREQ
92 | frequency = parse(frequency: varval[1])
93 | guard frequency != nil else { return nil } // invalid FREQ value
94 | case "COUNT":
95 | guard recurrenceEnd == nil else { return nil } // only one of either COUNT or UNTIL, not both
96 | recurrenceEnd = parse(count: varval[1])
97 | guard recurrenceEnd != nil else { return nil } // invalid COUNT
98 | case "UNTIL":
99 | guard recurrenceEnd == nil else { return nil } // only one of either COUNT or UNTIL, not both
100 | recurrenceEnd = parse(until: varval[1])
101 | guard recurrenceEnd != nil else { return nil } // invalid UNTIL
102 | case "INTERVAL":
103 | guard interval == nil else { return nil } // only allowed one INTERVAL
104 | interval = parse(interval: varval[1])
105 | guard interval != nil else { return nil } // invalid INTERVAL
106 | case "BYMONTH":
107 | guard monthsOfTheYear == nil else { return nil } // only allowed one BYMONTH
108 | monthsOfTheYear = parse(byMonth: varval[1])
109 | guard monthsOfTheYear != nil else { return nil } // invalid BYMONTH
110 | case "BYDAY":
111 | guard daysOfTheWeek == nil else { return nil } // only allowed one BYDAY
112 | daysOfTheWeek = parse(byDay: varval[1])
113 | guard daysOfTheWeek != nil else { return nil } // invalid BYDAY
114 | case "WKST":
115 | guard firstDayOfTheWeek == nil else { return nil } // only allowed one WKST
116 | firstDayOfTheWeek = parse(byWeekStart: varval[1])
117 | guard firstDayOfTheWeek != nil else { return nil } // invalid WKST
118 | case "BYMONTHDAY":
119 | guard daysOfTheMonth == nil else { return nil } // only allowed one BYMONTHDAY
120 | daysOfTheMonth = parse(byMonthDay: varval[1])
121 | guard daysOfTheMonth != nil else { return nil } // invalid BYMONTHDAY
122 | case "BYYEARDAY":
123 | guard daysOfTheYear == nil else { return nil } // only allowed one BYYEARDAY
124 | daysOfTheYear = parse(byYearDay: varval[1])
125 | guard daysOfTheYear != nil else { return nil } // invalid BYYEARDAY
126 | case "BYWEEKNO":
127 | guard weeksOfTheYear == nil else { return nil } // only allowed one BYWEEKNO
128 | weeksOfTheYear = parse(byWeekNo: varval[1])
129 | guard weeksOfTheYear != nil else { return nil } // invalid BYWEEKNO
130 | case "BYSETPOS":
131 | guard setPositions == nil else { return nil } // only allowed one BYSETPOS
132 | setPositions = parse(bySetPosition: varval[1])
133 | guard setPositions != nil else { return nil } // invalid BYSETPOS
134 | /* Not supported by EKRecurrenceRule
135 | case "BYHOUR":
136 | return nil
137 | case "BYMINUTE":
138 | return nil
139 | case "BYSECOND":
140 | return nil
141 | */
142 | default:
143 | return nil
144 | }
145 | }
146 |
147 | if let frequency = frequency {
148 | return RWMRecurrenceRule(recurrenceWith: frequency, interval: interval, daysOfTheWeek: daysOfTheWeek, daysOfTheMonth: daysOfTheMonth, monthsOfTheYear: monthsOfTheYear, weeksOfTheYear: weeksOfTheYear, daysOfTheYear: daysOfTheYear, setPositions: setPositions, end: recurrenceEnd, firstDay: firstDayOfTheWeek)
149 | } else {
150 | return nil // no FREQ
151 | }
152 | }
153 |
154 | /// Returns the RRULE string represented by the provided recurrence rule.
155 | ///
156 | /// - Parameter from: The recurrence rule.
157 | /// - Returns: The RRULE string.
158 | public func rule(from: RWMRecurrenceRule) -> String {
159 | var parts = [String]()
160 | parts.append("FREQ=\(string(from: from.frequency))")
161 |
162 | if let interval = from.interval {
163 | parts.append("INTERVAL=\(interval)")
164 | }
165 | if let end = from.recurrenceEnd {
166 | if let date = end.endDate {
167 | parts.append("UNTIL=\(untilFormat.string(from: date))")
168 | } else {
169 | parts.append("COUNT=\(end.count)")
170 | }
171 | }
172 | if let wkst = from.firstDayOfTheWeek {
173 | parts.append("WKST=\(string(from: wkst))")
174 | }
175 | if let nums = from.weeksOfTheYear {
176 | parts.append("BYWEEKNO=\(string(from: nums))")
177 | }
178 | if let days = from.daysOfTheWeek {
179 | parts.append("BYDAY=\(string(from: days))")
180 | }
181 | if let nums = from.monthsOfTheYear {
182 | parts.append("BYMONTH=\(string(from: nums))")
183 | }
184 | if let nums = from.daysOfTheMonth {
185 | parts.append("BYMONTHDAY=\(string(from: nums))")
186 | }
187 | if let nums = from.daysOfTheYear {
188 | parts.append("BYYEARDAY=\(string(from: nums))")
189 | }
190 | if let nums = from.setPositions {
191 | parts.append("BYSETPOS=\(string(from: nums))")
192 | }
193 |
194 | return "RRULE:" + parts.joined(separator: ";")
195 | }
196 |
197 | private func parse(frequency: String) -> RWMRecurrenceFrequency? {
198 | switch frequency {
199 | case "DAILY":
200 | return .daily
201 | case "WEEKLY":
202 | return .weekly
203 | case "MONTHLY":
204 | return .monthly
205 | case "YEARLY":
206 | return .yearly
207 | case "HOURLY":
208 | return nil // not supported by EKRecurrenceRule
209 | case "MINUTELY":
210 | return nil // not supported by EKRecurrenceRule
211 | case "SECONDLY":
212 | return nil // not supported by EKRecurrenceRule
213 | default:
214 | return nil
215 | }
216 | }
217 |
218 | private func string(from: RWMRecurrenceFrequency) -> String {
219 | switch from {
220 | case .daily:
221 | return "DAILY"
222 | case .weekly:
223 | return "WEEKLY"
224 | case .monthly:
225 | return "MONTHLY"
226 | case .yearly:
227 | return "YEARLY"
228 | }
229 | }
230 |
231 | private func parse(count: String) -> RWMRecurrenceEnd? {
232 | if let cnt = Int(count) {
233 | return RWMRecurrenceEnd(occurrenceCount: cnt)
234 | } else {
235 | return nil
236 | }
237 | }
238 |
239 | private func parse(until: String) -> RWMRecurrenceEnd? {
240 | let df = DateFormatter()
241 | df.locale = Locale(identifier: "en_US_POSIX")
242 | for format in [ "yyyyMMdd'T'HHmmssX", "yyyyMMdd'T'HHmmss", "'TZID'=VV:yyyyMMdd'T'HHmmss", "yyyyMMdd" ] {
243 | df.dateFormat = format
244 | if let date = df.date(from: until) {
245 | return RWMRecurrenceEnd(end: date)
246 | }
247 | }
248 |
249 | return nil
250 | }
251 |
252 | private func parse(interval: String) -> Int? {
253 | if let cnt = Int(interval) {
254 | return cnt
255 | } else {
256 | return nil
257 | }
258 | }
259 |
260 | private func parseNumberList(_ list: String) -> [Int]? {
261 | var res = [Int]()
262 | let parts = list.components(separatedBy: ",")
263 | for part in parts {
264 | if let num = Int(part) {
265 | res.append(num)
266 | } else {
267 | return nil
268 | }
269 | }
270 |
271 | return res
272 | }
273 |
274 | private func string(from: [Int]) -> String {
275 | return from.map { String($0) }.joined(separator: ",")
276 | }
277 |
278 | private func parse(byMonth: String) -> [Int]? {
279 | return parseNumberList(byMonth)
280 | }
281 |
282 | private func parse(byWeekStart: String) -> RWMWeekday? {
283 | switch byWeekStart {
284 | case "SU":
285 | return .sunday
286 | case "MO":
287 | return .monday
288 | case "TU":
289 | return .tuesday
290 | case "WE":
291 | return .wednesday
292 | case "TH":
293 | return .thursday
294 | case "FR":
295 | return .friday
296 | case "SA":
297 | return .saturday
298 | default:
299 | return nil
300 | }
301 | }
302 |
303 | private func string(from: RWMWeekday) -> String {
304 | switch from {
305 | case .sunday:
306 | return "SU"
307 | case .monday:
308 | return "MO"
309 | case .tuesday:
310 | return "TU"
311 | case .wednesday:
312 | return "WE"
313 | case .thursday:
314 | return "TH"
315 | case .friday:
316 | return "FR"
317 | case .saturday:
318 | return "SA"
319 | }
320 | }
321 |
322 | private func parse(byDay: String) -> [RWMRecurrenceDayOfWeek]? {
323 | var res = [RWMRecurrenceDayOfWeek]()
324 | let parts = byDay.components(separatedBy: ",")
325 | for part in parts {
326 | let scanner = Scanner(string: part)
327 | var count = 0
328 | scanner.scanInt(&count)
329 | var weekday: NSString?
330 | if scanner.scanCharacters(from: .alphanumerics, into: &weekday) && scanner.isAtEnd {
331 | if let weekday = weekday, let dow = parse(byWeekStart: weekday as String) {
332 | let rec = count == 0 ? RWMRecurrenceDayOfWeek(dow) : RWMRecurrenceDayOfWeek(dow, weekNumber: count)
333 | res.append(rec)
334 | } else {
335 | return nil
336 | }
337 | } else {
338 | return nil
339 | }
340 | }
341 |
342 | return res
343 | }
344 |
345 | private func string(from: [RWMRecurrenceDayOfWeek]) -> String {
346 | return from.map {
347 | var res = ""
348 | if $0.weekNumber != 0 {
349 | res += String($0.weekNumber)
350 | }
351 | res += string(from: $0.dayOfTheWeek)
352 | return res
353 | }.joined(separator: ",")
354 | }
355 |
356 | private func parse(byMonthDay: String) -> [Int]? {
357 | return parseNumberList(byMonthDay)
358 | }
359 |
360 | private func parse(byYearDay: String) -> [Int]? {
361 | return parseNumberList(byYearDay)
362 | }
363 |
364 | private func parse(byWeekNo: String) -> [Int]? {
365 | return parseNumberList(byWeekNo)
366 | }
367 |
368 | private func parse(bySetPosition: String) -> [Int]? {
369 | return parseNumberList(bySetPosition)
370 | }
371 | }
372 |
--------------------------------------------------------------------------------
/RWMRecurrenceRuleTests/CalendarTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CalendarTests.swift
3 | // RWMRecurrenceRuleTests
4 | //
5 | // Created by Richard W Maddy on 5/19/18.
6 | // Copyright © 2018 Maddysoft. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class CalendarTests: XCTestCase {
12 | func testWeeklyMonth() {
13 | let results2018: [[Int]] = [
14 | [4,5,5,5,4,4,4], // Jan
15 | [4,4,4,4,4,4,4], // Feb
16 | [4,4,4,4,5,5,5], // Mar
17 | [5,5,4,4,4,4,4], // Apr
18 | [4,4,5,5,5,4,4], // May
19 | [4,4,4,4,4,5,5], // Jun
20 | [5,5,5,4,4,4,4], // Jul
21 | [4,4,4,5,5,5,4], // Aug
22 | [5,4,4,4,4,4,5], // Sep
23 | [4,5,5,5,4,4,4], // Oct
24 | [4,4,4,4,5,5,4], // Nov
25 | [5,5,4,4,4,4,5] // Dec
26 | ]
27 |
28 | let cal = Calendar(identifier: .iso8601)
29 |
30 | for (month,monthResults) in results2018.enumerated() {
31 | for (weekday,result) in monthResults.enumerated() {
32 | if let range = cal.range(of: weekday + 1, in: 2018, month: month + 1) {
33 | XCTAssert(range.count == result, "Incorrect result \(range.count) (expected \(result)) for \(cal.weekdaySymbols[weekday]) \(cal.monthSymbols[month]) 2018")
34 | } else {
35 | XCTAssert(false, "Nil result for \(cal.weekdaySymbols[weekday]) \(cal.monthSymbols[month]) 2018")
36 | }
37 | }
38 | }
39 | }
40 |
41 | func testWeeklyYear() {
42 | let results: [Int: [Int]] = [
43 | 2018: [52,53,52,52,52,52,52],
44 | 2019: [52,52,53,52,52,52,52],
45 | 2020: [52,52,52,53,53,52,52],
46 | ]
47 |
48 | let cal = Calendar(identifier: .iso8601)
49 |
50 | for yearData in results {
51 | for (weekday,result) in yearData.value.enumerated() {
52 | if let range = cal.range(of: weekday + 1, in: yearData.key) {
53 | XCTAssert(range.count == result, "Incorrect result \(range.count) (expected \(result)) for \(cal.weekdaySymbols[weekday]) \(yearData.key)")
54 | } else {
55 | XCTAssert(false, "Nil result for \(cal.weekdaySymbols[weekday]) \(yearData.key)")
56 | }
57 | }
58 | }
59 | }
60 |
61 | func checkRelativeMonthDate(year: Int, month: Int, weekday: Int, ordinal: Int, resultDay: Int) {
62 | let cal = Calendar(identifier: .iso8601)
63 |
64 | let comps = DateComponents(year: year, month: month, weekday: weekday, weekdayOrdinal: ordinal)
65 | let result = cal.date(from: DateComponents(year: year, month: month, day: resultDay))!
66 | if let date = cal.date(fromRelative: comps) {
67 | XCTAssert(date == result, "Expected \(result), got \(date)")
68 | } else {
69 | XCTAssert(false, "No date")
70 | }
71 | }
72 |
73 | func checkRelativeYearDate(year: Int, weekday: Int, ordinal: Int, resultMonth: Int, resultDay: Int) {
74 | let cal = Calendar(identifier: .iso8601)
75 |
76 | let comps = DateComponents(year: year, weekday: weekday, weekdayOrdinal: ordinal)
77 | let result = cal.date(from: DateComponents(year: year, month: resultMonth, day: resultDay))!
78 | if let date = cal.date(fromRelative: comps) {
79 | XCTAssert(date == result, "Expected \(result), got \(date)")
80 | } else {
81 | XCTAssert(false, "No date")
82 | }
83 | }
84 |
85 | func checkRelativeDayDate(year: Int, day: Int, resultMonth: Int, resultDay: Int) {
86 | let cal = Calendar(identifier: .iso8601)
87 |
88 | let comps = DateComponents(year: year, day: day)
89 | let result = cal.date(from: DateComponents(year: year, month: resultMonth, day: resultDay))!
90 | if let date = cal.date(fromRelative: comps) {
91 | XCTAssert(date == result, "Expected \(result), got \(date)")
92 | } else {
93 | XCTAssert(false, "No date")
94 | }
95 | }
96 |
97 | func checkRelativeMonthDayDate(year: Int, month: Int, dayOrdinal: Int, resultDay: Int) {
98 | let cal = Calendar(identifier: .iso8601)
99 |
100 | let comps = DateComponents(year: year, month: month, day: dayOrdinal)
101 | let result = cal.date(from: DateComponents(year: year, month: month, day: resultDay))!
102 | if let date = cal.date(fromRelative: comps) {
103 | XCTAssert(date == result, "Expected \(result), got \(date)")
104 | } else {
105 | XCTAssert(false, "No date")
106 | }
107 | }
108 |
109 | func testRelativeData01() {
110 | // Third Tuesday of January
111 | checkRelativeMonthDate(year: 2018, month: 1, weekday: 3, ordinal: 3, resultDay: 16)
112 | }
113 |
114 | func testRelativeData02() {
115 | // Third-to-last Thursday of January
116 | checkRelativeMonthDate(year: 2018, month: 1, weekday: 5, ordinal: -3, resultDay: 11)
117 | }
118 |
119 | func testRelativeData03() {
120 | // First Sunday of January
121 | checkRelativeMonthDate(year: 2018, month: 1, weekday: 1, ordinal: 1, resultDay: 7)
122 | }
123 |
124 | func testRelativeData04() {
125 | // Last Sunday of January
126 | checkRelativeMonthDate(year: 2018, month: 1, weekday: 1, ordinal: -1, resultDay: 28)
127 | }
128 |
129 | func testRelativeData10() {
130 | // First Sunday of the year
131 | checkRelativeYearDate(year: 2018, weekday: 1, ordinal: 1, resultMonth: 1, resultDay: 7)
132 | }
133 |
134 | func testRelativeData11() {
135 | // Last Sunday of the year
136 | checkRelativeYearDate(year: 2018, weekday: 1, ordinal: -1, resultMonth: 12, resultDay: 30)
137 | }
138 |
139 | func testRelativeData12() {
140 | // 20th Wednesday of the year
141 | checkRelativeYearDate(year: 2018, weekday: 4, ordinal: 20, resultMonth: 5, resultDay: 16)
142 | }
143 |
144 | func testRelativeData13() {
145 | // 8th-to-last Saturday of the year
146 | checkRelativeYearDate(year: 2018, weekday: 7, ordinal: -8, resultMonth: 11, resultDay: 10)
147 | }
148 |
149 | func testRelativeData20() {
150 | // First day of year
151 | checkRelativeDayDate(year: 2018, day: 1, resultMonth: 1, resultDay: 1)
152 | }
153 |
154 | func testRelativeData21() {
155 | // Last day of the year
156 | checkRelativeDayDate(year: 2018, day: -1, resultMonth: 12, resultDay: 31)
157 | }
158 |
159 | func testRelativeData22() {
160 | // 100th day of the year
161 | checkRelativeDayDate(year: 2018, day: 100, resultMonth: 4, resultDay: 10)
162 | }
163 |
164 | func testRelativeData23() {
165 | // 76th-to-last day of the year
166 | checkRelativeDayDate(year: 2018, day: -76, resultMonth: 10, resultDay: 17)
167 | }
168 |
169 | func testRelativeData30() {
170 | // First day of February
171 | checkRelativeMonthDayDate(year: 2018, month: 2, dayOrdinal: 1, resultDay: 1)
172 | }
173 |
174 | func testRelativeData31() {
175 | // Last day of February
176 | checkRelativeMonthDayDate(year: 2018, month: 2, dayOrdinal: -1, resultDay: 28)
177 | }
178 |
179 | func testRelativeData32() {
180 | // 7th day of July
181 | checkRelativeMonthDayDate(year: 2018, month: 7, dayOrdinal: 10, resultDay: 10)
182 | }
183 |
184 | func testRelativeData33() {
185 | // 10th-to-last day of October
186 | checkRelativeMonthDayDate(year: 2018, month: 10, dayOrdinal: -10, resultDay: 22)
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/RWMRecurrenceRuleTests/RWMDailyTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RWMDailyTests.swift
3 | // RWMRecurrenceRuleTests
4 | //
5 | // Created by Richard W Maddy on 5/17/18.
6 | // Copyright © 2018 Maddysoft. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class RWMDailyTests: RWMRecurrenceRuleBase {
12 | // ----------- DAILY ------------
13 |
14 | // Daily can use BYMONTH, BYMONTHDAY, BYDAY, BYSETPOS
15 |
16 | func testDaily01() {
17 | // Start 20180517T090000
18 | // Daily with no BYxxx clauses. Should give several consecutive days at the same time
19 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
20 | run(rule: "RRULE:FREQ=DAILY;COUNT=10", start: start, results:
21 | ["2018-05-17T09:00:00", "2018-05-18T09:00:00", "2018-05-19T09:00:00", "2018-05-20T09:00:00",
22 | "2018-05-21T09:00:00", "2018-05-22T09:00:00", "2018-05-23T09:00:00", "2018-05-24T09:00:00",
23 | "2018-05-25T09:00:00", "2018-05-26T09:00:00"]
24 | )
25 | }
26 |
27 | func testDaily02() {
28 | // Start 20180517T090000
29 | // Daily every four days
30 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
31 | run(rule: "RRULE:FREQ=DAILY;INTERVAL=4;COUNT=10", start: start, results:
32 | ["2018-05-17T09:00:00", "2018-05-21T09:00:00", "2018-05-25T09:00:00", "2018-05-29T09:00:00",
33 | "2018-06-02T09:00:00", "2018-06-06T09:00:00", "2018-06-10T09:00:00", "2018-06-14T09:00:00",
34 | "2018-06-18T09:00:00", "2018-06-22T09:00:00"]
35 | )
36 | }
37 |
38 | func testDaily03() {
39 | // Start 20180517T090000
40 | // Daily in May and June
41 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
42 | run(rule: "RRULE:FREQ=DAILY;BYMONTH=5,6;COUNT=50", start: start, results:
43 | ["2018-05-17T09:00:00", "2018-05-18T09:00:00", "2018-05-19T09:00:00", "2018-05-20T09:00:00",
44 | "2018-05-21T09:00:00", "2018-05-22T09:00:00", "2018-05-23T09:00:00", "2018-05-24T09:00:00",
45 | "2018-05-25T09:00:00", "2018-05-26T09:00:00", "2018-05-27T09:00:00", "2018-05-28T09:00:00",
46 | "2018-05-29T09:00:00", "2018-05-30T09:00:00", "2018-05-31T09:00:00", "2018-06-01T09:00:00",
47 | "2018-06-02T09:00:00", "2018-06-03T09:00:00", "2018-06-04T09:00:00", "2018-06-05T09:00:00",
48 | "2018-06-06T09:00:00", "2018-06-07T09:00:00", "2018-06-08T09:00:00", "2018-06-09T09:00:00",
49 | "2018-06-10T09:00:00", "2018-06-11T09:00:00", "2018-06-12T09:00:00", "2018-06-13T09:00:00",
50 | "2018-06-14T09:00:00", "2018-06-15T09:00:00", "2018-06-16T09:00:00", "2018-06-17T09:00:00",
51 | "2018-06-18T09:00:00", "2018-06-19T09:00:00", "2018-06-20T09:00:00", "2018-06-21T09:00:00",
52 | "2018-06-22T09:00:00", "2018-06-23T09:00:00", "2018-06-24T09:00:00", "2018-06-25T09:00:00",
53 | "2018-06-26T09:00:00", "2018-06-27T09:00:00", "2018-06-28T09:00:00", "2018-06-29T09:00:00",
54 | "2018-06-30T09:00:00", "2019-05-01T09:00:00", "2019-05-02T09:00:00", "2019-05-03T09:00:00",
55 | "2019-05-04T09:00:00", "2019-05-05T09:00:00"]
56 | )
57 | }
58 |
59 | func testDaily04() {
60 | // Start 20180517T090000
61 | // Daily in July
62 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
63 | run(rule: "RRULE:FREQ=DAILY;BYMONTH=7;COUNT=50", start: start, results:
64 | ["2018-05-17T09:00:00", "2018-07-01T09:00:00", "2018-07-02T09:00:00", "2018-07-03T09:00:00",
65 | "2018-07-04T09:00:00", "2018-07-05T09:00:00", "2018-07-06T09:00:00", "2018-07-07T09:00:00",
66 | "2018-07-08T09:00:00", "2018-07-09T09:00:00", "2018-07-10T09:00:00", "2018-07-11T09:00:00",
67 | "2018-07-12T09:00:00", "2018-07-13T09:00:00", "2018-07-14T09:00:00", "2018-07-15T09:00:00",
68 | "2018-07-16T09:00:00", "2018-07-17T09:00:00", "2018-07-18T09:00:00", "2018-07-19T09:00:00",
69 | "2018-07-20T09:00:00", "2018-07-21T09:00:00", "2018-07-22T09:00:00", "2018-07-23T09:00:00",
70 | "2018-07-24T09:00:00", "2018-07-25T09:00:00", "2018-07-26T09:00:00", "2018-07-27T09:00:00",
71 | "2018-07-28T09:00:00", "2018-07-29T09:00:00", "2018-07-30T09:00:00", "2018-07-31T09:00:00",
72 | "2019-07-01T09:00:00", "2019-07-02T09:00:00", "2019-07-03T09:00:00", "2019-07-04T09:00:00",
73 | "2019-07-05T09:00:00", "2019-07-06T09:00:00", "2019-07-07T09:00:00", "2019-07-08T09:00:00",
74 | "2019-07-09T09:00:00", "2019-07-10T09:00:00", "2019-07-11T09:00:00", "2019-07-12T09:00:00",
75 | "2019-07-13T09:00:00", "2019-07-14T09:00:00", "2019-07-15T09:00:00", "2019-07-16T09:00:00",
76 | "2019-07-17T09:00:00", "2019-07-18T09:00:00"]
77 | )
78 | }
79 |
80 | func testDaily05() {
81 | // Start 20180517T090000
82 | // Every 3 days in May and June
83 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
84 | run(rule: "RRULE:FREQ=DAILY;BYMONTH=5,6;INTERVAL=3;COUNT=20", start: start, results:
85 | ["2018-05-17T09:00:00", "2018-05-20T09:00:00", "2018-05-23T09:00:00", "2018-05-26T09:00:00",
86 | "2018-05-29T09:00:00", "2018-06-01T09:00:00", "2018-06-04T09:00:00", "2018-06-07T09:00:00",
87 | "2018-06-10T09:00:00", "2018-06-13T09:00:00", "2018-06-16T09:00:00", "2018-06-19T09:00:00",
88 | "2018-06-22T09:00:00", "2018-06-25T09:00:00", "2018-06-28T09:00:00", "2019-05-03T09:00:00",
89 | "2019-05-06T09:00:00", "2019-05-09T09:00:00", "2019-05-12T09:00:00", "2019-05-15T09:00:00"]
90 | )
91 | }
92 |
93 | func testDaily06() {
94 | // Start 20180517T090000
95 | // Every other day in July
96 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
97 | run(rule: "RRULE:FREQ=DAILY;BYMONTH=7;INTERVAL=2;COUNT=30", start: start, results:
98 | ["2018-05-17T09:00:00", "2018-07-02T09:00:00", "2018-07-04T09:00:00", "2018-07-06T09:00:00",
99 | "2018-07-08T09:00:00", "2018-07-10T09:00:00", "2018-07-12T09:00:00", "2018-07-14T09:00:00",
100 | "2018-07-16T09:00:00", "2018-07-18T09:00:00", "2018-07-20T09:00:00", "2018-07-22T09:00:00",
101 | "2018-07-24T09:00:00", "2018-07-26T09:00:00", "2018-07-28T09:00:00", "2018-07-30T09:00:00",
102 | "2019-07-01T09:00:00", "2019-07-03T09:00:00", "2019-07-05T09:00:00", "2019-07-07T09:00:00",
103 | "2019-07-09T09:00:00", "2019-07-11T09:00:00", "2019-07-13T09:00:00", "2019-07-15T09:00:00",
104 | "2019-07-17T09:00:00", "2019-07-19T09:00:00", "2019-07-21T09:00:00", "2019-07-23T09:00:00",
105 | "2019-07-25T09:00:00", "2019-07-27T09:00:00"]
106 | )
107 | }
108 |
109 | func testDaily07() {
110 | // Start 20180517T090000
111 | // The 3rd and 23rd of each month
112 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
113 | run(rule: "RRULE:FREQ=DAILY;BYMONTHDAY=3,23;COUNT=20", start: start, results:
114 | ["2018-05-17T09:00:00", "2018-05-23T09:00:00", "2018-06-03T09:00:00", "2018-06-23T09:00:00",
115 | "2018-07-03T09:00:00", "2018-07-23T09:00:00", "2018-08-03T09:00:00", "2018-08-23T09:00:00",
116 | "2018-09-03T09:00:00", "2018-09-23T09:00:00", "2018-10-03T09:00:00", "2018-10-23T09:00:00",
117 | "2018-11-03T09:00:00", "2018-11-23T09:00:00", "2018-12-03T09:00:00", "2018-12-23T09:00:00",
118 | "2019-01-03T09:00:00", "2019-01-23T09:00:00", "2019-02-03T09:00:00", "2019-02-23T09:00:00"]
119 | )
120 | }
121 |
122 | func testDaily08() {
123 | // Start 20180517T090000
124 | // The 2nd and 2nd-to-last of each month
125 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
126 | run(rule: "RRULE:FREQ=DAILY;BYMONTHDAY=2,-2;COUNT=20", start: start, results:
127 | ["2018-05-17T09:00:00", "2018-05-30T09:00:00", "2018-06-02T09:00:00", "2018-06-29T09:00:00",
128 | "2018-07-02T09:00:00", "2018-07-30T09:00:00", "2018-08-02T09:00:00", "2018-08-30T09:00:00",
129 | "2018-09-02T09:00:00", "2018-09-29T09:00:00", "2018-10-02T09:00:00", "2018-10-30T09:00:00",
130 | "2018-11-02T09:00:00", "2018-11-29T09:00:00", "2018-12-02T09:00:00", "2018-12-30T09:00:00",
131 | "2019-01-02T09:00:00", "2019-01-30T09:00:00", "2019-02-02T09:00:00", "2019-02-27T09:00:00"]
132 | )
133 | }
134 |
135 | func testDaily09() {
136 | // Start 20180517T090000
137 | // Every Monday, Tuesday, and Friday
138 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
139 | run(rule: "RRULE:FREQ=DAILY;BYDAY=MO,TU,FR;COUNT=20", start: start, results:
140 | ["2018-05-17T09:00:00", "2018-05-18T09:00:00", "2018-05-21T09:00:00", "2018-05-22T09:00:00",
141 | "2018-05-25T09:00:00", "2018-05-28T09:00:00", "2018-05-29T09:00:00", "2018-06-01T09:00:00",
142 | "2018-06-04T09:00:00", "2018-06-05T09:00:00", "2018-06-08T09:00:00", "2018-06-11T09:00:00",
143 | "2018-06-12T09:00:00", "2018-06-15T09:00:00", "2018-06-18T09:00:00", "2018-06-19T09:00:00",
144 | "2018-06-22T09:00:00", "2018-06-25T09:00:00", "2018-06-26T09:00:00", "2018-06-29T09:00:00"]
145 | )
146 | }
147 |
148 | func testDaily10() {
149 | // Start 20180517T090000
150 | // Every Tuesday and Wednesday
151 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
152 | run(rule: "RRULE:FREQ=DAILY;BYDAY=WE,TU;COUNT=20", start: start, results:
153 | ["2018-05-17T09:00:00", "2018-05-22T09:00:00", "2018-05-23T09:00:00", "2018-05-29T09:00:00",
154 | "2018-05-30T09:00:00", "2018-06-05T09:00:00", "2018-06-06T09:00:00", "2018-06-12T09:00:00",
155 | "2018-06-13T09:00:00", "2018-06-19T09:00:00", "2018-06-20T09:00:00", "2018-06-26T09:00:00",
156 | "2018-06-27T09:00:00", "2018-07-03T09:00:00", "2018-07-04T09:00:00", "2018-07-10T09:00:00",
157 | "2018-07-11T09:00:00", "2018-07-17T09:00:00", "2018-07-18T09:00:00", "2018-07-24T09:00:00"]
158 | )
159 | }
160 |
161 | func testDaily11() {
162 | // Start 20180517T090000
163 | // The 1st, 2nd, 3rd, 4th, and 5th of May and June
164 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
165 | run(rule: "RRULE:FREQ=DAILY;BYMONTH=5,6;BYMONTHDAY=1,2,3,4,5;COUNT=50", start: start, results:
166 | ["2018-05-17T09:00:00", "2018-06-01T09:00:00", "2018-06-02T09:00:00", "2018-06-03T09:00:00",
167 | "2018-06-04T09:00:00", "2018-06-05T09:00:00", "2019-05-01T09:00:00", "2019-05-02T09:00:00",
168 | "2019-05-03T09:00:00", "2019-05-04T09:00:00", "2019-05-05T09:00:00", "2019-06-01T09:00:00",
169 | "2019-06-02T09:00:00", "2019-06-03T09:00:00", "2019-06-04T09:00:00", "2019-06-05T09:00:00",
170 | "2020-05-01T09:00:00", "2020-05-02T09:00:00", "2020-05-03T09:00:00", "2020-05-04T09:00:00",
171 | "2020-05-05T09:00:00", "2020-06-01T09:00:00", "2020-06-02T09:00:00", "2020-06-03T09:00:00",
172 | "2020-06-04T09:00:00", "2020-06-05T09:00:00", "2021-05-01T09:00:00", "2021-05-02T09:00:00",
173 | "2021-05-03T09:00:00", "2021-05-04T09:00:00", "2021-05-05T09:00:00", "2021-06-01T09:00:00",
174 | "2021-06-02T09:00:00", "2021-06-03T09:00:00", "2021-06-04T09:00:00", "2021-06-05T09:00:00",
175 | "2022-05-01T09:00:00", "2022-05-02T09:00:00", "2022-05-03T09:00:00", "2022-05-04T09:00:00",
176 | "2022-05-05T09:00:00", "2022-06-01T09:00:00", "2022-06-02T09:00:00", "2022-06-03T09:00:00",
177 | "2022-06-04T09:00:00", "2022-06-05T09:00:00", "2023-05-01T09:00:00", "2023-05-02T09:00:00",
178 | "2023-05-03T09:00:00", "2023-05-04T09:00:00"]
179 | )
180 | }
181 |
182 | func testDaily12() {
183 | // Start 20180517T090000
184 | // The 2nd, 4th, 6th, 2nd-to-last, and 4th-to-last days of July
185 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
186 | run(rule: "RRULE:FREQ=DAILY;BYMONTH=7;BYMONTHDAY=2,4,6,-2,-4;COUNT=50", start: start, results:
187 | ["2018-05-17T09:00:00", "2018-07-02T09:00:00", "2018-07-04T09:00:00", "2018-07-06T09:00:00",
188 | "2018-07-28T09:00:00", "2018-07-30T09:00:00", "2019-07-02T09:00:00", "2019-07-04T09:00:00",
189 | "2019-07-06T09:00:00", "2019-07-28T09:00:00", "2019-07-30T09:00:00", "2020-07-02T09:00:00",
190 | "2020-07-04T09:00:00", "2020-07-06T09:00:00", "2020-07-28T09:00:00", "2020-07-30T09:00:00",
191 | "2021-07-02T09:00:00", "2021-07-04T09:00:00", "2021-07-06T09:00:00", "2021-07-28T09:00:00",
192 | "2021-07-30T09:00:00", "2022-07-02T09:00:00", "2022-07-04T09:00:00", "2022-07-06T09:00:00",
193 | "2022-07-28T09:00:00", "2022-07-30T09:00:00", "2023-07-02T09:00:00", "2023-07-04T09:00:00",
194 | "2023-07-06T09:00:00", "2023-07-28T09:00:00", "2023-07-30T09:00:00", "2024-07-02T09:00:00",
195 | "2024-07-04T09:00:00", "2024-07-06T09:00:00", "2024-07-28T09:00:00", "2024-07-30T09:00:00",
196 | "2025-07-02T09:00:00", "2025-07-04T09:00:00", "2025-07-06T09:00:00", "2025-07-28T09:00:00",
197 | "2025-07-30T09:00:00", "2026-07-02T09:00:00", "2026-07-04T09:00:00", "2026-07-06T09:00:00",
198 | "2026-07-28T09:00:00", "2026-07-30T09:00:00", "2027-07-02T09:00:00", "2027-07-04T09:00:00",
199 | "2027-07-06T09:00:00", "2027-07-28T09:00:00"]
200 | )
201 | }
202 |
203 | func testDaily13() {
204 | // Start 20180517T090000
205 | // Every Monday and Friday of May and June
206 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
207 | run(rule: "RRULE:FREQ=DAILY;BYMONTH=5,6;BYDAY=MO,FR;COUNT=20", start: start, results:
208 | ["2018-05-17T09:00:00", "2018-05-18T09:00:00", "2018-05-21T09:00:00", "2018-05-25T09:00:00",
209 | "2018-05-28T09:00:00", "2018-06-01T09:00:00", "2018-06-04T09:00:00", "2018-06-08T09:00:00",
210 | "2018-06-11T09:00:00", "2018-06-15T09:00:00", "2018-06-18T09:00:00", "2018-06-22T09:00:00",
211 | "2018-06-25T09:00:00", "2018-06-29T09:00:00", "2019-05-03T09:00:00", "2019-05-06T09:00:00",
212 | "2019-05-10T09:00:00", "2019-05-13T09:00:00", "2019-05-17T09:00:00", "2019-05-20T09:00:00"]
213 | )
214 | }
215 |
216 | func testDaily14() {
217 | // Start 20180517T090000
218 | // Every Saturday and Sunday of July
219 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
220 | run(rule: "RRULE:FREQ=DAILY;BYMONTH=7;BYDAY=SU,SA;COUNT=20", start: start, results:
221 | ["2018-05-17T09:00:00", "2018-07-01T09:00:00", "2018-07-07T09:00:00", "2018-07-08T09:00:00",
222 | "2018-07-14T09:00:00", "2018-07-15T09:00:00", "2018-07-21T09:00:00", "2018-07-22T09:00:00",
223 | "2018-07-28T09:00:00", "2018-07-29T09:00:00", "2019-07-06T09:00:00", "2019-07-07T09:00:00",
224 | "2019-07-13T09:00:00", "2019-07-14T09:00:00", "2019-07-20T09:00:00", "2019-07-21T09:00:00",
225 | "2019-07-27T09:00:00", "2019-07-28T09:00:00", "2020-07-04T09:00:00", "2020-07-05T09:00:00"]
226 | )
227 | }
228 |
229 | func testDaily15() {
230 | // Start 20180517T090000
231 | // Every Monday and Friday falling on the 5th, 6th, 7th, 8th, 9th, or 10th
232 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
233 | run(rule: "RRULE:FREQ=DAILY;BYMONTHDAY=5,6,7,8,9,10;BYDAY=MO,FR;COUNT=20", start: start, results:
234 | ["2018-05-17T09:00:00", "2018-06-08T09:00:00", "2018-07-06T09:00:00", "2018-07-09T09:00:00",
235 | "2018-08-06T09:00:00", "2018-08-10T09:00:00", "2018-09-07T09:00:00", "2018-09-10T09:00:00",
236 | "2018-10-05T09:00:00", "2018-10-08T09:00:00", "2018-11-05T09:00:00", "2018-11-09T09:00:00",
237 | "2018-12-07T09:00:00", "2018-12-10T09:00:00", "2019-01-07T09:00:00", "2019-02-08T09:00:00",
238 | "2019-03-08T09:00:00", "2019-04-05T09:00:00", "2019-04-08T09:00:00", "2019-05-06T09:00:00"]
239 | )
240 | }
241 |
242 | func testDaily16() {
243 | // Start 20180517T090000
244 | // Every Saturday and Sunday falling on the last, 2nd-to-last, 3rd-to-last, 4th-to-last, or 5th-to-last of the month
245 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
246 | run(rule: "RRULE:FREQ=DAILY;BYMONTHDAY=-1,-2,-3,-4,-5;BYDAY=SU,SA;COUNT=20", start: start, results:
247 | ["2018-05-17T09:00:00", "2018-05-27T09:00:00", "2018-06-30T09:00:00", "2018-07-28T09:00:00",
248 | "2018-07-29T09:00:00", "2018-09-29T09:00:00", "2018-09-30T09:00:00", "2018-10-27T09:00:00",
249 | "2018-10-28T09:00:00", "2018-12-29T09:00:00", "2018-12-30T09:00:00", "2019-01-27T09:00:00",
250 | "2019-02-24T09:00:00", "2019-03-30T09:00:00", "2019-03-31T09:00:00", "2019-04-27T09:00:00",
251 | "2019-04-28T09:00:00", "2019-06-29T09:00:00", "2019-06-30T09:00:00", "2019-07-27T09:00:00"]
252 | )
253 | }
254 |
255 | func testDaily17() {
256 | // Start 20180517T090000
257 | // Every Monday and Friday in August, September, or October falling on the 5th, 6th, 7th, 8th, 9th, or 10th
258 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
259 | run(rule: "RRULE:FREQ=DAILY;BYMONTH=8,9,10;BYMONTHDAY=5,6,7,8,9,10;BYDAY=MO,FR;COUNT=20", start: start, results:
260 | ["2018-05-17T09:00:00", "2018-08-06T09:00:00", "2018-08-10T09:00:00", "2018-09-07T09:00:00",
261 | "2018-09-10T09:00:00", "2018-10-05T09:00:00", "2018-10-08T09:00:00", "2019-08-05T09:00:00",
262 | "2019-08-09T09:00:00", "2019-09-06T09:00:00", "2019-09-09T09:00:00", "2019-10-07T09:00:00",
263 | "2020-08-07T09:00:00", "2020-08-10T09:00:00", "2020-09-07T09:00:00", "2020-10-05T09:00:00",
264 | "2020-10-09T09:00:00", "2021-08-06T09:00:00", "2021-08-09T09:00:00", "2021-09-06T09:00:00"]
265 | )
266 | }
267 |
268 | func testDaily18() {
269 | // Start 20180517T090000
270 | // Every Saturday and Sunday in January or February falling on the last, 2nd-to-last, 3rd-to-last, 4th-to-last, or 5th-to-last of the month
271 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
272 | run(rule: "RRULE:FREQ=DAILY;BYMONTH=1,2;BYMONTHDAY=-1,-2,-3,-4,-5;BYDAY=SU,SA;COUNT=20", start: start, results:
273 | ["2018-05-17T09:00:00", "2019-01-27T09:00:00", "2019-02-24T09:00:00", "2020-02-29T09:00:00",
274 | "2021-01-30T09:00:00", "2021-01-31T09:00:00", "2021-02-27T09:00:00", "2021-02-28T09:00:00",
275 | "2022-01-29T09:00:00", "2022-01-30T09:00:00", "2022-02-26T09:00:00", "2022-02-27T09:00:00",
276 | "2023-01-28T09:00:00", "2023-01-29T09:00:00", "2023-02-25T09:00:00", "2023-02-26T09:00:00",
277 | "2024-01-27T09:00:00", "2024-01-28T09:00:00", "2024-02-25T09:00:00", "2026-01-31T09:00:00"]
278 | )
279 | }
280 |
281 | }
282 |
--------------------------------------------------------------------------------
/RWMRecurrenceRuleTests/RWMEventKitTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RWMEventKitTests.swift
3 | // RWMRecurrenceRuleTests
4 | //
5 | // Created by Richard W Maddy on 5/21/18.
6 | // Copyright © 2018 Maddysoft. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class RWMEventKitTests: RWMRecurrenceRuleBase {
12 | /*
13 | A - RRULE FREQ=DAILY;INTERVAL=1;UNTIL=20180629T055959Z
14 | B - RRULE FREQ=WEEKLY;INTERVAL=1;UNTIL=20180822T055959Z
15 | C - RRULE FREQ=WEEKLY;INTERVAL=2;UNTIL=20180922T055959Z
16 | D - RRULE FREQ=MONTHLY;INTERVAL=1;UNTIL=20200522T055959Z
17 | E - RRULE FREQ=YEARLY;INTERVAL=1;UNTIL=20230521T180000Z
18 | F - RRULE FREQ=DAILY;INTERVAL=3;UNTIL=20180722T055959Z
19 | G - RRULE FREQ=WEEKLY;INTERVAL=2;UNTIL=20180822T055959Z;BYDAY=SU,WE,SA;WKST=SU
20 | H - RRULE FREQ=MONTHLY;INTERVAL=2;UNTIL=20190622T055959Z;BYMONTHDAY=10,15,20
21 | I - RRULE FREQ=MONTHLY;INTERVAL=3;UNTIL=20190622T055959Z;BYDAY=TU;BYSETPOS=2
22 | J - RRULE FREQ=MONTHLY;INTERVAL=1;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1
23 | K - RRULE FREQ=MONTHLY;INTERVAL=1;UNTIL=20190622T055959Z;BYDAY=SU,SA;BYSETPOS=3
24 | L - RRULE FREQ=YEARLY;INTERVAL=2;UNTIL=20230622T055959Z;BYMONTH=9,10,11
25 | M - RRULE FREQ=YEARLY;INTERVAL=1;UNTIL=20190622T055959Z;BYMONTH=5,7;BYDAY=1WE
26 | */
27 |
28 | lazy var start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 21, hour: 9))!
29 |
30 | override func setUp() {
31 | scheduler.mode = .eventKit
32 | }
33 |
34 | func testA() {
35 | run(rule: "RRULE:FREQ=DAILY;INTERVAL=1;UNTIL=20180602T055959Z", start: start, results:
36 | ["2018-05-21T09:00:00", "2018-05-22T09:00:00", "2018-05-23T09:00:00", "2018-05-24T09:00:00",
37 | "2018-05-25T09:00:00", "2018-05-26T09:00:00", "2018-05-27T09:00:00", "2018-05-28T09:00:00",
38 | "2018-05-29T09:00:00", "2018-05-30T09:00:00", "2018-05-31T09:00:00", "2018-06-01T09:00:00"]
39 | )
40 | }
41 |
42 | func testB() {
43 | run(rule: "RRULE:FREQ=WEEKLY;INTERVAL=1;UNTIL=20180722T055959Z", start: start, results:
44 | ["2018-05-21T09:00:00", "2018-05-28T09:00:00", "2018-06-04T09:00:00", "2018-06-11T09:00:00",
45 | "2018-06-18T09:00:00", "2018-06-25T09:00:00", "2018-07-02T09:00:00", "2018-07-09T09:00:00",
46 | "2018-07-16T09:00:00"]
47 | )
48 | }
49 |
50 | func testC() {
51 | run(rule: "RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=20180822T055959Z", start: start, results:
52 | ["2018-05-21T09:00:00", "2018-06-04T09:00:00", "2018-06-18T09:00:00", "2018-07-02T09:00:00",
53 | "2018-07-16T09:00:00", "2018-07-30T09:00:00", "2018-08-13T09:00:00"]
54 | )
55 | }
56 |
57 | func testD() {
58 | run(rule: "RRULE:FREQ=MONTHLY;INTERVAL=1;UNTIL=20190522T055959Z", start: start, results:
59 | ["2018-05-21T09:00:00", "2018-06-21T09:00:00", "2018-07-21T09:00:00", "2018-08-21T09:00:00",
60 | "2018-09-21T09:00:00", "2018-10-21T09:00:00", "2018-11-21T09:00:00", "2018-12-21T09:00:00",
61 | "2019-01-21T09:00:00", "2019-02-21T09:00:00", "2019-03-21T09:00:00", "2019-04-21T09:00:00",
62 | "2019-05-21T09:00:00"]
63 | )
64 | }
65 |
66 | func testE() {
67 | run(rule: "RRULE:FREQ=YEARLY;INTERVAL=1;UNTIL=20230521T180000Z", start: start, results:
68 | ["2018-05-21T09:00:00", "2019-05-21T09:00:00", "2020-05-21T09:00:00", "2021-05-21T09:00:00",
69 | "2022-05-21T09:00:00", "2023-05-21T09:00:00"]
70 | )
71 | }
72 |
73 | func testF() {
74 | run(rule: "RRULE:FREQ=DAILY;INTERVAL=3;UNTIL=20180722T055959Z", start: start, results:
75 | ["2018-05-21T09:00:00", "2018-05-24T09:00:00", "2018-05-27T09:00:00", "2018-05-30T09:00:00",
76 | "2018-06-02T09:00:00", "2018-06-05T09:00:00", "2018-06-08T09:00:00", "2018-06-11T09:00:00",
77 | "2018-06-14T09:00:00", "2018-06-17T09:00:00", "2018-06-20T09:00:00", "2018-06-23T09:00:00",
78 | "2018-06-26T09:00:00", "2018-06-29T09:00:00", "2018-07-02T09:00:00", "2018-07-05T09:00:00",
79 | "2018-07-08T09:00:00", "2018-07-11T09:00:00", "2018-07-14T09:00:00", "2018-07-17T09:00:00",
80 | "2018-07-20T09:00:00"]
81 | )
82 | }
83 |
84 | func testG() {
85 | run(rule: "RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=20180822T055959Z;BYDAY=SU,WE,SA;WKST=SU", start: start, results:
86 | ["2018-05-21T09:00:00", "2018-05-23T09:00:00", "2018-05-26T09:00:00", "2018-05-27T09:00:00",
87 | "2018-06-06T09:00:00", "2018-06-09T09:00:00", "2018-06-10T09:00:00", "2018-06-20T09:00:00", "2018-06-23T09:00:00",
88 | "2018-06-24T09:00:00", "2018-07-04T09:00:00", "2018-07-07T09:00:00", "2018-07-08T09:00:00",
89 | "2018-07-18T09:00:00", "2018-07-21T09:00:00", "2018-07-22T09:00:00", "2018-08-01T09:00:00",
90 | "2018-08-04T09:00:00", "2018-08-05T09:00:00", "2018-08-15T09:00:00", "2018-08-18T09:00:00",
91 | "2018-08-19T09:00:00"]
92 | )
93 | }
94 |
95 | func testH() {
96 | run(rule: "RRULE:FREQ=MONTHLY;INTERVAL=2;UNTIL=20190622T055959Z;BYMONTHDAY=10,15,20", start: start, results:
97 | ["2018-05-21T09:00:00", "2018-07-10T09:00:00", "2018-07-15T09:00:00", "2018-07-20T09:00:00",
98 | "2018-09-10T09:00:00", "2018-09-15T09:00:00", "2018-09-20T09:00:00", "2018-11-10T09:00:00",
99 | "2018-11-15T09:00:00", "2018-11-20T09:00:00", "2019-01-10T09:00:00", "2019-01-15T09:00:00",
100 | "2019-01-20T09:00:00", "2019-03-10T09:00:00", "2019-03-15T09:00:00", "2019-03-20T09:00:00",
101 | "2019-05-10T09:00:00", "2019-05-15T09:00:00", "2019-05-20T09:00:00"]
102 | )
103 | }
104 |
105 | func testI() {
106 | run(rule: "RRULE:FREQ=MONTHLY;INTERVAL=3;UNTIL=20190622T055959Z;BYDAY=TU;BYSETPOS=2", start: start, results:
107 | ["2018-05-21T09:00:00", "2018-08-14T09:00:00", "2018-11-13T09:00:00", "2019-02-12T09:00:00",
108 | "2019-05-14T09:00:00"]
109 | )
110 | }
111 |
112 | func testJ() {
113 | run(rule: "RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1;COUNT=20", start: start, results:
114 | ["2018-05-21T09:00:00", "2018-05-31T09:00:00", "2018-06-30T09:00:00", "2018-07-31T09:00:00",
115 | "2018-08-31T09:00:00", "2018-09-30T09:00:00", "2018-10-31T09:00:00", "2018-11-30T09:00:00",
116 | "2018-12-31T09:00:00", "2019-01-31T09:00:00", "2019-02-28T09:00:00", "2019-03-31T09:00:00",
117 | "2019-04-30T09:00:00", "2019-05-31T09:00:00", "2019-06-30T09:00:00", "2019-07-31T09:00:00",
118 | "2019-08-31T09:00:00", "2019-09-30T09:00:00", "2019-10-31T09:00:00", "2019-11-30T09:00:00"]
119 | )
120 | }
121 |
122 | func testK() {
123 | run(rule: "RRULE:FREQ=MONTHLY;INTERVAL=1;UNTIL=20190622T055959Z;BYDAY=SU,SA;BYSETPOS=3", start: start, results:
124 | ["2018-05-21T09:00:00", "2018-06-09T09:00:00", "2018-07-08T09:00:00", "2018-08-11T09:00:00",
125 | "2018-09-08T09:00:00", "2018-10-13T09:00:00", "2018-11-10T09:00:00", "2018-12-08T09:00:00",
126 | "2019-01-12T09:00:00", "2019-02-09T09:00:00", "2019-03-09T09:00:00", "2019-04-13T09:00:00",
127 | "2019-05-11T09:00:00", "2019-06-08T09:00:00"]
128 | )
129 | }
130 |
131 | func testL() {
132 | run(rule: "RRULE:FREQ=YEARLY;INTERVAL=2;UNTIL=20230622T055959Z;BYMONTH=9,10,11", start: start, results:
133 | ["2018-05-21T09:00:00", "2018-09-21T09:00:00", "2018-10-21T09:00:00", "2018-11-21T09:00:00",
134 | "2020-09-21T09:00:00", "2020-10-21T09:00:00", "2020-11-21T09:00:00", "2022-09-21T09:00:00",
135 | "2022-10-21T09:00:00", "2022-11-21T09:00:00"]
136 | )
137 | }
138 |
139 | func testM() {
140 | run(rule: "RRULE:FREQ=YEARLY;INTERVAL=1;UNTIL=20190622T055959Z;BYMONTH=5,7;BYDAY=1WE", start: start, results:
141 | ["2018-05-21T09:00:00", "2018-07-04T09:00:00", "2019-05-01T09:00:00"]
142 | )
143 | }
144 |
145 | }
146 |
--------------------------------------------------------------------------------
/RWMRecurrenceRuleTests/RWMMonthlyTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RWMMonthlyTests.swift
3 | // RWMRecurrenceRuleTests
4 | //
5 | // Created by Richard W Maddy on 5/17/18.
6 | // Copyright © 2018 Maddysoft. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class RWMMonthlyTests: RWMRecurrenceRuleBase {
12 | // ----------- MONTHLY ------------
13 |
14 | // Monthly can use BYMONTH, BYMONTHDAY, BYDAY, BYSETPOS
15 |
16 | func testMonthly01() {
17 | // Start 20180517T090000
18 | // Monthly with no BYxxx clauses. Should give 3 months with same day as start date
19 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
20 | run(rule: "RRULE:FREQ=MONTHLY;COUNT=3", start: start, results:
21 | ["2018-05-17T09:00:00", "2018-06-17T09:00:00", "2018-07-17T09:00:00"]
22 | )
23 | }
24 |
25 | func testMonthly02() {
26 | // Start 20180517T090000
27 | // Once every three months
28 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
29 | run(rule: "RRULE:FREQ=MONTHLY;INTERVAL=3;COUNT=3", start: start, results:
30 | ["2018-05-17T09:00:00", "2018-08-17T09:00:00", "2018-11-17T09:00:00"]
31 | )
32 | }
33 |
34 | func testMonthly03() {
35 | // Start 20180517T090000
36 | // Start day each May and June
37 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
38 | run(rule: "RRULE:FREQ=MONTHLY;BYMONTH=5,6;COUNT=3", start: start, results:
39 | ["2018-05-17T09:00:00", "2018-06-17T09:00:00", "2019-05-17T09:00:00"]
40 | )
41 | }
42 |
43 | func testMonthly03a() {
44 | // Start 20180617T090000
45 | // Start day each May and June
46 | let start = calendar.date(from: DateComponents(year: 2018, month: 6, day: 17, hour: 9))!
47 | run(rule: "RRULE:FREQ=MONTHLY;INTERVAL=2;BYMONTH=1,3,5,7,9,11;COUNT=5", start: start, results:
48 | ["2018-06-17T09:00:00"/*, "2018-07-17T09:00:00", "2018-09-17T09:00:00", "2018-11-17T09:00:00",
49 | "2019-01-17T09:00:00"*/]
50 | )
51 | }
52 |
53 | func testMonthly04() {
54 | // Start 20180517T090000
55 | // 2nd, 4th, and 6th of each month
56 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
57 | run(rule: "RRULE:FREQ=MONTHLY;BYMONTHDAY=2,4,6;COUNT=10", start: start, results:
58 | ["2018-05-17T09:00:00", "2018-06-02T09:00:00", "2018-06-04T09:00:00", "2018-06-06T09:00:00",
59 | "2018-07-02T09:00:00", "2018-07-04T09:00:00", "2018-07-06T09:00:00", "2018-08-02T09:00:00",
60 | "2018-08-04T09:00:00", "2018-08-06T09:00:00"]
61 | )
62 | }
63 |
64 | func testMonthly05() {
65 | // Start 20180517T090000
66 | // 2nd and last day of each month
67 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
68 | run(rule: "RRULE:FREQ=MONTHLY;BYMONTHDAY=2,-1;COUNT=10", start: start, results:
69 | ["2018-05-17T09:00:00", "2018-05-31T09:00:00", "2018-06-02T09:00:00", "2018-06-30T09:00:00",
70 | "2018-07-02T09:00:00", "2018-07-31T09:00:00", "2018-08-02T09:00:00", "2018-08-31T09:00:00",
71 | "2018-09-02T09:00:00", "2018-09-30T09:00:00"]
72 | )
73 | }
74 |
75 | func testMonthly06() {
76 | // Start 20180517T090000
77 | // 2nd, 4th, and 6th of every other month
78 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
79 | run(rule: "RRULE:FREQ=MONTHLY;BYMONTHDAY=2,4,6;INTERVAL=2;COUNT=10", start: start, results:
80 | ["2018-05-17T09:00:00", "2018-07-02T09:00:00", "2018-07-04T09:00:00", "2018-07-06T09:00:00",
81 | "2018-09-02T09:00:00", "2018-09-04T09:00:00", "2018-09-06T09:00:00", "2018-11-02T09:00:00",
82 | "2018-11-04T09:00:00", "2018-11-06T09:00:00"]
83 | )
84 | }
85 |
86 | func testMonthly07() {
87 | // Start 20180517T090000
88 | // 2nd and last day of each 3rd month
89 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
90 | run(rule: "RRULE:FREQ=MONTHLY;BYMONTHDAY=2,-1;INTERVAL=3;COUNT=10", start: start, results:
91 | ["2018-05-17T09:00:00", "2018-05-31T09:00:00", "2018-08-02T09:00:00", "2018-08-31T09:00:00",
92 | "2018-11-02T09:00:00", "2018-11-30T09:00:00", "2019-02-02T09:00:00", "2019-02-28T09:00:00",
93 | "2019-05-02T09:00:00", "2019-05-31T09:00:00"]
94 | )
95 | }
96 |
97 | func testMonthly08() {
98 | // Start 20180517T090000
99 | // Every Tuesday and Thursday of every month
100 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
101 | run(rule: "RRULE:FREQ=MONTHLY;BYDAY=TU,TH;COUNT=10", start: start, results:
102 | ["2018-05-17T09:00:00", "2018-05-22T09:00:00", "2018-05-24T09:00:00", "2018-05-29T09:00:00",
103 | "2018-05-31T09:00:00", "2018-06-05T09:00:00", "2018-06-07T09:00:00", "2018-06-12T09:00:00",
104 | "2018-06-14T09:00:00", "2018-06-19T09:00:00"]
105 | )
106 | }
107 |
108 | func testMonthly09() {
109 | // Start 20180517T090000
110 | // 1st Monday and last Friday of every month
111 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
112 | run(rule: "RRULE:FREQ=MONTHLY;BYDAY=1MO,-1FR;COUNT=10", start: start, results:
113 | ["2018-05-17T09:00:00", "2018-05-25T09:00:00", "2018-06-04T09:00:00", "2018-06-29T09:00:00",
114 | "2018-07-02T09:00:00", "2018-07-27T09:00:00", "2018-08-06T09:00:00", "2018-08-31T09:00:00",
115 | "2018-09-03T09:00:00", "2018-09-28T09:00:00"]
116 | )
117 | }
118 |
119 | func testMonthly10() {
120 | // Start 20180517T090000
121 | // Every Tuesday and Thursday of every third month
122 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
123 | run(rule: "RRULE:FREQ=MONTHLY;BYDAY=TU,TH;INTERVAL=3;COUNT=10", start: start, results:
124 | ["2018-05-17T09:00:00", "2018-05-22T09:00:00", "2018-05-24T09:00:00", "2018-05-29T09:00:00",
125 | "2018-05-31T09:00:00", "2018-08-02T09:00:00", "2018-08-07T09:00:00", "2018-08-09T09:00:00",
126 | "2018-08-14T09:00:00", "2018-08-16T09:00:00"]
127 | )
128 | }
129 |
130 | func testMonthly11() {
131 | // Start 20180517T090000
132 | // 1st Monday and last Friday of every other month
133 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
134 | run(rule: "RRULE:FREQ=MONTHLY;BYDAY=1MO,-1FR;INTERVAL=2;COUNT=10", start: start, results:
135 | ["2018-05-17T09:00:00", "2018-05-25T09:00:00", "2018-07-02T09:00:00", "2018-07-27T09:00:00",
136 | "2018-09-03T09:00:00", "2018-09-28T09:00:00", "2018-11-05T09:00:00", "2018-11-30T09:00:00",
137 | "2019-01-07T09:00:00", "2019-01-25T09:00:00"]
138 | )
139 | }
140 |
141 | func testMonthly12() {
142 | // Start 20180517T090000
143 | // 2nd, 4th, and 6th of March and May
144 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
145 | run(rule: "RRULE:FREQ=MONTHLY;BYMONTHDAY=2,4,6;BYMONTH=3,5;COUNT=10", start: start, results:
146 | ["2018-05-17T09:00:00", "2019-03-02T09:00:00", "2019-03-04T09:00:00", "2019-03-06T09:00:00",
147 | "2019-05-02T09:00:00", "2019-05-04T09:00:00", "2019-05-06T09:00:00", "2020-03-02T09:00:00",
148 | "2020-03-04T09:00:00", "2020-03-06T09:00:00"]
149 | )
150 | }
151 |
152 | func testMonthly13() {
153 | // Start 20180517T090000
154 | // 2nd and last day of June
155 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
156 | run(rule: "RRULE:FREQ=MONTHLY;BYMONTH=6;BYMONTHDAY=2,-1;COUNT=10", start: start, results:
157 | ["2018-05-17T09:00:00", "2018-06-02T09:00:00", "2018-06-30T09:00:00", "2019-06-02T09:00:00",
158 | "2019-06-30T09:00:00", "2020-06-02T09:00:00", "2020-06-30T09:00:00", "2021-06-02T09:00:00",
159 | "2021-06-30T09:00:00", "2022-06-02T09:00:00"]
160 | )
161 | }
162 |
163 | func testMonthly14() {
164 | // Start 20180517T090000
165 | // Every Tuesday and Thursday of every August
166 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
167 | run(rule: "RRULE:FREQ=MONTHLY;BYDAY=TU,TH;BYMONTH=8;COUNT=15", start: start, results:
168 | ["2018-05-17T09:00:00", "2018-08-02T09:00:00", "2018-08-07T09:00:00", "2018-08-09T09:00:00",
169 | "2018-08-14T09:00:00", "2018-08-16T09:00:00", "2018-08-21T09:00:00", "2018-08-23T09:00:00",
170 | "2018-08-28T09:00:00", "2018-08-30T09:00:00", "2019-08-01T09:00:00", "2019-08-06T09:00:00",
171 | "2019-08-08T09:00:00", "2019-08-13T09:00:00", "2019-08-15T09:00:00"]
172 | )
173 | }
174 |
175 | func testMonthly14a() {
176 | // Start 20180517T090000
177 | // Every Tuesday and the 2nd Thursday of every August
178 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
179 | run(rule: "RRULE:FREQ=MONTHLY;BYDAY=TU,2TH;BYMONTH=8;COUNT=15", start: start, results:
180 | ["2018-05-17T09:00:00", "2018-08-07T09:00:00", "2018-08-09T09:00:00", "2018-08-14T09:00:00",
181 | "2018-08-21T09:00:00", "2018-08-28T09:00:00", "2019-08-06T09:00:00", "2019-08-08T09:00:00",
182 | "2019-08-13T09:00:00", "2019-08-20T09:00:00", "2019-08-27T09:00:00", "2020-08-04T09:00:00",
183 | "2020-08-11T09:00:00", "2020-08-13T09:00:00", "2020-08-18T09:00:00"]
184 | )
185 | }
186 |
187 | func testMonthly15() {
188 | // Start 20180517T090000
189 | // 1st Monday and last Friday of every March and September
190 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
191 | run(rule: "RRULE:FREQ=MONTHLY;BYDAY=1MO,-1FR;BYMONTH=3,9;COUNT=10", start: start, results:
192 | ["2018-05-17T09:00:00", "2018-09-03T09:00:00", "2018-09-28T09:00:00", "2019-03-04T09:00:00",
193 | "2019-03-29T09:00:00", "2019-09-02T09:00:00", "2019-09-27T09:00:00", "2020-03-02T09:00:00",
194 | "2020-03-27T09:00:00", "2020-09-07T09:00:00"]
195 | )
196 | }
197 |
198 | func testMonthly16() {
199 | // Start 20180517T090000
200 | // The 10th, 11th, 12th, 13th, and 14th of every month that falls on a Tuesday or Thursday
201 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
202 | run(rule: "RRULE:FREQ=MONTHLY;BYDAY=TU,TH;BYMONTHDAY=10,11,12,13,14;COUNT=10", start: start, results:
203 | ["2018-05-17T09:00:00", "2018-06-12T09:00:00", "2018-06-14T09:00:00", "2018-07-10T09:00:00",
204 | "2018-07-12T09:00:00", "2018-08-14T09:00:00", "2018-09-11T09:00:00", "2018-09-13T09:00:00",
205 | "2018-10-11T09:00:00", "2018-11-13T09:00:00"]
206 | )
207 | }
208 |
209 | func testMonthly17() {
210 | // Start 20180517T090000
211 | // The 1st, 2nd, 3rd, last, 2nd-to-last, and 3rd-to-last of every month that falls on the 1st Monday or the last Friday
212 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
213 | run(rule: "RRULE:FREQ=MONTHLY;BYMONTHDAY=1,2,3,-1,-2,-3;BYDAY=1MO,-1FR;COUNT=10", start: start, results:
214 | ["2018-05-17T09:00:00", "2018-06-29T09:00:00", "2018-07-02T09:00:00", "2018-08-31T09:00:00",
215 | "2018-09-03T09:00:00", "2018-09-28T09:00:00", "2018-10-01T09:00:00", "2018-11-30T09:00:00",
216 | "2018-12-03T09:00:00", "2019-03-29T09:00:00"]
217 | )
218 | }
219 |
220 | func testMonthly18() {
221 | // Start 20180517T090000
222 | // The 1st, 2nd, and 3rd of January, February, and March that fall on a Sunday, Monday, or Tuesday
223 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
224 | run(rule: "RRULE:FREQ=MONTHLY;BYMONTH=1,2,3;BYMONTHDAY=1,2,3;BYDAY=SU,MO,TU;COUNT=10", start: start, results:
225 | ["2018-05-17T09:00:00", "2019-01-01T09:00:00", "2019-02-03T09:00:00", "2019-03-03T09:00:00",
226 | "2020-02-02T09:00:00", "2020-02-03T09:00:00", "2020-03-01T09:00:00", "2020-03-02T09:00:00",
227 | "2020-03-03T09:00:00", "2021-01-03T09:00:00"]
228 | )
229 | }
230 |
231 | // TODO - test BYSETPOS
232 | }
233 |
--------------------------------------------------------------------------------
/RWMRecurrenceRuleTests/RWMRecurrenceRuleBase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RWMRecurrenceRuleBase.swift
3 | // RWMRecurrenceRuleTests
4 | //
5 | // Created by Richard W Maddy on 5/13/18.
6 | // Copyright © 2018 Maddysoft. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import EventKit
11 |
12 | // Helpful site to verify results: http://worthfreeman.com/projects/online-icalendar-recurrence-event-parser/
13 |
14 | class RWMRecurrenceRuleBase: XCTestCase {
15 | let calendar = Calendar(identifier: .gregorian)
16 | let scheduler = RWMRuleScheduler()
17 | let formatter: DateFormatter = {
18 | let res = DateFormatter()
19 | res.locale = Locale(identifier: "en_US_POSIX")
20 | res.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
21 |
22 | return res
23 | }()
24 |
25 | func run(rule: String, start: Date, max: Int = 200, results: [String]) {
26 | run(rule: rule) { (rule) in
27 | var dates = [Date]()
28 | scheduler.enumerateDates(with: rule, startingFrom: start, using: { (date, stop) in
29 | if let date = date {
30 | dates.append(date)
31 | if dates.count >= max {
32 | stop = true
33 | }
34 | }
35 | })
36 |
37 | let list = dates.map { formatter.string(from: $0) }
38 | XCTAssert(list == results, "Incorrect results. Expected \(results), generated \(list)")
39 | }
40 | }
41 |
42 | func run(rule: String, start: Date, last: Date? = nil, count: Int, max: Int = 200) {
43 | run(rule: rule) { (rule) in
44 | var dates = [Date]()
45 | scheduler.enumerateDates(with: rule, startingFrom: start, using: { (date, stop) in
46 | if let date = date {
47 | dates.append(date)
48 | if dates.count >= max {
49 | stop = true
50 | }
51 | }
52 | })
53 |
54 | XCTAssert(dates.count == count, "Should be \(count) dates - \(dates.count)")
55 | XCTAssert(dates.first == start, "Wrong first date")
56 | if let last = last {
57 | XCTAssert(dates.last == last, "Wrong last date \(dates.last?.description ?? "??"), expected \(last)")
58 | }
59 |
60 | let list = dates.map { formatter.string(from: $0) }
61 | print("Results: [\(list.map { "\"\($0)\"" }.joined(separator: ", "))]")
62 | }
63 | }
64 |
65 | func run(rule: String, valid: (RWMRecurrenceRule) -> ()) {
66 | let parser = RWMRuleParser()
67 | if let rules = parser.parse(rule: rule) {
68 | let str = parser.rule(from: rules)
69 | if parser.compare(rule: rule, to: str) {
70 | valid(rules)
71 | } else {
72 | XCTAssert(false, "\(str) doesn't match \(rule)")
73 | }
74 |
75 | if let ekrule = EKRecurrenceRule(recurrenceWith: rule) {
76 | if let _/*rwmrule*/ = RWMRecurrenceRule(recurrenceWith: ekrule) {
77 | // TODO - for now any rule with WKST causes a difference due to EKRecurrenceRule not supported a writable start weekday
78 | //XCTAssert(rwmrule == rules, "rules and rwmrule are not the same: \(parser.rule(from: rules) ?? "X") and \(parser.rule(from: rwmrule) ?? "Y")")
79 | } else {
80 | XCTAssert(false, "Couldn't create RWMRecurrenceRule from EKRecurrenceRule")
81 | }
82 | } else {
83 | XCTAssert(false, "Couldn't create EKRecurrenceRule from \(rule)")
84 | }
85 | } else {
86 | XCTAssert(false, "Couldn't parse \(rule)")
87 | }
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/RWMRecurrenceRuleTests/RWMSpecTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RWMSpecTests.swift
3 | // RWMRecurrenceRuleTests
4 | //
5 | // Created by Richard W Maddy on 5/17/18.
6 | // Copyright © 2018 Maddysoft. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class RWMSpecTests: RWMRecurrenceRuleBase {
12 | // The following tests are based on samples rules from https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html
13 |
14 | func testRule01() {
15 | // Start 19970902T090000
16 | // Daily for 10 occurrences
17 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 2, hour: 9))!
18 | run(rule: "RRULE:FREQ=DAILY;COUNT=10", start: start, results:
19 | ["1997-09-02T09:00:00", "1997-09-03T09:00:00", "1997-09-04T09:00:00", "1997-09-05T09:00:00",
20 | "1997-09-06T09:00:00", "1997-09-07T09:00:00", "1997-09-08T09:00:00", "1997-09-09T09:00:00",
21 | "1997-09-10T09:00:00", "1997-09-11T09:00:00"]
22 | )
23 | }
24 |
25 | func testRule02() {
26 | // Start 19970902T090000
27 | // Daily until December 24, 1997
28 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 2, hour: 9))!
29 | run(rule: "RRULE:FREQ=DAILY;UNTIL=19971224T000000Z", start: start, results:
30 | ["1997-09-02T09:00:00", "1997-09-03T09:00:00", "1997-09-04T09:00:00", "1997-09-05T09:00:00",
31 | "1997-09-06T09:00:00", "1997-09-07T09:00:00", "1997-09-08T09:00:00", "1997-09-09T09:00:00",
32 | "1997-09-10T09:00:00", "1997-09-11T09:00:00", "1997-09-12T09:00:00", "1997-09-13T09:00:00",
33 | "1997-09-14T09:00:00", "1997-09-15T09:00:00", "1997-09-16T09:00:00", "1997-09-17T09:00:00",
34 | "1997-09-18T09:00:00", "1997-09-19T09:00:00", "1997-09-20T09:00:00", "1997-09-21T09:00:00",
35 | "1997-09-22T09:00:00", "1997-09-23T09:00:00", "1997-09-24T09:00:00", "1997-09-25T09:00:00",
36 | "1997-09-26T09:00:00", "1997-09-27T09:00:00", "1997-09-28T09:00:00", "1997-09-29T09:00:00",
37 | "1997-09-30T09:00:00", "1997-10-01T09:00:00", "1997-10-02T09:00:00", "1997-10-03T09:00:00",
38 | "1997-10-04T09:00:00", "1997-10-05T09:00:00", "1997-10-06T09:00:00", "1997-10-07T09:00:00",
39 | "1997-10-08T09:00:00", "1997-10-09T09:00:00", "1997-10-10T09:00:00", "1997-10-11T09:00:00",
40 | "1997-10-12T09:00:00", "1997-10-13T09:00:00", "1997-10-14T09:00:00", "1997-10-15T09:00:00",
41 | "1997-10-16T09:00:00", "1997-10-17T09:00:00", "1997-10-18T09:00:00", "1997-10-19T09:00:00",
42 | "1997-10-20T09:00:00", "1997-10-21T09:00:00", "1997-10-22T09:00:00", "1997-10-23T09:00:00",
43 | "1997-10-24T09:00:00", "1997-10-25T09:00:00", "1997-10-26T09:00:00", "1997-10-27T09:00:00",
44 | "1997-10-28T09:00:00", "1997-10-29T09:00:00", "1997-10-30T09:00:00", "1997-10-31T09:00:00",
45 | "1997-11-01T09:00:00", "1997-11-02T09:00:00", "1997-11-03T09:00:00", "1997-11-04T09:00:00",
46 | "1997-11-05T09:00:00", "1997-11-06T09:00:00", "1997-11-07T09:00:00", "1997-11-08T09:00:00",
47 | "1997-11-09T09:00:00", "1997-11-10T09:00:00", "1997-11-11T09:00:00", "1997-11-12T09:00:00",
48 | "1997-11-13T09:00:00", "1997-11-14T09:00:00", "1997-11-15T09:00:00", "1997-11-16T09:00:00",
49 | "1997-11-17T09:00:00", "1997-11-18T09:00:00", "1997-11-19T09:00:00", "1997-11-20T09:00:00",
50 | "1997-11-21T09:00:00", "1997-11-22T09:00:00", "1997-11-23T09:00:00", "1997-11-24T09:00:00",
51 | "1997-11-25T09:00:00", "1997-11-26T09:00:00", "1997-11-27T09:00:00", "1997-11-28T09:00:00",
52 | "1997-11-29T09:00:00", "1997-11-30T09:00:00", "1997-12-01T09:00:00", "1997-12-02T09:00:00",
53 | "1997-12-03T09:00:00", "1997-12-04T09:00:00", "1997-12-05T09:00:00", "1997-12-06T09:00:00",
54 | "1997-12-07T09:00:00", "1997-12-08T09:00:00", "1997-12-09T09:00:00", "1997-12-10T09:00:00",
55 | "1997-12-11T09:00:00", "1997-12-12T09:00:00", "1997-12-13T09:00:00", "1997-12-14T09:00:00",
56 | "1997-12-15T09:00:00", "1997-12-16T09:00:00", "1997-12-17T09:00:00", "1997-12-18T09:00:00",
57 | "1997-12-19T09:00:00", "1997-12-20T09:00:00", "1997-12-21T09:00:00", "1997-12-22T09:00:00",
58 | "1997-12-23T09:00:00"]
59 | )
60 | }
61 |
62 | func testRule03() {
63 | // Start 19970902T090000
64 | // Every other day - forever
65 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 2, hour: 9))!
66 | run(rule: "RRULE:FREQ=DAILY;INTERVAL=2", start: start, max: 15, results:
67 | ["1997-09-02T09:00:00", "1997-09-04T09:00:00", "1997-09-06T09:00:00", "1997-09-08T09:00:00",
68 | "1997-09-10T09:00:00", "1997-09-12T09:00:00", "1997-09-14T09:00:00", "1997-09-16T09:00:00",
69 | "1997-09-18T09:00:00", "1997-09-20T09:00:00", "1997-09-22T09:00:00", "1997-09-24T09:00:00",
70 | "1997-09-26T09:00:00", "1997-09-28T09:00:00", "1997-09-30T09:00:00"]
71 | )
72 | }
73 |
74 | func testRule04() {
75 | // Start 19970902T090000
76 | // Every 10 days, 5 occurrences
77 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 2, hour: 9))!
78 | run(rule: "RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5", start: start, results:
79 | ["1997-09-02T09:00:00", "1997-09-12T09:00:00", "1997-09-22T09:00:00", "1997-10-02T09:00:00",
80 | "1997-10-12T09:00:00"]
81 | )
82 | }
83 |
84 | func testRule05() {
85 | // Start 19980101T090000
86 | // Every day in January, for 3 years
87 | let start = calendar.date(from: DateComponents(year: 1998, month: 1, day: 1, hour: 9))!
88 | run(rule: "RRULE:FREQ=YEARLY;UNTIL=20000131T140000;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA", start: start, results:
89 | ["1998-01-01T09:00:00", "1998-01-02T09:00:00", "1998-01-03T09:00:00", "1998-01-04T09:00:00",
90 | "1998-01-05T09:00:00", "1998-01-06T09:00:00", "1998-01-07T09:00:00", "1998-01-08T09:00:00",
91 | "1998-01-09T09:00:00", "1998-01-10T09:00:00", "1998-01-11T09:00:00", "1998-01-12T09:00:00",
92 | "1998-01-13T09:00:00", "1998-01-14T09:00:00", "1998-01-15T09:00:00", "1998-01-16T09:00:00",
93 | "1998-01-17T09:00:00", "1998-01-18T09:00:00", "1998-01-19T09:00:00", "1998-01-20T09:00:00",
94 | "1998-01-21T09:00:00", "1998-01-22T09:00:00", "1998-01-23T09:00:00", "1998-01-24T09:00:00",
95 | "1998-01-25T09:00:00", "1998-01-26T09:00:00", "1998-01-27T09:00:00", "1998-01-28T09:00:00",
96 | "1998-01-29T09:00:00", "1998-01-30T09:00:00", "1998-01-31T09:00:00", "1999-01-01T09:00:00",
97 | "1999-01-02T09:00:00", "1999-01-03T09:00:00", "1999-01-04T09:00:00", "1999-01-05T09:00:00",
98 | "1999-01-06T09:00:00", "1999-01-07T09:00:00", "1999-01-08T09:00:00", "1999-01-09T09:00:00",
99 | "1999-01-10T09:00:00", "1999-01-11T09:00:00", "1999-01-12T09:00:00", "1999-01-13T09:00:00",
100 | "1999-01-14T09:00:00", "1999-01-15T09:00:00", "1999-01-16T09:00:00", "1999-01-17T09:00:00",
101 | "1999-01-18T09:00:00", "1999-01-19T09:00:00", "1999-01-20T09:00:00", "1999-01-21T09:00:00",
102 | "1999-01-22T09:00:00", "1999-01-23T09:00:00", "1999-01-24T09:00:00", "1999-01-25T09:00:00",
103 | "1999-01-26T09:00:00", "1999-01-27T09:00:00", "1999-01-28T09:00:00", "1999-01-29T09:00:00",
104 | "1999-01-30T09:00:00", "1999-01-31T09:00:00", "2000-01-01T09:00:00", "2000-01-02T09:00:00",
105 | "2000-01-03T09:00:00", "2000-01-04T09:00:00", "2000-01-05T09:00:00", "2000-01-06T09:00:00",
106 | "2000-01-07T09:00:00", "2000-01-08T09:00:00", "2000-01-09T09:00:00", "2000-01-10T09:00:00",
107 | "2000-01-11T09:00:00", "2000-01-12T09:00:00", "2000-01-13T09:00:00", "2000-01-14T09:00:00",
108 | "2000-01-15T09:00:00", "2000-01-16T09:00:00", "2000-01-17T09:00:00", "2000-01-18T09:00:00",
109 | "2000-01-19T09:00:00", "2000-01-20T09:00:00", "2000-01-21T09:00:00", "2000-01-22T09:00:00",
110 | "2000-01-23T09:00:00", "2000-01-24T09:00:00", "2000-01-25T09:00:00", "2000-01-26T09:00:00",
111 | "2000-01-27T09:00:00", "2000-01-28T09:00:00", "2000-01-29T09:00:00", "2000-01-30T09:00:00",
112 | "2000-01-31T09:00:00"]
113 | )
114 | }
115 |
116 | func testRule06() {
117 | // Start 19980101T090000
118 | // Every day in January, for 3 years
119 | let start = calendar.date(from: DateComponents(year: 1998, month: 1, day: 1, hour: 9))!
120 | run(rule: "RRULE:FREQ=DAILY;UNTIL=20000131T140000;BYMONTH=1", start: start, results:
121 | ["1998-01-01T09:00:00", "1998-01-02T09:00:00", "1998-01-03T09:00:00", "1998-01-04T09:00:00",
122 | "1998-01-05T09:00:00", "1998-01-06T09:00:00", "1998-01-07T09:00:00", "1998-01-08T09:00:00",
123 | "1998-01-09T09:00:00", "1998-01-10T09:00:00", "1998-01-11T09:00:00", "1998-01-12T09:00:00",
124 | "1998-01-13T09:00:00", "1998-01-14T09:00:00", "1998-01-15T09:00:00", "1998-01-16T09:00:00",
125 | "1998-01-17T09:00:00", "1998-01-18T09:00:00", "1998-01-19T09:00:00", "1998-01-20T09:00:00",
126 | "1998-01-21T09:00:00", "1998-01-22T09:00:00", "1998-01-23T09:00:00", "1998-01-24T09:00:00",
127 | "1998-01-25T09:00:00", "1998-01-26T09:00:00", "1998-01-27T09:00:00", "1998-01-28T09:00:00",
128 | "1998-01-29T09:00:00", "1998-01-30T09:00:00", "1998-01-31T09:00:00", "1999-01-01T09:00:00",
129 | "1999-01-02T09:00:00", "1999-01-03T09:00:00", "1999-01-04T09:00:00", "1999-01-05T09:00:00",
130 | "1999-01-06T09:00:00", "1999-01-07T09:00:00", "1999-01-08T09:00:00", "1999-01-09T09:00:00",
131 | "1999-01-10T09:00:00", "1999-01-11T09:00:00", "1999-01-12T09:00:00", "1999-01-13T09:00:00",
132 | "1999-01-14T09:00:00", "1999-01-15T09:00:00", "1999-01-16T09:00:00", "1999-01-17T09:00:00",
133 | "1999-01-18T09:00:00", "1999-01-19T09:00:00", "1999-01-20T09:00:00", "1999-01-21T09:00:00",
134 | "1999-01-22T09:00:00", "1999-01-23T09:00:00", "1999-01-24T09:00:00", "1999-01-25T09:00:00",
135 | "1999-01-26T09:00:00", "1999-01-27T09:00:00", "1999-01-28T09:00:00", "1999-01-29T09:00:00",
136 | "1999-01-30T09:00:00", "1999-01-31T09:00:00", "2000-01-01T09:00:00", "2000-01-02T09:00:00",
137 | "2000-01-03T09:00:00", "2000-01-04T09:00:00", "2000-01-05T09:00:00", "2000-01-06T09:00:00",
138 | "2000-01-07T09:00:00", "2000-01-08T09:00:00", "2000-01-09T09:00:00", "2000-01-10T09:00:00",
139 | "2000-01-11T09:00:00", "2000-01-12T09:00:00", "2000-01-13T09:00:00", "2000-01-14T09:00:00",
140 | "2000-01-15T09:00:00", "2000-01-16T09:00:00", "2000-01-17T09:00:00", "2000-01-18T09:00:00",
141 | "2000-01-19T09:00:00", "2000-01-20T09:00:00", "2000-01-21T09:00:00", "2000-01-22T09:00:00",
142 | "2000-01-23T09:00:00", "2000-01-24T09:00:00", "2000-01-25T09:00:00", "2000-01-26T09:00:00",
143 | "2000-01-27T09:00:00", "2000-01-28T09:00:00", "2000-01-29T09:00:00", "2000-01-30T09:00:00",
144 | "2000-01-31T09:00:00"]
145 | )
146 | }
147 |
148 | func testRule07() {
149 | // Start 19970902T090000
150 | // Weekly for 10 occurrences
151 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 2, hour: 9))!
152 | run(rule: "RRULE:FREQ=WEEKLY;COUNT=10", start: start, results:
153 | ["1997-09-02T09:00:00", "1997-09-09T09:00:00", "1997-09-16T09:00:00", "1997-09-23T09:00:00",
154 | "1997-09-30T09:00:00", "1997-10-07T09:00:00", "1997-10-14T09:00:00", "1997-10-21T09:00:00",
155 | "1997-10-28T09:00:00", "1997-11-04T09:00:00"]
156 | )
157 | }
158 |
159 | func testRule08() {
160 | // Start 19970902T090000
161 | // Weekly until December 24, 1997
162 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 2, hour: 9))!
163 | run(rule: "RRULE:FREQ=WEEKLY;UNTIL=19971224T000000Z", start: start, results:
164 | ["1997-09-02T09:00:00", "1997-09-09T09:00:00", "1997-09-16T09:00:00", "1997-09-23T09:00:00",
165 | "1997-09-30T09:00:00", "1997-10-07T09:00:00", "1997-10-14T09:00:00", "1997-10-21T09:00:00",
166 | "1997-10-28T09:00:00", "1997-11-04T09:00:00", "1997-11-11T09:00:00", "1997-11-18T09:00:00",
167 | "1997-11-25T09:00:00", "1997-12-02T09:00:00", "1997-12-09T09:00:00", "1997-12-16T09:00:00",
168 | "1997-12-23T09:00:00"]
169 | )
170 | }
171 |
172 | func testRule09() {
173 | // Start 19970902T090000
174 | // Every other week - forever
175 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 2, hour: 9))!
176 | run(rule: "RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU", start: start, max: 15, results:
177 | ["1997-09-02T09:00:00", "1997-09-16T09:00:00", "1997-09-30T09:00:00", "1997-10-14T09:00:00",
178 | "1997-10-28T09:00:00", "1997-11-11T09:00:00", "1997-11-25T09:00:00", "1997-12-09T09:00:00",
179 | "1997-12-23T09:00:00", "1998-01-06T09:00:00", "1998-01-20T09:00:00", "1998-02-03T09:00:00",
180 | "1998-02-17T09:00:00", "1998-03-03T09:00:00", "1998-03-17T09:00:00"]
181 | )
182 | }
183 |
184 | func testRule10() {
185 | // Start 19970902T090000
186 | // Weekly on Tuesday and Thursday for five weeks
187 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 2, hour: 9))!
188 | run(rule: "RRULE:FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH", start: start, results:
189 | ["1997-09-02T09:00:00", "1997-09-04T09:00:00", "1997-09-09T09:00:00", "1997-09-11T09:00:00",
190 | "1997-09-16T09:00:00", "1997-09-18T09:00:00", "1997-09-23T09:00:00", "1997-09-25T09:00:00",
191 | "1997-09-30T09:00:00", "1997-10-02T09:00:00"]
192 | )
193 | }
194 |
195 | func testRule11() {
196 | // Start 19970902T090000
197 | // Weekly on Tuesday and Thursday for five weeks
198 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 2, hour: 9))!
199 | run(rule: "RRULE:FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH", start: start, results:
200 | ["1997-09-02T09:00:00", "1997-09-04T09:00:00", "1997-09-09T09:00:00", "1997-09-11T09:00:00",
201 | "1997-09-16T09:00:00", "1997-09-18T09:00:00", "1997-09-23T09:00:00", "1997-09-25T09:00:00",
202 | "1997-09-30T09:00:00", "1997-10-02T09:00:00"]
203 | )
204 | }
205 |
206 | func testRule12() {
207 | // Start 19970901T090000
208 | // Every other week on Monday, Wednesday, and Friday until December 24, 1997, starting on Monday, September 1, 1997
209 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 1, hour: 9))!
210 | run(rule: "RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR", start: start, results:
211 | ["1997-09-01T09:00:00", "1997-09-03T09:00:00", "1997-09-05T09:00:00", "1997-09-15T09:00:00",
212 | "1997-09-17T09:00:00", "1997-09-19T09:00:00", "1997-09-29T09:00:00", "1997-10-01T09:00:00",
213 | "1997-10-03T09:00:00", "1997-10-13T09:00:00", "1997-10-15T09:00:00", "1997-10-17T09:00:00",
214 | "1997-10-27T09:00:00", "1997-10-29T09:00:00", "1997-10-31T09:00:00", "1997-11-10T09:00:00",
215 | "1997-11-12T09:00:00", "1997-11-14T09:00:00", "1997-11-24T09:00:00", "1997-11-26T09:00:00",
216 | "1997-11-28T09:00:00", "1997-12-08T09:00:00", "1997-12-10T09:00:00", "1997-12-12T09:00:00",
217 | "1997-12-22T09:00:00"]
218 | )
219 | }
220 |
221 | func testRule13() {
222 | // Start 19970902T090000
223 | // Every other week on Tuesday and Thursday, for 8 occurrences
224 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 2, hour: 9))!
225 | run(rule: "RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH", start: start, results:
226 | ["1997-09-02T09:00:00", "1997-09-04T09:00:00", "1997-09-16T09:00:00", "1997-09-18T09:00:00",
227 | "1997-09-30T09:00:00", "1997-10-02T09:00:00", "1997-10-14T09:00:00", "1997-10-16T09:00:00"]
228 | )
229 | }
230 |
231 | func testRule14() {
232 | // Start 19970905T090000
233 | // Monthly on the first Friday for 10 occurrences
234 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 5, hour: 9))!
235 | run(rule: "RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR", start: start, results:
236 | ["1997-09-05T09:00:00", "1997-10-03T09:00:00", "1997-11-07T09:00:00", "1997-12-05T09:00:00",
237 | "1998-01-02T09:00:00", "1998-02-06T09:00:00", "1998-03-06T09:00:00", "1998-04-03T09:00:00",
238 | "1998-05-01T09:00:00", "1998-06-05T09:00:00"]
239 | )
240 | }
241 |
242 | func testRule15() {
243 | // Start 19970905T090000
244 | // Monthly on the first Friday until December 24, 1997
245 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 5, hour: 9))!
246 | run(rule: "RRULE:FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR", start: start, results:
247 | ["1997-09-05T09:00:00", "1997-10-03T09:00:00", "1997-11-07T09:00:00", "1997-12-05T09:00:00"]
248 | )
249 | }
250 |
251 | func testRule16() {
252 | // Start 19970907T090000
253 | // Every other month on the first and last Sunday of the month for 10 occurrences
254 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 7, hour: 9))!
255 | run(rule: "RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU", start: start, results:
256 | ["1997-09-07T09:00:00", "1997-09-28T09:00:00", "1997-11-02T09:00:00", "1997-11-30T09:00:00",
257 | "1998-01-04T09:00:00", "1998-01-25T09:00:00", "1998-03-01T09:00:00", "1998-03-29T09:00:00",
258 | "1998-05-03T09:00:00", "1998-05-31T09:00:00"]
259 | )
260 | }
261 |
262 | func testRule17() {
263 | // Start 19970922T090000
264 | // Monthly on the second-to-last Monday of the month for 6 months
265 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 22, hour: 9))!
266 | run(rule: "RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=-2MO", start: start, results:
267 | ["1997-09-22T09:00:00", "1997-10-20T09:00:00", "1997-11-17T09:00:00", "1997-12-22T09:00:00",
268 | "1998-01-19T09:00:00", "1998-02-16T09:00:00"]
269 | )
270 | }
271 |
272 | func testRule18() {
273 | // Start 19970928T090000
274 | // Monthly on the third-to-the-last day of the month, forever
275 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 28, hour: 9))!
276 | run(rule: "RRULE:FREQ=MONTHLY;BYMONTHDAY=-3", start: start, max: 6, results:
277 | ["1997-09-28T09:00:00", "1997-10-29T09:00:00", "1997-11-28T09:00:00", "1997-12-29T09:00:00",
278 | "1998-01-29T09:00:00", "1998-02-26T09:00:00"]
279 | )
280 | }
281 |
282 | func testRule19() {
283 | // Start 19970902T090000
284 | // Monthly on the 2nd and 15th of the month for 10 occurrences
285 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 2, hour: 9))!
286 | run(rule: "RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15", start: start, results:
287 | ["1997-09-02T09:00:00", "1997-09-15T09:00:00", "1997-10-02T09:00:00", "1997-10-15T09:00:00",
288 | "1997-11-02T09:00:00", "1997-11-15T09:00:00", "1997-12-02T09:00:00", "1997-12-15T09:00:00",
289 | "1998-01-02T09:00:00", "1998-01-15T09:00:00"]
290 | )
291 | }
292 |
293 | func testRule20() {
294 | // Start 19970930T090000
295 | // Monthly on the first and last day of the month for 10 occurrences
296 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 30, hour: 9))!
297 | run(rule: "RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1", start: start, results:
298 | ["1997-09-30T09:00:00", "1997-10-01T09:00:00", "1997-10-31T09:00:00", "1997-11-01T09:00:00",
299 | "1997-11-30T09:00:00", "1997-12-01T09:00:00", "1997-12-31T09:00:00", "1998-01-01T09:00:00",
300 | "1998-01-31T09:00:00", "1998-02-01T09:00:00"]
301 | )
302 | }
303 |
304 | func testRule21() {
305 | // Start 19970910T090000
306 | // Every 18 months on the 10th thru 15th of the month for 10 occurrences
307 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 10, hour: 9))!
308 | run(rule: "RRULE:FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15", start: start, results:
309 | ["1997-09-10T09:00:00", "1997-09-11T09:00:00", "1997-09-12T09:00:00", "1997-09-13T09:00:00",
310 | "1997-09-14T09:00:00", "1997-09-15T09:00:00", "1999-03-10T09:00:00", "1999-03-11T09:00:00",
311 | "1999-03-12T09:00:00", "1999-03-13T09:00:00"]
312 | )
313 | }
314 |
315 | func testRule22() {
316 | // Start 19970902T090000
317 | // Every Tuesday, every other month
318 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 2, hour: 9))!
319 | run(rule: "RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=TU", start: start, max: 18, results:
320 | ["1997-09-02T09:00:00", "1997-09-09T09:00:00", "1997-09-16T09:00:00", "1997-09-23T09:00:00",
321 | "1997-09-30T09:00:00", "1997-11-04T09:00:00", "1997-11-11T09:00:00", "1997-11-18T09:00:00",
322 | "1997-11-25T09:00:00", "1998-01-06T09:00:00", "1998-01-13T09:00:00", "1998-01-20T09:00:00",
323 | "1998-01-27T09:00:00", "1998-03-03T09:00:00", "1998-03-10T09:00:00", "1998-03-17T09:00:00",
324 | "1998-03-24T09:00:00", "1998-03-31T09:00:00"]
325 | )
326 | }
327 |
328 | func testRule23() {
329 | // Start 19970610T090000
330 | // Yearly in June and July for 10 occurrences
331 | let start = calendar.date(from: DateComponents(year: 1997, month: 6, day: 10, hour: 9))!
332 | run(rule: "RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7", start: start, results:
333 | ["1997-06-10T09:00:00", "1997-07-10T09:00:00", "1998-06-10T09:00:00", "1998-07-10T09:00:00",
334 | "1999-06-10T09:00:00", "1999-07-10T09:00:00", "2000-06-10T09:00:00", "2000-07-10T09:00:00",
335 | "2001-06-10T09:00:00", "2001-07-10T09:00:00"]
336 | )
337 | }
338 |
339 | func testRule24() {
340 | // Start 19970310T090000
341 | // Every other year on January, February, and March for 10 occurrences
342 | let start = calendar.date(from: DateComponents(year: 1997, month: 3, day: 10, hour: 9))!
343 | run(rule: "RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3", start: start, results:
344 | ["1997-03-10T09:00:00", "1999-01-10T09:00:00", "1999-02-10T09:00:00", "1999-03-10T09:00:00",
345 | "2001-01-10T09:00:00", "2001-02-10T09:00:00", "2001-03-10T09:00:00", "2003-01-10T09:00:00",
346 | "2003-02-10T09:00:00", "2003-03-10T09:00:00"]
347 | )
348 | }
349 |
350 | func testRule25() {
351 | // Start 19970101T090000
352 | // Every third year on the 1st, 100th, and 200th day for 10 occurrences
353 | let start = calendar.date(from: DateComponents(year: 1997, month: 1, day: 1, hour: 9))!
354 | run(rule: "RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200", start: start, results:
355 | ["1997-01-01T09:00:00", "1997-04-10T09:00:00", "1997-07-19T09:00:00", "2000-01-01T09:00:00",
356 | "2000-04-09T09:00:00", "2000-07-18T09:00:00", "2003-01-01T09:00:00", "2003-04-10T09:00:00",
357 | "2003-07-19T09:00:00", "2006-01-01T09:00:00"]
358 | )
359 | }
360 |
361 | func testRule26() {
362 | // Start 19970519T090000
363 | // Every 20th Monday of the year, forever
364 | let start = calendar.date(from: DateComponents(year: 1997, month: 5, day: 19, hour: 9))!
365 | run(rule: "RRULE:FREQ=YEARLY;BYDAY=20MO", start: start, max: 3, results:
366 | ["1997-05-19T09:00:00", "1998-05-18T09:00:00", "1999-05-17T09:00:00"]
367 | )
368 | }
369 |
370 | func testRule27() {
371 | // Start 19970512T090000
372 | // Monday of week number 20 (where the default start of the week is Monday), forever
373 | let start = calendar.date(from: DateComponents(year: 1997, month: 5, day: 12, hour: 9))!
374 | run(rule: "RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO", start: start, max: 3, results:
375 | ["1997-05-12T09:00:00", "1998-05-11T09:00:00", "1999-05-17T09:00:00"]
376 | )
377 | }
378 |
379 | func testRule28() {
380 | // Start 19970313T090000
381 | // Every Thursday in March, forever
382 | let start = calendar.date(from: DateComponents(year: 1997, month: 3, day: 13, hour: 9))!
383 | run(rule: "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=TH", start: start, max: 11, results:
384 | ["1997-03-13T09:00:00", "1997-03-20T09:00:00", "1997-03-27T09:00:00", "1998-03-05T09:00:00",
385 | "1998-03-12T09:00:00", "1998-03-19T09:00:00", "1998-03-26T09:00:00", "1999-03-04T09:00:00",
386 | "1999-03-11T09:00:00", "1999-03-18T09:00:00", "1999-03-25T09:00:00"]
387 | )
388 | }
389 |
390 | func testRule29() {
391 | // Start 19970605T090000
392 | // Every Thursday, but only during June, July, and August, forever
393 | let start = calendar.date(from: DateComponents(year: 1997, month: 6, day: 5, hour: 9))!
394 | run(rule: "RRULE:FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8", start: start, max: 39, results:
395 | ["1997-06-05T09:00:00", "1997-06-12T09:00:00", "1997-06-19T09:00:00", "1997-06-26T09:00:00",
396 | "1997-07-03T09:00:00", "1997-07-10T09:00:00", "1997-07-17T09:00:00", "1997-07-24T09:00:00",
397 | "1997-07-31T09:00:00", "1997-08-07T09:00:00", "1997-08-14T09:00:00", "1997-08-21T09:00:00",
398 | "1997-08-28T09:00:00", "1998-06-04T09:00:00", "1998-06-11T09:00:00", "1998-06-18T09:00:00",
399 | "1998-06-25T09:00:00", "1998-07-02T09:00:00", "1998-07-09T09:00:00", "1998-07-16T09:00:00",
400 | "1998-07-23T09:00:00", "1998-07-30T09:00:00", "1998-08-06T09:00:00", "1998-08-13T09:00:00",
401 | "1998-08-20T09:00:00", "1998-08-27T09:00:00", "1999-06-03T09:00:00", "1999-06-10T09:00:00",
402 | "1999-06-17T09:00:00", "1999-06-24T09:00:00", "1999-07-01T09:00:00", "1999-07-08T09:00:00",
403 | "1999-07-15T09:00:00", "1999-07-22T09:00:00", "1999-07-29T09:00:00", "1999-08-05T09:00:00",
404 | "1999-08-12T09:00:00", "1999-08-19T09:00:00", "1999-08-26T09:00:00"]
405 | )
406 | }
407 |
408 | func testRule30() {
409 | // Start 19970902T090000
410 | // Every Friday the 13th, forever
411 | // NOTE: The spec example for this includes EXDATE;TZID=America/New_York:19970902T090000 so the start date
412 | // isn't returned. This test doesn't support that so this results in that one extra date.
413 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 2, hour: 9))!
414 | run(rule: "RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13", start: start, max: 6, results:
415 | ["1997-09-02T09:00:00", "1998-02-13T09:00:00", "1998-03-13T09:00:00", "1998-11-13T09:00:00",
416 | "1999-08-13T09:00:00", "2000-10-13T09:00:00"]
417 | )
418 | }
419 |
420 | func testRule31() {
421 | // Start 19970913T090000
422 | // The first Saturday that follows the first Sunday of the month, forever
423 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 13, hour: 9))!
424 | run(rule: "RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13", start: start, max: 10, results:
425 | ["1997-09-13T09:00:00", "1997-10-11T09:00:00", "1997-11-08T09:00:00", "1997-12-13T09:00:00",
426 | "1998-01-10T09:00:00", "1998-02-07T09:00:00", "1998-03-07T09:00:00", "1998-04-11T09:00:00",
427 | "1998-05-09T09:00:00", "1998-06-13T09:00:00"]
428 | )
429 | }
430 |
431 | func testRule32() {
432 | // 19961105T090000
433 | // Every 4 years, the first Tuesday after a Monday in November, forever (U.S. Presidential Election day)
434 | let start = calendar.date(from: DateComponents(year: 1996, month: 11, day: 5, hour: 9))!
435 | run(rule: "RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8", start: start, max: 3, results:
436 | ["1996-11-05T09:00:00", "2000-11-07T09:00:00", "2004-11-02T09:00:00"]
437 | )
438 | }
439 |
440 | func testRule33() {
441 | // Start 19970904T090000
442 | // The third instance into the month of one of Tuesday, Wednesday, or Thursday, for the next 3 months
443 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 4, hour: 9))!
444 | run(rule: "RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3", start: start, results:
445 | ["1997-09-04T09:00:00", "1997-10-07T09:00:00", "1997-11-06T09:00:00"]
446 | )
447 | }
448 |
449 | func testRule34() {
450 | // Start 19970929T090000
451 | // The second-to-last weekday of the month
452 | let start = calendar.date(from: DateComponents(year: 1997, month: 9, day: 29, hour: 9))!
453 | run(rule: "RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2", start: start, max: 7, results:
454 | ["1997-09-29T09:00:00", "1997-10-30T09:00:00", "1997-11-27T09:00:00", "1997-12-30T09:00:00",
455 | "1998-01-29T09:00:00", "1998-02-26T09:00:00", "1998-03-30T09:00:00"]
456 | )
457 | }
458 |
459 | /*
460 | func testRule35() {
461 | run(rule: "RRULE:FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000Z") { (rule) in
462 | }
463 | }
464 | */
465 |
466 | /*
467 | func testRule36() {
468 | run(rule: "RRULE:FREQ=MINUTELY;INTERVAL=15;COUNT=6") { (rule) in
469 | }
470 | }
471 | */
472 |
473 | /*
474 | func testRule37() {
475 | run(rule: "RRULE:FREQ=MINUTELY;INTERVAL=90;COUNT=4") { (rule) in
476 | }
477 | }
478 | */
479 |
480 | /*
481 | func testRule38() {
482 | run(rule: "RRULE:FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40") { (rule) in
483 | }
484 | }
485 | */
486 |
487 | /*
488 | func testRule39() {
489 | run(rule: "RRULE:FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16") { (rule) in
490 | }
491 | }
492 | */
493 |
494 | func testRule40() {
495 | // Start 19970805T090000
496 | // An example where the days generated makes a difference because of WKST
497 | let start = calendar.date(from: DateComponents(year: 1997, month: 8, day: 5, hour: 9))!
498 | run(rule: "RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO", start: start, results:
499 | ["1997-08-05T09:00:00", "1997-08-10T09:00:00", "1997-08-19T09:00:00", "1997-08-24T09:00:00"])
500 | }
501 |
502 | func testRule41() {
503 | // Start 19970805T090000
504 | // changing only WKST from MO to SU, yields different results...
505 | let start = calendar.date(from: DateComponents(year: 1997, month: 8, day: 5, hour: 9))!
506 | run(rule: "RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU", start: start, results:
507 | ["1997-08-05T09:00:00", "1997-08-17T09:00:00", "1997-08-19T09:00:00", "1997-08-31T09:00:00"]
508 | )
509 | }
510 |
511 | func testRule42() {
512 | // Start 20070115T090000
513 | // An example where an invalid date (i.e., February 30) is ignored
514 | let start = calendar.date(from: DateComponents(year: 2007, month: 1, day: 15, hour: 9))!
515 | run(rule: "RRULE:FREQ=MONTHLY;BYMONTHDAY=15,30;COUNT=5", start: start, results:
516 | ["2007-01-15T09:00:00", "2007-01-30T09:00:00", "2007-02-15T09:00:00", "2007-03-15T09:00:00",
517 | "2007-03-30T09:00:00"]
518 | )
519 | }
520 | }
521 |
--------------------------------------------------------------------------------
/RWMRecurrenceRuleTests/RWMWeeklyTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RWMWeeklyTests.swift
3 | // RWMRecurrenceRuleTests
4 | //
5 | // Created by Richard W Maddy on 5/17/18.
6 | // Copyright © 2018 Maddysoft. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class RWMWeeklyTests: RWMRecurrenceRuleBase {
12 | // ----------- WEEKLY ------------
13 |
14 | // Weekly can use BYMONTH, BYDAY, BYSETPOS
15 |
16 | func testWeekly01() {
17 | // Start 20180517T090000
18 | // Weekly with no BYxxx clauses. Should give several weeks with same day as start date
19 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
20 | run(rule: "RRULE:FREQ=WEEKLY;COUNT=10", start: start, results:
21 | ["2018-05-17T09:00:00", "2018-05-24T09:00:00", "2018-05-31T09:00:00", "2018-06-07T09:00:00",
22 | "2018-06-14T09:00:00", "2018-06-21T09:00:00", "2018-06-28T09:00:00", "2018-07-05T09:00:00",
23 | "2018-07-12T09:00:00", "2018-07-19T09:00:00"]
24 | )
25 | }
26 |
27 | func testWeekly02() {
28 | // Start 20180517T090000
29 | // Every third week.
30 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
31 | run(rule: "RRULE:FREQ=WEEKLY;INTERVAL=3;COUNT=10", start: start, results:
32 | ["2018-05-17T09:00:00", "2018-06-07T09:00:00", "2018-06-28T09:00:00", "2018-07-19T09:00:00",
33 | "2018-08-09T09:00:00", "2018-08-30T09:00:00", "2018-09-20T09:00:00", "2018-10-11T09:00:00",
34 | "2018-11-01T09:00:00", "2018-11-22T09:00:00"]
35 | )
36 | }
37 |
38 | func testWeekly03() {
39 | // Start 20180517T090000
40 | // Weekly but only in June.
41 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
42 | run(rule: "RRULE:FREQ=WEEKLY;BYMONTH=6;COUNT=10", start: start, results:
43 | ["2018-05-17T09:00:00", "2018-06-07T09:00:00", "2018-06-14T09:00:00", "2018-06-21T09:00:00",
44 | "2018-06-28T09:00:00", "2019-06-06T09:00:00", "2019-06-13T09:00:00", "2019-06-20T09:00:00",
45 | "2019-06-27T09:00:00", "2020-06-04T09:00:00"]
46 | )
47 | }
48 |
49 | func testWeekly04() {
50 | // Start 20180517T090000
51 | // Every third week, but only in June
52 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
53 | run(rule: "RRULE:FREQ=WEEKLY;INTERVAL=3;BYMONTH=6;COUNT=10", start: start, results:
54 | ["2018-05-17T09:00:00", "2018-06-07T09:00:00", "2018-06-28T09:00:00", "2019-06-20T09:00:00",
55 | "2020-06-11T09:00:00", "2021-06-03T09:00:00", "2021-06-24T09:00:00", "2022-06-16T09:00:00",
56 | "2023-06-08T09:00:00", "2023-06-29T09:00:00"]
57 | )
58 | }
59 |
60 | func testWeekly05() {
61 | // Start 20180517T090000
62 | // Weekly but only in June or September.
63 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
64 | run(rule: "RRULE:FREQ=WEEKLY;BYMONTH=6,9;COUNT=10", start: start, results:
65 | ["2018-05-17T09:00:00", "2018-06-07T09:00:00", "2018-06-14T09:00:00", "2018-06-21T09:00:00",
66 | "2018-06-28T09:00:00", "2018-09-06T09:00:00", "2018-09-13T09:00:00", "2018-09-20T09:00:00",
67 | "2018-09-27T09:00:00", "2019-06-06T09:00:00"]
68 | )
69 | }
70 |
71 | func testWeekly06() {
72 | // Start 20180517T090000
73 | // Weekly on Monday, Wednesday, Friday.
74 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
75 | run(rule: "RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=20", start: start, results:
76 | ["2018-05-17T09:00:00", "2018-05-18T09:00:00", "2018-05-21T09:00:00", "2018-05-23T09:00:00",
77 | "2018-05-25T09:00:00", "2018-05-28T09:00:00", "2018-05-30T09:00:00", "2018-06-01T09:00:00",
78 | "2018-06-04T09:00:00", "2018-06-06T09:00:00", "2018-06-08T09:00:00", "2018-06-11T09:00:00",
79 | "2018-06-13T09:00:00", "2018-06-15T09:00:00", "2018-06-18T09:00:00", "2018-06-20T09:00:00",
80 | "2018-06-22T09:00:00", "2018-06-25T09:00:00", "2018-06-27T09:00:00", "2018-06-29T09:00:00"]
81 | )
82 | }
83 |
84 | func testWeekly07() {
85 | // Start 20180517T090000
86 | // Every third week on Tuesday/Thursday
87 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
88 | run(rule: "RRULE:FREQ=WEEKLY;INTERVAL=3;BYDAY=TU,TH;COUNT=20", start: start, results:
89 | ["2018-05-17T09:00:00", "2018-06-05T09:00:00", "2018-06-07T09:00:00", "2018-06-26T09:00:00",
90 | "2018-06-28T09:00:00", "2018-07-17T09:00:00", "2018-07-19T09:00:00", "2018-08-07T09:00:00",
91 | "2018-08-09T09:00:00", "2018-08-28T09:00:00", "2018-08-30T09:00:00", "2018-09-18T09:00:00",
92 | "2018-09-20T09:00:00", "2018-10-09T09:00:00", "2018-10-11T09:00:00", "2018-10-30T09:00:00",
93 | "2018-11-01T09:00:00", "2018-11-20T09:00:00", "2018-11-22T09:00:00", "2018-12-11T09:00:00"]
94 | )
95 | }
96 |
97 | func testWeekly08() {
98 | // Start 20180517T090000
99 | // Weekly on Monday, Wednesday, Friday in April or August
100 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
101 | run(rule: "RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;BYMONTH=4,8;COUNT=20", start: start, results:
102 | ["2018-05-17T09:00:00", "2018-08-01T09:00:00", "2018-08-03T09:00:00", "2018-08-06T09:00:00",
103 | "2018-08-08T09:00:00", "2018-08-10T09:00:00", "2018-08-13T09:00:00", "2018-08-15T09:00:00",
104 | "2018-08-17T09:00:00", "2018-08-20T09:00:00", "2018-08-22T09:00:00", "2018-08-24T09:00:00",
105 | "2018-08-27T09:00:00", "2018-08-29T09:00:00", "2018-08-31T09:00:00", "2019-04-01T09:00:00",
106 | "2019-04-03T09:00:00", "2019-04-05T09:00:00", "2019-04-08T09:00:00", "2019-04-10T09:00:00"]
107 | )
108 | }
109 |
110 | func testWeekly09() {
111 | // Start 20180517T090000
112 | // Every third week on Tuesday/Thursday in June
113 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
114 | run(rule: "RRULE:FREQ=WEEKLY;INTERVAL=3;BYDAY=TU,TH;BYMONTH=6;COUNT=20", start: start, results:
115 | ["2018-05-17T09:00:00", "2018-06-05T09:00:00", "2018-06-07T09:00:00", "2018-06-26T09:00:00",
116 | "2018-06-28T09:00:00", "2019-06-18T09:00:00", "2019-06-20T09:00:00", "2020-06-09T09:00:00",
117 | "2020-06-11T09:00:00", "2020-06-30T09:00:00", "2021-06-01T09:00:00", "2021-06-03T09:00:00",
118 | "2021-06-22T09:00:00", "2021-06-24T09:00:00", "2022-06-14T09:00:00", "2022-06-16T09:00:00",
119 | "2023-06-06T09:00:00", "2023-06-08T09:00:00", "2023-06-27T09:00:00", "2023-06-29T09:00:00"]
120 | )
121 | }
122 |
123 | func testWeekly10() {
124 | // Start 20180517T090000
125 | // Weekly on Monday, Wednesday, Friday in April or August
126 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
127 | run(rule: "RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;BYMONTH=4,8;BYSETPOS=-1,1;COUNT=20", start: start, results:
128 | ["2018-05-17T09:00:00", "2018-08-01T09:00:00", "2018-08-03T09:00:00", "2018-08-06T09:00:00",
129 | "2018-08-10T09:00:00", "2018-08-13T09:00:00", "2018-08-17T09:00:00", "2018-08-20T09:00:00",
130 | "2018-08-24T09:00:00", "2018-08-27T09:00:00", "2018-08-31T09:00:00", "2019-04-01T09:00:00",
131 | "2019-04-05T09:00:00", "2019-04-08T09:00:00", "2019-04-12T09:00:00", "2019-04-15T09:00:00",
132 | "2019-04-19T09:00:00", "2019-04-22T09:00:00", "2019-04-26T09:00:00", "2019-04-29T09:00:00"]
133 | )
134 | }
135 |
136 | func testWeekly11() {
137 | // Start 20180517T090000
138 | // Every third week on Tuesday/Thursday in June
139 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
140 | run(rule: "RRULE:FREQ=WEEKLY;INTERVAL=3;BYDAY=TU,TH;BYMONTH=6;BYSETPOS=1;COUNT=20", start: start, results:
141 | ["2018-05-17T09:00:00", "2018-06-05T09:00:00", "2018-06-26T09:00:00", "2019-06-18T09:00:00",
142 | "2020-06-09T09:00:00", "2020-06-30T09:00:00", "2021-06-01T09:00:00", "2021-06-22T09:00:00",
143 | "2022-06-14T09:00:00", "2023-06-06T09:00:00", "2023-06-27T09:00:00", "2024-06-18T09:00:00",
144 | "2025-06-10T09:00:00", "2026-06-02T09:00:00", "2026-06-23T09:00:00", "2027-06-15T09:00:00",
145 | "2028-06-06T09:00:00", "2028-06-27T09:00:00", "2029-06-19T09:00:00", "2030-06-11T09:00:00"]
146 | )
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/RWMRecurrenceRuleTests/RWMYearlyTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RWMYearlyTests.swift
3 | // RWMRecurrenceRuleTests
4 | //
5 | // Created by Richard W Maddy on 5/17/18.
6 | // Copyright © 2018 Maddysoft. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class RWMYearlyTests: RWMRecurrenceRuleBase {
12 | // ----------- YEARLY ------------
13 |
14 | // Yearly can use BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY, BYSETPOS
15 |
16 | func testYearly01() {
17 | // Start 20180517T090000
18 | // Yearly with no BYxxx clauses. Should give same date as start date for 3 years
19 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
20 | run(rule: "RRULE:FREQ=YEARLY;COUNT=3", start: start, results:
21 | ["2018-05-17T09:00:00", "2019-05-17T09:00:00", "2020-05-17T09:00:00"])
22 | }
23 |
24 | func testYearly02() {
25 | // Start 20180517T090000
26 | // Start day in February, April, and June of each year
27 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
28 | run(rule: "RRULE:FREQ=YEARLY;BYMONTH=2,4,6;COUNT=10", start: start, results:
29 | ["2018-05-17T09:00:00", "2018-06-17T09:00:00", "2019-02-17T09:00:00", "2019-04-17T09:00:00",
30 | "2019-06-17T09:00:00", "2020-02-17T09:00:00", "2020-04-17T09:00:00", "2020-06-17T09:00:00",
31 | "2021-02-17T09:00:00", "2021-04-17T09:00:00"]
32 | )
33 | }
34 |
35 | func testYearly03() {
36 | // Start 20180517T090000
37 | // Every day in the 2nd, 4th, and 6th week of the year
38 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
39 | run(rule: "RRULE:FREQ=YEARLY;BYWEEKNO=2,4,6;COUNT=15", start: start, results:
40 | ["2018-05-17T09:00:00", "2019-01-07T09:00:00", "2019-01-08T09:00:00", "2019-01-09T09:00:00",
41 | "2019-01-10T09:00:00", "2019-01-11T09:00:00", "2019-01-12T09:00:00", "2019-01-13T09:00:00",
42 | "2019-01-21T09:00:00", "2019-01-22T09:00:00", "2019-01-23T09:00:00", "2019-01-24T09:00:00",
43 | "2019-01-25T09:00:00", "2019-01-26T09:00:00", "2019-01-27T09:00:00"]
44 | )
45 | }
46 |
47 | func testYearly04() {
48 | // Start 20180110T090000
49 | // Every day in the last week of the year
50 | let start = calendar.date(from: DateComponents(year: 2018, month: 1, day: 10, hour: 9))!
51 | run(rule: "RRULE:FREQ=YEARLY;BYWEEKNO=-1;COUNT=45", start: start, results:
52 | ["2018-01-10T09:00:00", "2018-12-24T09:00:00", "2018-12-25T09:00:00", "2018-12-26T09:00:00",
53 | "2018-12-27T09:00:00", "2018-12-28T09:00:00", "2018-12-29T09:00:00", "2018-12-30T09:00:00",
54 | "2019-12-23T09:00:00", "2019-12-24T09:00:00", "2019-12-25T09:00:00", "2019-12-26T09:00:00",
55 | "2019-12-27T09:00:00", "2019-12-28T09:00:00", "2019-12-29T09:00:00", "2020-12-28T09:00:00",
56 | "2020-12-29T09:00:00", "2020-12-30T09:00:00", "2020-12-31T09:00:00", "2021-12-27T09:00:00",
57 | "2021-12-28T09:00:00", "2021-12-29T09:00:00", "2021-12-30T09:00:00", "2021-12-31T09:00:00",
58 | "2022-12-26T09:00:00", "2022-12-27T09:00:00", "2022-12-28T09:00:00", "2022-12-29T09:00:00",
59 | "2022-12-30T09:00:00", "2022-12-31T09:00:00", "2023-12-25T09:00:00", "2023-12-26T09:00:00",
60 | "2023-12-27T09:00:00", "2023-12-28T09:00:00", "2023-12-29T09:00:00", "2023-12-30T09:00:00",
61 | "2023-12-31T09:00:00", "2024-12-23T09:00:00", "2024-12-24T09:00:00", "2024-12-25T09:00:00",
62 | "2024-12-26T09:00:00", "2024-12-27T09:00:00", "2024-12-28T09:00:00", "2024-12-29T09:00:00",
63 | "2025-12-22T09:00:00"]
64 | )
65 | }
66 |
67 | func testYearly04a() {
68 | // Start 20180110T090000
69 | // Every day in the 53rd week of the year
70 | let start = calendar.date(from: DateComponents(year: 2018, month: 1, day: 10, hour: 9))!
71 | run(rule: "RRULE:FREQ=YEARLY;BYWEEKNO=53;COUNT=25", start: start, results:
72 | ["2018-01-10T09:00:00", "2018-12-31T09:00:00", "2019-12-30T09:00:00", "2019-12-31T09:00:00",
73 | "2020-12-28T09:00:00", "2020-12-29T09:00:00", "2020-12-30T09:00:00", "2020-12-31T09:00:00",
74 | "2024-12-30T09:00:00", "2024-12-31T09:00:00", "2025-12-29T09:00:00", "2025-12-30T09:00:00",
75 | "2025-12-31T09:00:00", "2026-12-28T09:00:00", "2026-12-29T09:00:00", "2026-12-30T09:00:00",
76 | "2026-12-31T09:00:00", "2029-12-31T09:00:00", "2030-12-30T09:00:00", "2030-12-31T09:00:00",
77 | "2031-12-29T09:00:00", "2031-12-30T09:00:00", "2031-12-31T09:00:00", "2032-12-27T09:00:00",
78 | "2032-12-28T09:00:00"]
79 | )
80 | }
81 |
82 | func testYearly05() {
83 | // Start 20180517T090000
84 | // Every day in the 2nd-to-last, 4th-to-last, and 6th-to-last week of the year
85 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
86 | run(rule: "RRULE:FREQ=YEARLY;BYWEEKNO=-2,-4,-6;COUNT=15", start: start, results:
87 | ["2018-05-17T09:00:00", "2018-11-19T09:00:00", "2018-11-20T09:00:00", "2018-11-21T09:00:00",
88 | "2018-11-22T09:00:00", "2018-11-23T09:00:00", "2018-11-24T09:00:00", "2018-11-25T09:00:00",
89 | "2018-12-03T09:00:00", "2018-12-04T09:00:00", "2018-12-05T09:00:00", "2018-12-06T09:00:00",
90 | "2018-12-07T09:00:00", "2018-12-08T09:00:00", "2018-12-09T09:00:00"]
91 | )
92 | }
93 |
94 | func testYearly06() {
95 | // Start 20180517T090000
96 | // The 20th, 45th, and 160th day of each year (Jan 10, Feb 14, and Jun 8 or 9 (depending on leap year))
97 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
98 | run(rule: "RRULE:FREQ=YEARLY;BYYEARDAY=20,45,160;COUNT=10", start: start, results:
99 | ["2018-05-17T09:00:00", "2018-06-09T09:00:00", "2019-01-20T09:00:00", "2019-02-14T09:00:00",
100 | "2019-06-09T09:00:00", "2020-01-20T09:00:00", "2020-02-14T09:00:00", "2020-06-08T09:00:00",
101 | "2021-01-20T09:00:00", "2021-02-14T09:00:00"]
102 | )
103 | }
104 |
105 | func testYearly07() {
106 | // Start 20180517T090000
107 | // The 20th, 45th, and 160th day of each year (Jan 10, Feb 14, and Jun 8 or 9 (depending on leap year))
108 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
109 | run(rule: "RRULE:FREQ=YEARLY;BYYEARDAY=-1,-150;COUNT=10", start: start, results:
110 | ["2018-05-17T09:00:00", "2018-08-04T09:00:00", "2018-12-31T09:00:00", "2019-08-04T09:00:00",
111 | "2019-12-31T09:00:00", "2020-08-04T09:00:00", "2020-12-31T09:00:00", "2021-08-04T09:00:00",
112 | "2021-12-31T09:00:00", "2022-08-04T09:00:00"]
113 | )
114 | }
115 |
116 | func testYearly08() {
117 | // Start 20180517T090000
118 | // The 4th, 8th, and 12th of each month of the year
119 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
120 | run(rule: "RRULE:FREQ=YEARLY;BYMONTHDAY=4,8,12;COUNT=10", start: start, results:
121 | ["2018-05-17T09:00:00", "2018-06-04T09:00:00", "2018-06-08T09:00:00", "2018-06-12T09:00:00",
122 | "2018-07-04T09:00:00", "2018-07-08T09:00:00", "2018-07-12T09:00:00", "2018-08-04T09:00:00",
123 | "2018-08-08T09:00:00", "2018-08-12T09:00:00"]
124 | )
125 | }
126 |
127 | func testYearly09() {
128 | // Start 20180517T090000
129 | // Every Sunday of the year
130 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
131 | run(rule: "RRULE:FREQ=YEARLY;BYDAY=SU;COUNT=10", start: start, results:
132 | ["2018-05-17T09:00:00", "2018-05-20T09:00:00", "2018-05-27T09:00:00", "2018-06-03T09:00:00",
133 | "2018-06-10T09:00:00", "2018-06-17T09:00:00", "2018-06-24T09:00:00", "2018-07-01T09:00:00",
134 | "2018-07-08T09:00:00", "2018-07-15T09:00:00"]
135 | )
136 | }
137 |
138 | func testYearly10() {
139 | // Start 20180517T090000
140 | // Every Monday of the year and the 23rd Thursday of the year
141 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
142 | run(rule: "RRULE:FREQ=YEARLY;BYDAY=MO,23TH;COUNT=10", start: start, results:
143 | ["2018-05-17T09:00:00", "2018-05-21T09:00:00", "2018-05-28T09:00:00", "2018-06-04T09:00:00",
144 | "2018-06-07T09:00:00", "2018-06-11T09:00:00", "2018-06-18T09:00:00", "2018-06-25T09:00:00",
145 | "2018-07-02T09:00:00", "2018-07-09T09:00:00"]
146 | )
147 | }
148 |
149 | func testYearly11() {
150 | // Start 20180517T090000
151 | // The last Thursday, 10th-to-last Thursday, and 15th-to-last Wednesday of each year
152 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
153 | run(rule: "RRULE:FREQ=YEARLY;BYDAY=-1TH,-10TH,-15WE;COUNT=10", start: start, results:
154 | ["2018-05-17T09:00:00", "2018-09-19T09:00:00", "2018-10-25T09:00:00", "2018-12-27T09:00:00",
155 | "2019-09-18T09:00:00", "2019-10-24T09:00:00", "2019-12-26T09:00:00", "2020-09-23T09:00:00",
156 | "2020-10-29T09:00:00", "2020-12-31T09:00:00"]
157 | )
158 | }
159 |
160 | func testYearly12() {
161 | // Start 20180517T090000
162 | // Days from the 5th week of the year falling in February
163 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
164 | run(rule: "RRULE:FREQ=YEARLY;BYWEEKNO=5;BYMONTH=2;COUNT=10", start: start, results:
165 | ["2018-05-17T09:00:00", "2019-02-01T09:00:00", "2019-02-02T09:00:00", "2019-02-03T09:00:00",
166 | "2020-02-01T09:00:00", "2020-02-02T09:00:00", "2021-02-01T09:00:00", "2021-02-02T09:00:00",
167 | "2021-02-03T09:00:00", "2021-02-04T09:00:00"]
168 | )
169 | }
170 |
171 | func testYearly13() {
172 | // Start 20180517T090000
173 | // Days from the 6th-to-last and 10th-to-last weeks of the year falling in October or December
174 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
175 | run(rule: "RRULE:FREQ=YEARLY;BYWEEKNO=-6,-10;BYMONTH=12,10;COUNT=10", start: start, results:
176 | ["2018-05-17T09:00:00", "2018-10-22T09:00:00", "2018-10-23T09:00:00", "2018-10-24T09:00:00",
177 | "2018-10-25T09:00:00", "2018-10-26T09:00:00", "2018-10-27T09:00:00", "2018-10-28T09:00:00",
178 | "2019-10-21T09:00:00", "2019-10-22T09:00:00"]
179 | )
180 | }
181 |
182 | func testYearly14() {
183 | // Start 20180517T090000
184 | // The 20th, 45th, and 160th day of each year falling in June (Jun 8 or 9 (depending on leap year))
185 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
186 | run(rule: "RRULE:FREQ=YEARLY;BYMONTH=6;BYYEARDAY=20,45,160;COUNT=5", start: start, results:
187 | ["2018-05-17T09:00:00", "2018-06-09T09:00:00", "2019-06-09T09:00:00", "2020-06-08T09:00:00",
188 | "2021-06-09T09:00:00"]
189 | )
190 | }
191 |
192 | func testYearly15() {
193 | // Start 20180517T090000
194 | // The 4th, 8th, and 12th of April, May, and June of the year
195 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
196 | run(rule: "RRULE:FREQ=YEARLY;BYMONTH=4,5,6;BYMONTHDAY=4,8,12;COUNT=10", start: start, results:
197 | ["2018-05-17T09:00:00", "2018-06-04T09:00:00", "2018-06-08T09:00:00", "2018-06-12T09:00:00",
198 | "2019-04-04T09:00:00", "2019-04-08T09:00:00", "2019-04-12T09:00:00", "2019-05-04T09:00:00",
199 | "2019-05-08T09:00:00", "2019-05-12T09:00:00"]
200 | )
201 | }
202 |
203 | func testYearly16() {
204 | // Start 20180517T090000
205 | // The last and 15th-to-last day of September and October
206 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
207 | run(rule: "RRULE:FREQ=YEARLY;BYMONTH=9,10;BYMONTHDAY=-1,-15;COUNT=10", start: start, results:
208 | ["2018-05-17T09:00:00", "2018-09-16T09:00:00", "2018-09-30T09:00:00", "2018-10-17T09:00:00",
209 | "2018-10-31T09:00:00", "2019-09-16T09:00:00", "2019-09-30T09:00:00", "2019-10-17T09:00:00",
210 | "2019-10-31T09:00:00", "2020-09-16T09:00:00"]
211 | )
212 | }
213 |
214 | func testYearly16a() {
215 | // Start 20180517T090000
216 | // Leap days
217 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
218 | run(rule: "RRULE:FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=29;COUNT=5", start: start, results:
219 | ["2018-05-17T09:00:00", "2020-02-29T09:00:00", "2024-02-29T09:00:00", "2028-02-29T09:00:00",
220 | "2032-02-29T09:00:00"]
221 | )
222 | }
223 |
224 | func testYearly17() {
225 | // Start 20180517T090000
226 | // Every Tuesday and Thursday in April and October
227 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
228 | run(rule: "RRULE:FREQ=YEARLY;BYMONTH=4,10;BYDAY=TU,TH;COUNT=15", start: start, results:
229 | ["2018-05-17T09:00:00", "2018-10-02T09:00:00", "2018-10-04T09:00:00", "2018-10-09T09:00:00",
230 | "2018-10-11T09:00:00", "2018-10-16T09:00:00", "2018-10-18T09:00:00", "2018-10-23T09:00:00",
231 | "2018-10-25T09:00:00", "2018-10-30T09:00:00", "2019-04-02T09:00:00", "2019-04-04T09:00:00",
232 | "2019-04-09T09:00:00", "2019-04-11T09:00:00", "2019-04-16T09:00:00"]
233 | )
234 | }
235 |
236 | func testYearly18() {
237 | // Start 20180517T090000
238 | // The first Tuesday and Thursday of May, July, and September
239 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
240 | run(rule: "RRULE:FREQ=YEARLY;BYMONTH=5,7,9;BYDAY=1TU,1TH;COUNT=15", start: start, results:
241 | ["2018-05-17T09:00:00", "2018-07-03T09:00:00", "2018-07-05T09:00:00", "2018-09-04T09:00:00",
242 | "2018-09-06T09:00:00", "2019-05-02T09:00:00", "2019-05-07T09:00:00", "2019-07-02T09:00:00",
243 | "2019-07-04T09:00:00", "2019-09-03T09:00:00", "2019-09-05T09:00:00", "2020-05-05T09:00:00",
244 | "2020-05-07T09:00:00", "2020-07-02T09:00:00", "2020-07-07T09:00:00"]
245 | )
246 | }
247 |
248 | func testYearly19() {
249 | // Start 20180517T090000
250 | // The 2nd Monday and 2nd-to-last Friday in January and December
251 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
252 | run(rule: "RRULE:FREQ=YEARLY;BYMONTH=1,12;BYDAY=2MO,-2FR;COUNT=15", start: start, results:
253 | ["2018-05-17T09:00:00", "2018-12-10T09:00:00", "2018-12-21T09:00:00", "2019-01-14T09:00:00",
254 | "2019-01-18T09:00:00", "2019-12-09T09:00:00", "2019-12-20T09:00:00", "2020-01-13T09:00:00",
255 | "2020-01-24T09:00:00", "2020-12-14T09:00:00", "2020-12-18T09:00:00", "2021-01-11T09:00:00",
256 | "2021-01-22T09:00:00", "2021-12-13T09:00:00", "2021-12-24T09:00:00"]
257 | )
258 | }
259 |
260 | func testYearly20() {
261 | // Start 20180517T090000
262 | // The 10th, 27th, and 40th days of the year if also in the 2nd, 4th, or 6th week of the year
263 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
264 | run(rule: "RRULE:FREQ=YEARLY;BYWEEKNO=2,4,6;BYYEARDAY=10,27,40;COUNT=15", start: start, results:
265 | ["2018-05-17T09:00:00", "2019-01-10T09:00:00", "2019-01-27T09:00:00", "2019-02-09T09:00:00",
266 | "2020-01-10T09:00:00", "2020-02-09T09:00:00", "2021-01-27T09:00:00", "2021-02-09T09:00:00",
267 | "2022-01-10T09:00:00", "2022-01-27T09:00:00", "2022-02-09T09:00:00", "2023-01-10T09:00:00",
268 | "2023-01-27T09:00:00", "2023-02-09T09:00:00", "2024-01-10T09:00:00"]
269 | )
270 | }
271 |
272 | func testYearly21() {
273 | // Start 20180517T090000
274 | // The 357 and 21st-to-last days of the year if also in the 2nd-to-last or 4th-to-last week of the year
275 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
276 | run(rule: "RRULE:FREQ=YEARLY;BYWEEKNO=-2,-4;BYYEARDAY=357,-21;COUNT=15", start: start, results:
277 | ["2018-05-17T09:00:00", "2018-12-23T09:00:00", "2020-12-11T09:00:00", "2020-12-22T09:00:00",
278 | "2021-12-11T09:00:00", "2021-12-23T09:00:00", "2022-12-11T09:00:00", "2022-12-23T09:00:00",
279 | "2023-12-23T09:00:00", "2024-12-22T09:00:00", "2026-12-11T09:00:00", "2026-12-23T09:00:00",
280 | "2027-12-11T09:00:00", "2027-12-23T09:00:00", "2028-12-22T09:00:00"]
281 | )
282 | }
283 |
284 | func testYearly22() {
285 | // Start 20180517T090000
286 | // The 3rd and 4th day of each month if in the 5th week of the year
287 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
288 | run(rule: "RRULE:FREQ=YEARLY;BYWEEKNO=5;BYMONTHDAY=3,4;COUNT=10", start: start, results:
289 | ["2018-05-17T09:00:00", "2019-02-03T09:00:00", "2021-02-03T09:00:00", "2021-02-04T09:00:00",
290 | "2022-02-03T09:00:00", "2022-02-04T09:00:00", "2023-02-03T09:00:00", "2023-02-04T09:00:00",
291 | "2024-02-03T09:00:00", "2024-02-04T09:00:00"]
292 | )
293 | }
294 |
295 | func testYearly23() {
296 | // Start 20180517T090000
297 | // The 29th of each month if in the last week of the year
298 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
299 | run(rule: "RRULE:FREQ=YEARLY;BYWEEKNO=-1;BYMONTHDAY=29;COUNT=10", start: start, results:
300 | ["2018-05-17T09:00:00", "2018-12-29T09:00:00", "2019-12-29T09:00:00", "2020-12-29T09:00:00",
301 | "2021-12-29T09:00:00", "2022-12-29T09:00:00", "2023-12-29T09:00:00", "2024-12-29T09:00:00",
302 | "2026-12-29T09:00:00", "2027-12-29T09:00:00"]
303 | )
304 | }
305 |
306 | func testYearly24() {
307 | // Start 20180517T090000
308 | // The Monday, Wednesday, and Friday in the 3rd week of the year
309 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
310 | run(rule: "RRULE:FREQ=YEARLY;BYWEEKNO=3;BYDAY=MO,WE,FR;COUNT=10", start: start, results :
311 | ["2018-05-17T09:00:00", "2019-01-14T09:00:00", "2019-01-16T09:00:00", "2019-01-18T09:00:00",
312 | "2020-01-13T09:00:00", "2020-01-15T09:00:00", "2020-01-17T09:00:00", "2021-01-18T09:00:00",
313 | "2021-01-20T09:00:00", "2021-01-22T09:00:00"]
314 | )
315 | }
316 |
317 | func testYearly25() {
318 | // Start 20180517T090000
319 | // The Monday, Wednesday, and Friday in the 3rd-to-last week of the year
320 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
321 | run(rule: "RRULE:FREQ=YEARLY;BYWEEKNO=-3;BYDAY=MO,WE,FR;COUNT=10", start: start, results:
322 | ["2018-05-17T09:00:00", "2018-12-10T09:00:00", "2018-12-12T09:00:00", "2018-12-14T09:00:00",
323 | "2019-12-09T09:00:00", "2019-12-11T09:00:00", "2019-12-13T09:00:00", "2020-12-14T09:00:00",
324 | "2020-12-16T09:00:00", "2020-12-18T09:00:00"]
325 | )
326 | }
327 |
328 | func testYearly26() {
329 | // Start 20180517T090000
330 | // The 160th day of each year (Jun 8 or 9 (depending on leap year)) that is also the 9th of the month
331 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
332 | run(rule: "RRULE:FREQ=YEARLY;BYYEARDAY=160;BYMONTHDAY=9;COUNT=10", start: start, results:
333 | ["2018-05-17T09:00:00", "2018-06-09T09:00:00", "2019-06-09T09:00:00", "2021-06-09T09:00:00",
334 | "2022-06-09T09:00:00", "2023-06-09T09:00:00", "2025-06-09T09:00:00", "2026-06-09T09:00:00",
335 | "2027-06-09T09:00:00", "2029-06-09T09:00:00"]
336 | )
337 | }
338 |
339 | func testYearly27() {
340 | // Start 20180517T090000
341 | // The 160th day of each year (Jun 8 or 9 (depending on leap year)) that is also on a Tuesday or Thursday
342 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
343 | run(rule: "RRULE:FREQ=YEARLY;BYYEARDAY=160;BYDAY=TU,TH;COUNT=5", start: start, results:
344 | ["2018-05-17T09:00:00", "2022-06-09T09:00:00", "2026-06-09T09:00:00", "2028-06-08T09:00:00",
345 | "2032-06-08T09:00:00"]
346 | )
347 | }
348 |
349 | func testYearly28() {
350 | // Start 20180517T090000
351 | // The 160th day of each year (Jun 8 or 9 (depending on leap year)) that is also on a Tuesday or Thursday
352 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
353 | run(rule: "RRULE:FREQ=YEARLY;BYMONTHDAY=5,10,15;BYDAY=TU,TH;COUNT=15", start: start, results:
354 | ["2018-05-17T09:00:00", "2018-06-05T09:00:00", "2018-07-05T09:00:00", "2018-07-10T09:00:00",
355 | "2018-11-15T09:00:00", "2019-01-10T09:00:00", "2019-01-15T09:00:00", "2019-02-05T09:00:00",
356 | "2019-03-05T09:00:00", "2019-08-15T09:00:00", "2019-09-05T09:00:00", "2019-09-10T09:00:00",
357 | "2019-10-10T09:00:00", "2019-10-15T09:00:00", "2019-11-05T09:00:00"]
358 | )
359 | }
360 |
361 | func testYearly29() {
362 | // Start 20180517T090000
363 | // The 160th day of each year (Jun 8 or 9 (depending on leap year)) that is also on a Tuesday or Thursday
364 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
365 | run(rule: "RRULE:FREQ=YEARLY;BYMONTHDAY=-1,-2;BYDAY=SA,SU;COUNT=15", start: start, results:
366 | ["2018-05-17T09:00:00", "2018-06-30T09:00:00", "2018-09-29T09:00:00", "2018-09-30T09:00:00",
367 | "2018-12-30T09:00:00", "2019-03-30T09:00:00", "2019-03-31T09:00:00", "2019-06-29T09:00:00",
368 | "2019-06-30T09:00:00", "2019-08-31T09:00:00", "2019-09-29T09:00:00", "2019-11-30T09:00:00",
369 | "2020-02-29T09:00:00", "2020-05-30T09:00:00", "2020-05-31T09:00:00"]
370 | )
371 | }
372 |
373 | func testYearly30() {
374 | // Start 20180517T090000
375 | // The 160th day of each year (Jun 8 or 9 (depending on leap year)) that is also on a Tuesday or Thursday
376 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
377 | run(rule: "RRULE:FREQ=YEARLY;BYMONTHDAY=1,-1;BYDAY=1SA,1SU,-1SA,-1SU;COUNT=15", start: start, results:
378 | ["2018-05-17T09:00:00", "2018-06-30T09:00:00", "2018-07-01T09:00:00", "2018-09-01T09:00:00",
379 | "2018-09-30T09:00:00", "2018-12-01T09:00:00", "2019-03-31T09:00:00", "2019-06-01T09:00:00",
380 | "2019-06-30T09:00:00", "2019-08-31T09:00:00", "2019-09-01T09:00:00", "2019-11-30T09:00:00",
381 | "2019-12-01T09:00:00", "2020-02-01T09:00:00", "2020-02-29T09:00:00"]
382 | )
383 | }
384 |
385 | func testYearly31() {
386 | // Start 20180517T090000
387 | // Days from the 5th week of the year falling in February that are also either the 33rd or 34th day of the year
388 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
389 | run(rule: "RRULE:FREQ=YEARLY;BYWEEKNO=5;BYMONTH=2;BYYEARDAY=33,34;COUNT=10", start: start, results:
390 | ["2018-05-17T09:00:00", "2019-02-02T09:00:00", "2019-02-03T09:00:00", "2020-02-02T09:00:00",
391 | "2021-02-02T09:00:00", "2021-02-03T09:00:00", "2022-02-02T09:00:00", "2022-02-03T09:00:00",
392 | "2023-02-02T09:00:00", "2023-02-03T09:00:00"]
393 | )
394 | }
395 |
396 | func testYearly32() {
397 | // Start 20180517T090000
398 | // Days from the 6th-to-last and 10th-to-last weeks of the year falling in October or December that are also eith the 297th or 70th-to-last day of the year
399 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
400 | run(rule: "RRULE:FREQ=YEARLY;BYWEEKNO=-6,-10;BYMONTH=12,10;BYYEARDAY=297,-70;COUNT=10", start: start, results:
401 | ["2018-05-17T09:00:00", "2018-10-23T09:00:00", "2018-10-24T09:00:00", "2019-10-23T09:00:00",
402 | "2019-10-24T09:00:00", "2022-10-24T09:00:00", "2023-10-23T09:00:00", "2023-10-24T09:00:00",
403 | "2024-10-23T09:00:00", "2025-10-23T09:00:00"]
404 | )
405 | }
406 |
407 | func testYearly33() {
408 | // Start 20180517T090000
409 | // The 2nd, 4th, 6th, 8th, and 10th of February and March that fall on the 5th or 10th week of the year
410 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
411 | run(rule: "RRULE:FREQ=YEARLY;BYMONTH=2,3;BYWEEKNO=5,10;BYMONTHDAY=2,4,6,8,10;COUNT=10", start: start, results:
412 | ["2018-05-17T09:00:00", "2019-02-02T09:00:00", "2019-03-04T09:00:00", "2019-03-06T09:00:00",
413 | "2019-03-08T09:00:00", "2019-03-10T09:00:00", "2020-02-02T09:00:00", "2020-03-02T09:00:00",
414 | "2020-03-04T09:00:00", "2020-03-06T09:00:00"]
415 | )
416 | }
417 |
418 | func testYearly34() {
419 | // Start 20180517T090000
420 | // The 2nd, 4th, 6th, and 8th of February and March that fall on a Wednesday or Saturday
421 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
422 | run(rule: "RRULE:FREQ=YEARLY;BYMONTH=2,3;BYDAY=WE,SA;BYMONTHDAY=2,4,6,8;COUNT=10", start: start, results:
423 | ["2018-05-17T09:00:00", "2019-02-02T09:00:00", "2019-02-06T09:00:00", "2019-03-02T09:00:00",
424 | "2019-03-06T09:00:00", "2020-02-08T09:00:00", "2020-03-04T09:00:00", "2021-02-06T09:00:00",
425 | "2021-03-06T09:00:00", "2022-02-02T09:00:00"]
426 | )
427 | }
428 |
429 | func testYearly35() {
430 | // Start 20180517T090000
431 | // The 3rd and 31st of a month that is also The 62nd or last day of the year falling in week number 9 or 52
432 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
433 | run(rule: "RRULE:FREQ=YEARLY;BYWEEKNO=9,52;BYYEARDAY=62,-1;BYMONTHDAY=3,31;COUNT=10", start: start, results:
434 | ["2018-05-17T09:00:00", "2019-03-03T09:00:00", "2021-03-03T09:00:00", "2021-12-31T09:00:00",
435 | "2022-03-03T09:00:00", "2022-12-31T09:00:00", "2023-03-03T09:00:00", "2023-12-31T09:00:00",
436 | "2027-03-03T09:00:00", "2027-12-31T09:00:00"]
437 | )
438 | }
439 |
440 | func testYearly36() {
441 | // Start 20180517T090000
442 | // The 62nd or last day of the year falling in the 9th or 52nd week of the year and also falling on a Monday, Wednesday, or Friday
443 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
444 | run(rule: "RRULE:FREQ=YEARLY;BYWEEKNO=9,52;BYYEARDAY=62,-1;BYDAY=MO,WE,FR;COUNT=5", start: start, results:
445 | ["2018-05-17T09:00:00", "2021-03-03T09:00:00", "2021-12-31T09:00:00", "2023-03-03T09:00:00",
446 | "2027-03-03T09:00:00"]
447 | )
448 | }
449 |
450 | func testYearly37() {
451 | // Start 20180517T090000
452 | // The 3rd and 31st of a month that is also The 62nd or last day of the year falling on a Monday, Tuesday, Wednesday, or Thursday
453 | let start = calendar.date(from: DateComponents(year: 2018, month: 5, day: 17, hour: 9))!
454 | run(rule: "RRULE:FREQ=YEARLY;BYYEARDAY=62,-1;BYMONTHDAY=3,31;BYDAY=MO,TU,WE,TH;COUNT=10", start: start, results:
455 | ["2018-05-17T09:00:00", "2018-12-31T09:00:00", "2019-12-31T09:00:00", "2020-12-31T09:00:00",
456 | "2021-03-03T09:00:00", "2022-03-03T09:00:00", "2024-12-31T09:00:00", "2025-03-03T09:00:00",
457 | "2025-12-31T09:00:00", "2026-03-03T09:00:00"]
458 | )
459 | }
460 |
461 | // TODO - there should be tests with the 5 combinations of 4 "BY" clauses and 1 with all 5 (not counting BYSETPOS)
462 |
463 | // With 4
464 | // BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY
465 | // BYMONTH, BYYEARDAY, BYMONTHDAY, BYDAY
466 | // BYMONTH, BYWEEKNO, BYMONTHDAY, BYDAY
467 | // BYMONTH, BYWEEKNO, BYYEARDAY, BYDAY
468 | // BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY
469 |
470 | // All 5
471 | // BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY
472 |
473 | // TODO - need lots of test using BYSETPOS with lots of various combinations of the other 5 "BY" clauses
474 | }
475 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule_iOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSPrincipalClass
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule_iOSTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule_macOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSPrincipalClass
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule_macOSTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/RWMRecurrenceRule_watchOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSPrincipalClass
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------