├── .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 | ![Installation Screen Shot](Documentation/Resources/installation.png) 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 | --------------------------------------------------------------------------------