├── .codecov.yml ├── .gitignore ├── .swiftlint.yml ├── .travis.yml ├── LICENSE ├── Package.swift ├── README.md ├── README.zh_cn.md ├── Schedule.playground ├── Contents.swift └── contents.xcplayground ├── Schedule.podspec ├── Schedule.xcodeproj ├── ScheduleTests_Info.plist ├── Schedule_Info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ └── Schedule-Package.xcscheme ├── Sources └── Schedule │ ├── Atomic.swift │ ├── Bag.swift │ ├── Extensions.swift │ ├── Interval.swift │ ├── Monthday.swift │ ├── Period.swift │ ├── Plan.swift │ ├── RunLoopTask.swift │ ├── Task.swift │ ├── TaskCenter.swift │ ├── Time.swift │ └── Weekday.swift ├── Tests ├── LinuxMain.swift └── ScheduleTests │ ├── AtomicTests.swift │ ├── BagTests.swift │ ├── ExtensionsTests.swift │ ├── Helpers.swift │ ├── IntervalTests.swift │ ├── MonthdayTests.swift │ ├── PeriodTests.swift │ ├── PlanTests.swift │ ├── TaskCenterTests.swift │ ├── TaskTests.swift │ ├── TimeTests.swift │ ├── WeekdayTests.swift │ └── XCTestManifests.swift └── assets └── demo.png /.codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "Tests/" 3 | - "Schedule.playground" 4 | 5 | comment: 6 | layout: header, changes, diff 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Build generated 2 | build/ 3 | DerivedData/ 4 | 5 | ## Various settings 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | 16 | ## Other 17 | *.moved-aside 18 | *.xccheckout 19 | *.xcscmblueprint 20 | 21 | ## Obj-C/Swift specific 22 | *.hmap 23 | *.ipa 24 | *.dSYM.zip 25 | *.dSYM 26 | 27 | ## Playgrounds 28 | timeline.xctimeline 29 | playground.xcworkspace 30 | 31 | # Swift Package Manager 32 | # 33 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 34 | # Packages/ 35 | # Package.pins 36 | # Package.resolved 37 | .build/ 38 | 39 | # CocoaPods 40 | # 41 | # We recommend against adding the Pods directory to your .gitignore. However 42 | # you should judge for yourself, the pros and cons are mentioned at: 43 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 44 | # 45 | # Pods/ 46 | # 47 | # Add this line if you want to avoid checking in source code from the Xcode workspace 48 | # *.xcworkspace 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots/**/*.png 67 | fastlane/test_output -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | - Tests 4 | 5 | disabled_rules: 6 | - cyclomatic_complexity 7 | - file_length 8 | - function_body_length 9 | - identifier_name 10 | - type_name -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | only: 3 | - master 4 | 5 | language: objective-c 6 | osx_image: xcode10.2 7 | 8 | env: 9 | global: 10 | - PROJECT="Schedule.xcodeproj" 11 | - SCHEME="Schedule-Package" 12 | 13 | matrix: 14 | include: 15 | - os: osx 16 | env: 17 | - SDK="iphonesimulator12.2" 18 | - DESTINATION="platform=iOS Simulator,name=iPhone 8,OS=12.2" 19 | - os: osx 20 | env: 21 | - SDK="macosx10.14" 22 | - DESTINATION="arch=x86_64" 23 | - os: osx 24 | env: 25 | - SDK="appletvsimulator12.0" 26 | - DESTINATION="OS=12.0,name=Apple TV 4K" 27 | - os: linux 28 | sudo: required 29 | dist: trusty 30 | 31 | before_install: 32 | - if [[ $TRAVIS_OS_NAME == 'osx' ]]; then 33 | gem install xcpretty; 34 | fi 35 | - if [[ $TRAVIS_OS_NAME == 'linux' ]]; then 36 | eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"; 37 | fi 38 | 39 | script: 40 | - if [[ $TRAVIS_OS_NAME == 'osx' ]]; then 41 | xcodebuild clean build test -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -enableCodeCoverage YES | xcpretty; 42 | fi 43 | - if [[ $TRAVIS_OS_NAME == 'linux' ]]; then 44 | swift test; 45 | fi 46 | 47 | after_success: 48 | if [[ $TRAVIS_OS_NAME == 'osx' ]]; then 49 | bash <(curl -s https://codecov.io/bash) -J 'Schedule'; 50 | fi -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Quentin Jin 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 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Schedule", 7 | platforms: [ 8 | .macOS(.v10_11), 9 | .iOS(.v9), 10 | .tvOS(.v9), 11 | .watchOS(.v2) 12 | ], 13 | products: [ 14 | .library(name: "Schedule", targets: ["Schedule"]) 15 | ], 16 | targets: [ 17 | .target(name: "Schedule"), 18 | .testTarget(name: "ScheduleTests", dependencies: ["Schedule"]) 19 | ], 20 | swiftLanguageVersions: [ 21 | .v5 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Schedule([简体中文](README.zh_cn.md)) 2 | 3 |

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

13 | 14 | Schedule is a timing tasks scheduler written in Swift. It allows you run timing tasks with elegant and intuitive syntax. 15 | 16 |

17 | 18 |

19 | 20 | ## Features 21 | 22 | - [x] Elegant and intuitive API 23 | - [x] Rich preset rules 24 | - [x] Powerful management mechanism 25 | - [x] Detailed execution history 26 | - [x] Thread safe 27 | - [x] Complete documentation 28 | - [x] ~100%+ test coverage 29 | 30 | ### Why You Should Use Schedule 31 | 32 | | Features | Timer | DispatchSourceTimer | Schedule | 33 | | --- | :---: | :---: | :---: | 34 | | ⏰ Interval-based Schedule | ✓ | ✓ | ✓ | 35 | | 📆 Date-based Schedule | ✓ | | ✓ | 36 | | 🌈 Combined Plan Schedule | | | ✓ | 37 | | 🗣️ Natural Language Parse | | | ✓ | 38 | | 🏷 Batch Task Management | | | ✓ | 39 | | 📝 Execution Record | | | ✓ | 40 | | 🎡 Plan Reset | | ✓ | ✓ | 41 | | 🚦 Suspend, Resume, Cancel | | ✓ | ✓ | 42 | | 🍰 Child-action | | | ✓ | 43 | 44 | ## Usage 45 | 46 | ### Overview 47 | 48 | Scheduling a task has never been so elegant and intuitive, all you have to do is: 49 | 50 | ```swift 51 | // 1. define your plan: 52 | let plan = Plan.after(3.seconds) 53 | 54 | // 2. do your task: 55 | let task = plan.do { 56 | print("3 seconds passed!") 57 | } 58 | ``` 59 | 60 | ### Rules 61 | 62 | #### Interval-based Schedule 63 | 64 | The running mechanism of Schedule is based on `Plan`, and `Plan` is actually a sequence of `Interval`. 65 | 66 | Schedule makes `Plan` definitions more elegant and intuitive by extending `Int` and `Double`. Also, because `Interval` is a built-in type of Schedule, you don't have to worry about it being polluting your namespace. 67 | 68 | ```swift 69 | let t1 = Plan.every(1.second).do { } 70 | 71 | let t2 = Plan.after(1.hour, repeating: 1.minute).do { } 72 | 73 | let t3 = Plan.of(1.second, 2.minutes, 3.hours).do { } 74 | ``` 75 | 76 | #### Date-based Schedule 77 | 78 | Configuring date-based `Plan` is the same, with the expressive Swift syntax, Schedule makes your code look like a fluent conversation. 79 | 80 | ```swift 81 | let t1 = Plan.at(date).do { } 82 | 83 | let t2 = Plan.every(.monday, .tuesday).at("9:00:00").do { } 84 | 85 | let t3 = Plan.every(.september(30)).at(10, 30).do { } 86 | 87 | let t4 = Plan.every("one month and ten days").do { } 88 | 89 | let t5 = Plan.of(date0, date1, date2).do { } 90 | ``` 91 | 92 | #### Natural Language Parse 93 | 94 | In addition, Schedule also supports simple natural language parsing. 95 | 96 | ```swift 97 | let t1 = Plan.every("one hour and ten minutes").do { } 98 | 99 | let t2 = Plan.every("1 hour, 5 minutes and 10 seconds").do { } 100 | 101 | let t3 = Plan.every(.friday).at("9:00 pm").do { } 102 | 103 | Period.registerQuantifier("many", for: 100 * 1000) 104 | let t4 = Plan.every("many days").do { } 105 | ``` 106 | 107 | #### Combined Plan Schedule 108 | 109 | Schedule provides several basic collection operators, which means you can use them to customize your own powerful plans. 110 | 111 | ```swift 112 | /// Concat 113 | let p0 = Plan.at(birthdate) 114 | let p1 = Plan.every(1.year) 115 | let birthday = p0.concat.p1 116 | let t1 = birthday.do { 117 | print("Happy birthday") 118 | } 119 | 120 | /// Merge 121 | let p3 = Plan.every(.january(1)).at("8:00") 122 | let p4 = Plan.every(.october(1)).at("9:00 AM") 123 | let holiday = p3.merge(p4) 124 | let t2 = holiday.do { 125 | print("Happy holiday") 126 | } 127 | 128 | /// First 129 | let p5 = Plan.after(5.seconds).concat(Schedule.every(1.day)) 130 | let p6 = s5.first(10) 131 | 132 | /// Until 133 | let p7 = P.every(.monday).at(11, 12) 134 | let p8 = p7.until(date) 135 | ``` 136 | 137 | ### Management 138 | 139 | #### DispatchQueue 140 | 141 | When calling `plan.do` to dispatch a timing task, you can use `queue` to specify which `DispatchQueue` the task will be dispatched to when the time is up. This operation does not rely on `RunLoop` like `Timer`, so you can call it on any thread. 142 | 143 | ```swift 144 | Plan.every(1.second).do(queue: .global()) { 145 | print("On a globle queue") 146 | } 147 | ``` 148 | 149 | 150 | #### RunLoop 151 | 152 | If `queue` is not specified, Schedule will use `RunLoop` to dispatch the task, at which point the task will execute on the current thread. **Please note**, like `Timer`, which is also based on `RunLoop`, you need to ensure that the current thread has an **available** `RunLoop`. By default, the task will be added to `.common` mode, you can specify another mode when creating the task. 153 | 154 | ```swift 155 | let task = Plan.every(1.second).do(mode: .default) { 156 | print("on default mode...") 157 | } 158 | ``` 159 | 160 | #### Timeline 161 | 162 | You can observe the execution record of the task in real time using the following properties. 163 | 164 | ```swift 165 | task.creationDate 166 | 167 | task.executionHistory 168 | 169 | task.firstExecutionDate 170 | task.lastExecutionDate 171 | 172 | task.estimatedNextExecutionDate 173 | ``` 174 | 175 | #### TaskCenter & Tag 176 | 177 | Tasks are automatically added to `TaskCenter.default` by default,you can organize them using tags and task center. 178 | 179 | ```swift 180 | let plan = Plan.every(1.day) 181 | let task0 = plan.do(queue: myTaskQueue) { } 182 | let task1 = plan.do(queue: myTaskQueue) { } 183 | 184 | TaskCenter.default.addTags(["database", "log"], to: task1) 185 | TaskCenter.default.removeTag("log", from: task1) 186 | 187 | TaskCenter.default.suspend(byTag: "log") 188 | TaskCenter.default.resume(byTag: "log") 189 | TaskCenter.default.cancel(byTag: "log") 190 | 191 | TaskCenter.default.clear() 192 | 193 | let myCenter = TaskCenter() 194 | myCenter.add(task0) 195 | ``` 196 | 197 | 198 | ### Suspend,Resume, Cancel 199 | 200 | You can `suspend`, `resume`, `cancel` a task. 201 | 202 | ```swift 203 | let task = Plan.every(1.minute).do { } 204 | 205 | // will increase task's suspensionCount 206 | task.suspend() 207 | 208 | // will decrease task's suspensionCount, 209 | // but don't worry about excessive resumptions, I will handle these for you~ 210 | task.resume() 211 | 212 | // will clear task's suspensionCount 213 | // a canceled task can't do anything, event if it is set to a new plan. 214 | task.cancel() 215 | ``` 216 | 217 | #### Action 218 | 219 | You can add more actions to a task and remove them at any time you want: 220 | 221 | ```swift 222 | let dailyTask = Plan.every(1.day) 223 | dailyTask.addAction { 224 | print("open eyes") 225 | } 226 | dailyTask.addAction { 227 | print("get up") 228 | } 229 | let key = dailyTask.addAction { 230 | print("take a shower") 231 | } 232 | dailyTask.removeAction(byKey: key) 233 | ``` 234 | 235 | ## Installation 236 | 237 | ### CocoaPods 238 | 239 | ```ruby 240 | # Podfile 241 | use_frameworks! 242 | 243 | target 'YOUR_TARGET_NAME' do 244 | pod 'Schedule', '~> 2.0' 245 | end 246 | ``` 247 | 248 | ### Carthage 249 | 250 | ``` 251 | github "luoxiu/Schedule" ~> 2.0 252 | ``` 253 | 254 | ### Swift Package Manager 255 | 256 | ```swift 257 | dependencies: [ 258 | .package( 259 | url: "https://github.com/luoxiu/Schedule", .upToNextMajor(from: "2.0.0") 260 | ) 261 | ] 262 | ``` 263 | 264 | ## Contributing 265 | 266 | Like **Schedule**? Thanks!!! 267 | 268 | At the same time, I need your help~ 269 | 270 | ### Finding Bugs 271 | 272 | Schedule is just getting started. If you could help the Schedule find or fix potential bugs, I would be grateful! 273 | 274 | ### New Features 275 | 276 | Have some awesome ideas? Feel free to open an issue or submit your pull request directly! 277 | 278 | ### Documentation improvements. 279 | 280 | Improvements to README and documentation are welcome at all times, whether typos or my lame English, 🤣. 281 | 282 | ## Acknowledgement 283 | 284 | Inspired by Dan Bader's [schedule](https://github.com/dbader/schedule)! 285 | -------------------------------------------------------------------------------- /README.zh_cn.md: -------------------------------------------------------------------------------- 1 | # Schedule 2 | 3 |

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

13 | 14 | Schedule 是一个用 Swift 编写的定时任务调度器,它能让你用优雅、直观的语法执行定时任务。 15 | 16 |

17 | 18 |

19 | 20 | ## 功能 21 | 22 | - [x] 优雅,直观的 API 23 | - [x] 丰富的预置规则 24 | - [x] 强大的管理机制 25 | - [x] 细致的执行记录 26 | - [x] 线程安全 27 | - [x] 完整的文档 28 | - [x] ~100% 的测试覆盖 29 | 30 | ### 为什么你该使用 Schedule,而不是…… 31 | 32 | | 功能 | Timer | DispatchSourceTimer | Schedule | 33 | | --- | :---: | :---: | :---: | 34 | | ⏰ 基于时间间隔调度 | ✓ | ✓ | ✓ | 35 | | 📆 基于日期调度 | ✓ | | ✓ | 36 | | 🌈 组合计划调度 | | | ✓ | 37 | | 🗣️ 自然语言解析 | | | ✓ | 38 | | 🏷 批任务管理 | | | ✓ | 39 | | 📝 执行记录 | | | ✓ | 40 | | 🎡 规则重置 | | ✓ | ✓ | 41 | | 🚦 暂停、继续、取消 | | ✓ | ✓ | 42 | | 🍰 子动作 | | | ✓ | 43 | 44 | ## 用法 45 | 46 | ### 一瞥 47 | 48 | 调度一个定时任务从未如此优雅、直观,你只需要: 49 | 50 | ```swift 51 | // 1. 定义你的计划: 52 | let plan = Plan.after(3.seconds) 53 | 54 | // 2. 执行你的任务: 55 | let task = plan.do { 56 | print("3 seconds passed!") 57 | } 58 | ``` 59 | 60 | ### 计划 61 | 62 | #### 基于时间间隔调度 63 | 64 | Schedule 的机制基于 `Plan`,而 `Plan` 的本质是一系列 `Interval`。 65 | 66 | Schedule 通过扩展 `Int` 和 `Double` 让 `Plan` 的定义更加优雅、直观。同时,因为 `Interval` 是 Schedule 的内置类型,所以你不用担心这会对你的命名空间产生污染。 67 | 68 | ```swift 69 | let t1 = Plan.every(1.second).do { } 70 | 71 | let t2 = Plan.after(1.hour, repeating: 1.minute).do { } 72 | 73 | let t3 = Plan.of(1.second, 2.minutes, 3.hours).do { } 74 | ``` 75 | 76 | #### 基于日期调度 77 | 78 | 定制基于日期的 `Plan` 同样如此,配合富有表现力的 Swift 语法,Schedule 让你的代码看起来就像一场流畅的对话。 79 | 80 | ```swift 81 | let t1 = Plan.at(date).do { } 82 | 83 | let t2 = Plan.every(.monday, .tuesday).at("9:00:00").do { } 84 | 85 | let t3 = Plan.every(.september(30)).at(10, 30).do { } 86 | 87 | let t4 = Plan.every("one month and ten days").do { } 88 | 89 | let t5 = Plan.of(date0, date1, date2).do { } 90 | ``` 91 | 92 | #### 自然语言解析 93 | 94 | 除此之外,Schedule 还支持简单的自然语言解析。 95 | 96 | ```swift 97 | let t1 = Plan.every("one hour and ten minutes").do { } 98 | 99 | let t2 = Plan.every("1 hour, 5 minutes and 10 seconds").do { } 100 | 101 | let t3 = Plan.every(.friday).at("9:00 pm").do { } 102 | 103 | Period.registerQuantifier("many", for: 100 * 1000) 104 | let t4 = Plan.every("many days").do { } 105 | ``` 106 | 107 | #### 组合计划调度 108 | 109 | Schedule 提供了几个基本的集合操作符,这意味着,你可以使用它们自由组合,定制属于你的强大规则。 110 | 111 | ```swift 112 | /// Concat 113 | let p0 = Plan.at(birthdate) 114 | let p1 = Plan.every(1.year) 115 | let birthday = p0.concat.p1 116 | let t1 = birthday.do { 117 | print("Happy birthday") 118 | } 119 | 120 | /// Merge 121 | let p3 = Plan.every(.january(1)).at("8:00") 122 | let p4 = Plan.every(.october(1)).at("9:00 AM") 123 | let holiday = p3.merge(p4) 124 | let t2 = holiday.do { 125 | print("Happy holiday") 126 | } 127 | 128 | /// First 129 | let p5 = Plan.after(5.seconds).concat(Schedule.every(1.day)) 130 | let p6 = s5.first(10) 131 | 132 | /// Until 133 | let p7 = P.every(.monday).at(11, 12) 134 | let p8 = p7.until(date) 135 | ``` 136 | 137 | ### 管理 138 | 139 | #### DispatchQueue 140 | 141 | 调用 `plan.do` 来调度定时任务时,你可以使用 `queue` 来指定当时间到时,task 会被派发到哪个 `DispatchQueue` 上。这个操作不像 `Timer` 那样依赖 `RunLoop`,所以你可以在任意线程上使用它。 142 | 143 | ```swift 144 | let task = Plan.every(1.second).do(queue: .global()) { 145 | print("On a globle queue") 146 | } 147 | ``` 148 | 149 | #### RunLoop 150 | 151 | 如果没有指定 `queue`,Schedule 会使用 `RunLoop` 来调度 task,这时,task 会在当前线程上执行。**要注意**,和同样基于 `RunLoop` 的 `Timer` 一样,你需要保证当前线程有一个**可用**的 `RunLoop`。默认情况下, task 会被添加到 `.common` mode 上,你可以在创建 task 时指定其它 mode。 152 | 153 | ```swift 154 | let task = Plan.every(1.second).do(mode: .default) { 155 | print("on default mode...") 156 | } 157 | ``` 158 | 159 | #### Timeline 160 | 161 | 你可以使用以下属性实时地观察 task 的执行记录。 162 | 163 | ```swift 164 | task.creationDate 165 | 166 | task.executionHistory 167 | 168 | task.firstExecutionDate 169 | task.lastExecutionDate 170 | 171 | task.estimatedNextExecutionDate 172 | ``` 173 | 174 | #### TaskCenter 和 Tag 175 | 176 | task 默认会被自动添加到 `TaskCenter.default` 上,你可以使用 tag 配合 taskCenter 来组织 tasks: 177 | 178 | ```swift 179 | let plan = Plan.every(1.day) 180 | let task0 = plan.do(queue: myTaskQueue) { } 181 | let task1 = plan.do(queue: myTaskQueue) { } 182 | 183 | TaskCenter.default.addTags(["database", "log"], to: task1) 184 | TaskCenter.default.removeTag("log", from: task1) 185 | 186 | TaskCenter.default.suspend(byTag: "log") 187 | TaskCenter.default.resume(byTag: "log") 188 | TaskCenter.default.cancel(byTag: "log") 189 | 190 | TaskCenter.default.removeAll() 191 | 192 | let myCenter = TaskCenter() 193 | myCenter.add(task0) 194 | ``` 195 | 196 | ### Suspend、Resume、Cancel 197 | 198 | 你可以 suspend,resume,cancel 一个 task。 199 | 200 | ```swift 201 | let task = Plan.every(1.minute).do { } 202 | 203 | // 会增加 task 的暂停计数 204 | task.suspend() 205 | 206 | task.suspensions == 1 207 | 208 | // 会减少 task 的暂停计数 209 | // 不过不用担心过度减少,我会帮你处理好这些~ 210 | task.resume() 211 | 212 | task.suspensions == 0 213 | 214 | // 会清零 task 的暂停计数 215 | // 被 cancel 的 task 即使重新设置其它调度规则也不会有任何作用了 216 | task.cancel() 217 | ``` 218 | 219 | #### 子动作 220 | 221 | 你可以添加更多的 action 到 task,并在任意时刻移除它们。 222 | 223 | ```swift 224 | let dailyTask = Plan.every(1.day) 225 | dailyTask.addAction { 226 | print("open eyes") 227 | } 228 | dailyTask.addAction { 229 | print("get up") 230 | } 231 | let key = dailyTask.addAction { 232 | print("take a shower") 233 | } 234 | dailyTask.removeAction(byKey: key) 235 | ``` 236 | 237 | ## 安装 238 | 239 | ### CocoaPods 240 | 241 | ```ruby 242 | # Podfile 243 | use_frameworks! 244 | 245 | target 'YOUR_TARGET_NAME' do 246 | pod 'Schedule', '~> 2.0' 247 | end 248 | ``` 249 | 250 | ### Carthage 251 | 252 | ``` 253 | # Cartfile 254 | github "luoxiu/Schedule" ~> 2.0 255 | ``` 256 | 257 | ### Swift Package Manager 258 | 259 | ```swift 260 | dependencies: [ 261 | .package( 262 | url: "https://github.com/luoxiu/Schedule", .upToNextMajor(from: "2.0.0") 263 | ) 264 | ] 265 | ``` 266 | 267 | ## 贡献 268 | 269 | 喜欢 **Schedule** 吗?谢谢!!! 270 | 271 | 与此同时如果你想参与进来的话,你可以: 272 | 273 | ### 找 Bugs 274 | 275 | Schedule 还是一个非常年轻的项目,如果你能帮 Schedule 找到甚至解决潜在的 bugs 的话,那就太感谢啦! 276 | 277 | ### 新功能 278 | 279 | 有一些有趣的想法?尽管在 issue 里分享出来,或者直接提交你的 Pull Request! 280 | 281 | ### 改善文档 282 | 283 | 任何时候都欢迎对 README 或者文档注释的修改建议,无论是错别字还是纠正我的蹩脚英文,🤣。 284 | 285 | ## 致谢 286 | 287 | 项目灵感来自 Dan Bader 的 [schedule](https://github.com/dbader/schedule)! 288 | -------------------------------------------------------------------------------- /Schedule.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import PlaygroundSupport 2 | import Schedule 3 | 4 | PlaygroundPage.current.needsIndefiniteExecution = true 5 | 6 | let t1 = Plan.after(1.second).do { 7 | print("1 second passed!") 8 | } 9 | 10 | let t2 = Plan.after(1.minute, repeating: 0.5.seconds).do { 11 | print("Ping!") 12 | } 13 | 14 | let t3 = Plan.every("one minute and ten seconds").do { 15 | print("One minute and ten seconds have elapsed!") 16 | } 17 | 18 | let t4 = Plan.every(.monday, .tuesday, .wednesday, .thursday, .friday).at(6, 30).do { 19 | print("Get up!") 20 | } 21 | 22 | let t5 = Plan.every(.june(14)).at("9:30").do { 23 | print("Happy birthday!") 24 | } 25 | -------------------------------------------------------------------------------- /Schedule.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Schedule.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Schedule" 3 | s.version = "2.1.1" 4 | s.license = { :type => "MIT" } 5 | s.homepage = "https://github.com/luoxiu/Schedule" 6 | s.author = { "Quentin Jin" => "luoxiustm@gmail.com" } 7 | s.summary = "Schedule timing task in Swift using a fluent API" 8 | 9 | s.source = { :git => "https://github.com/luoxiu/Schedule.git", :tag => "#{s.version}" } 10 | s.source_files = "Sources/Schedule/*.swift" 11 | 12 | s.swift_version = "5.0" 13 | 14 | s.ios.deployment_target = "9.0" 15 | s.osx.deployment_target = "10.11" 16 | s.tvos.deployment_target = "9.0" 17 | s.watchos.deployment_target = "2.0" 18 | end 19 | -------------------------------------------------------------------------------- /Schedule.xcodeproj/ScheduleTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Schedule.xcodeproj/Schedule_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 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 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Schedule.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXAggregateTarget section */ 10 | "Schedule::SchedulePackageTests::ProductTarget" /* SchedulePackageTests */ = { 11 | isa = PBXAggregateTarget; 12 | buildConfigurationList = OBJ_69 /* Build configuration list for PBXAggregateTarget "SchedulePackageTests" */; 13 | buildPhases = ( 14 | ); 15 | dependencies = ( 16 | OBJ_72 /* PBXTargetDependency */, 17 | ); 18 | name = SchedulePackageTests; 19 | productName = SchedulePackageTests; 20 | }; 21 | /* End PBXAggregateTarget section */ 22 | 23 | /* Begin PBXBuildFile section */ 24 | OBJ_49 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Atomic.swift */; }; 25 | OBJ_50 /* Bag.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* Bag.swift */; }; 26 | OBJ_51 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* Extensions.swift */; }; 27 | OBJ_52 /* Interval.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* Interval.swift */; }; 28 | OBJ_53 /* Monthday.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* Monthday.swift */; }; 29 | OBJ_54 /* Period.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_14 /* Period.swift */; }; 30 | OBJ_55 /* Plan.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* Plan.swift */; }; 31 | OBJ_56 /* RunLoopTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_16 /* RunLoopTask.swift */; }; 32 | OBJ_57 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* Task.swift */; }; 33 | OBJ_58 /* TaskCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_18 /* TaskCenter.swift */; }; 34 | OBJ_59 /* Time.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_19 /* Time.swift */; }; 35 | OBJ_60 /* Weekday.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_20 /* Weekday.swift */; }; 36 | OBJ_67 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; }; 37 | OBJ_78 /* AtomicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_23 /* AtomicTests.swift */; }; 38 | OBJ_79 /* BagTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_24 /* BagTests.swift */; }; 39 | OBJ_80 /* ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_25 /* ExtensionsTests.swift */; }; 40 | OBJ_81 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_26 /* Helpers.swift */; }; 41 | OBJ_82 /* IntervalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_27 /* IntervalTests.swift */; }; 42 | OBJ_83 /* MonthdayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_28 /* MonthdayTests.swift */; }; 43 | OBJ_84 /* PeriodTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_29 /* PeriodTests.swift */; }; 44 | OBJ_85 /* PlanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_30 /* PlanTests.swift */; }; 45 | OBJ_86 /* TaskCenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_31 /* TaskCenterTests.swift */; }; 46 | OBJ_87 /* TaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_32 /* TaskTests.swift */; }; 47 | OBJ_88 /* TimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_33 /* TimeTests.swift */; }; 48 | OBJ_89 /* WeekdayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_34 /* WeekdayTests.swift */; }; 49 | OBJ_90 /* XCTestManifests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_35 /* XCTestManifests.swift */; }; 50 | OBJ_92 /* Schedule.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "Schedule::Schedule::Product" /* Schedule.framework */; }; 51 | /* End PBXBuildFile section */ 52 | 53 | /* Begin PBXContainerItemProxy section */ 54 | 62CABDC722B9ECA400D8BDFE /* PBXContainerItemProxy */ = { 55 | isa = PBXContainerItemProxy; 56 | containerPortal = OBJ_1 /* Project object */; 57 | proxyType = 1; 58 | remoteGlobalIDString = "Schedule::Schedule"; 59 | remoteInfo = Schedule; 60 | }; 61 | 62CABDC822B9ECA400D8BDFE /* PBXContainerItemProxy */ = { 62 | isa = PBXContainerItemProxy; 63 | containerPortal = OBJ_1 /* Project object */; 64 | proxyType = 1; 65 | remoteGlobalIDString = "Schedule::ScheduleTests"; 66 | remoteInfo = ScheduleTests; 67 | }; 68 | /* End PBXContainerItemProxy section */ 69 | 70 | /* Begin PBXFileReference section */ 71 | OBJ_10 /* Bag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bag.swift; sourceTree = ""; }; 72 | OBJ_11 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 73 | OBJ_12 /* Interval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interval.swift; sourceTree = ""; }; 74 | OBJ_13 /* Monthday.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Monthday.swift; sourceTree = ""; }; 75 | OBJ_14 /* Period.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Period.swift; sourceTree = ""; }; 76 | OBJ_15 /* Plan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Plan.swift; sourceTree = ""; }; 77 | OBJ_16 /* RunLoopTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunLoopTask.swift; sourceTree = ""; }; 78 | OBJ_17 /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; }; 79 | OBJ_18 /* TaskCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskCenter.swift; sourceTree = ""; }; 80 | OBJ_19 /* Time.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Time.swift; sourceTree = ""; }; 81 | OBJ_20 /* Weekday.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weekday.swift; sourceTree = ""; }; 82 | OBJ_23 /* AtomicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicTests.swift; sourceTree = ""; }; 83 | OBJ_24 /* BagTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BagTests.swift; sourceTree = ""; }; 84 | OBJ_25 /* ExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionsTests.swift; sourceTree = ""; }; 85 | OBJ_26 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; 86 | OBJ_27 /* IntervalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalTests.swift; sourceTree = ""; }; 87 | OBJ_28 /* MonthdayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthdayTests.swift; sourceTree = ""; }; 88 | OBJ_29 /* PeriodTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeriodTests.swift; sourceTree = ""; }; 89 | OBJ_30 /* PlanTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanTests.swift; sourceTree = ""; }; 90 | OBJ_31 /* TaskCenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskCenterTests.swift; sourceTree = ""; }; 91 | OBJ_32 /* TaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskTests.swift; sourceTree = ""; }; 92 | OBJ_33 /* TimeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeTests.swift; sourceTree = ""; }; 93 | OBJ_34 /* WeekdayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekdayTests.swift; sourceTree = ""; }; 94 | OBJ_35 /* XCTestManifests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestManifests.swift; sourceTree = ""; }; 95 | OBJ_39 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = SOURCE_ROOT; }; 96 | OBJ_40 /* Schedule.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = Schedule.podspec; sourceTree = ""; }; 97 | OBJ_41 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 98 | OBJ_42 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 99 | OBJ_43 /* README.zh_cn.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.zh_cn.md; sourceTree = ""; }; 100 | OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 101 | OBJ_9 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; 102 | "Schedule::Schedule::Product" /* Schedule.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Schedule.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 103 | "Schedule::ScheduleTests::Product" /* ScheduleTests.xctest */ = {isa = PBXFileReference; lastKnownFileType = file; path = ScheduleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 104 | /* End PBXFileReference section */ 105 | 106 | /* Begin PBXFrameworksBuildPhase section */ 107 | OBJ_61 /* Frameworks */ = { 108 | isa = PBXFrameworksBuildPhase; 109 | buildActionMask = 0; 110 | files = ( 111 | ); 112 | runOnlyForDeploymentPostprocessing = 0; 113 | }; 114 | OBJ_91 /* Frameworks */ = { 115 | isa = PBXFrameworksBuildPhase; 116 | buildActionMask = 0; 117 | files = ( 118 | OBJ_92 /* Schedule.framework in Frameworks */, 119 | ); 120 | runOnlyForDeploymentPostprocessing = 0; 121 | }; 122 | /* End PBXFrameworksBuildPhase section */ 123 | 124 | /* Begin PBXGroup section */ 125 | OBJ_21 /* Tests */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | OBJ_22 /* ScheduleTests */, 129 | ); 130 | name = Tests; 131 | sourceTree = SOURCE_ROOT; 132 | }; 133 | OBJ_22 /* ScheduleTests */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | OBJ_23 /* AtomicTests.swift */, 137 | OBJ_24 /* BagTests.swift */, 138 | OBJ_25 /* ExtensionsTests.swift */, 139 | OBJ_26 /* Helpers.swift */, 140 | OBJ_27 /* IntervalTests.swift */, 141 | OBJ_28 /* MonthdayTests.swift */, 142 | OBJ_29 /* PeriodTests.swift */, 143 | OBJ_30 /* PlanTests.swift */, 144 | OBJ_31 /* TaskCenterTests.swift */, 145 | OBJ_32 /* TaskTests.swift */, 146 | OBJ_33 /* TimeTests.swift */, 147 | OBJ_34 /* WeekdayTests.swift */, 148 | OBJ_35 /* XCTestManifests.swift */, 149 | ); 150 | name = ScheduleTests; 151 | path = Tests/ScheduleTests; 152 | sourceTree = SOURCE_ROOT; 153 | }; 154 | OBJ_36 /* Products */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | "Schedule::Schedule::Product" /* Schedule.framework */, 158 | "Schedule::ScheduleTests::Product" /* ScheduleTests.xctest */, 159 | ); 160 | name = Products; 161 | sourceTree = BUILT_PRODUCTS_DIR; 162 | }; 163 | OBJ_5 /* */ = { 164 | isa = PBXGroup; 165 | children = ( 166 | OBJ_6 /* Package.swift */, 167 | OBJ_7 /* Sources */, 168 | OBJ_21 /* Tests */, 169 | OBJ_36 /* Products */, 170 | OBJ_39 /* assets */, 171 | OBJ_40 /* Schedule.podspec */, 172 | OBJ_41 /* LICENSE */, 173 | OBJ_42 /* README.md */, 174 | OBJ_43 /* README.zh_cn.md */, 175 | ); 176 | name = ""; 177 | sourceTree = ""; 178 | }; 179 | OBJ_7 /* Sources */ = { 180 | isa = PBXGroup; 181 | children = ( 182 | OBJ_8 /* Schedule */, 183 | ); 184 | name = Sources; 185 | sourceTree = SOURCE_ROOT; 186 | }; 187 | OBJ_8 /* Schedule */ = { 188 | isa = PBXGroup; 189 | children = ( 190 | OBJ_9 /* Atomic.swift */, 191 | OBJ_10 /* Bag.swift */, 192 | OBJ_11 /* Extensions.swift */, 193 | OBJ_12 /* Interval.swift */, 194 | OBJ_13 /* Monthday.swift */, 195 | OBJ_14 /* Period.swift */, 196 | OBJ_15 /* Plan.swift */, 197 | OBJ_16 /* RunLoopTask.swift */, 198 | OBJ_17 /* Task.swift */, 199 | OBJ_18 /* TaskCenter.swift */, 200 | OBJ_19 /* Time.swift */, 201 | OBJ_20 /* Weekday.swift */, 202 | ); 203 | name = Schedule; 204 | path = Sources/Schedule; 205 | sourceTree = SOURCE_ROOT; 206 | }; 207 | /* End PBXGroup section */ 208 | 209 | /* Begin PBXNativeTarget section */ 210 | "Schedule::Schedule" /* Schedule */ = { 211 | isa = PBXNativeTarget; 212 | buildConfigurationList = OBJ_45 /* Build configuration list for PBXNativeTarget "Schedule" */; 213 | buildPhases = ( 214 | OBJ_48 /* Sources */, 215 | OBJ_61 /* Frameworks */, 216 | ); 217 | buildRules = ( 218 | ); 219 | dependencies = ( 220 | ); 221 | name = Schedule; 222 | productName = Schedule; 223 | productReference = "Schedule::Schedule::Product" /* Schedule.framework */; 224 | productType = "com.apple.product-type.framework"; 225 | }; 226 | "Schedule::ScheduleTests" /* ScheduleTests */ = { 227 | isa = PBXNativeTarget; 228 | buildConfigurationList = OBJ_74 /* Build configuration list for PBXNativeTarget "ScheduleTests" */; 229 | buildPhases = ( 230 | OBJ_77 /* Sources */, 231 | OBJ_91 /* Frameworks */, 232 | ); 233 | buildRules = ( 234 | ); 235 | dependencies = ( 236 | OBJ_93 /* PBXTargetDependency */, 237 | ); 238 | name = ScheduleTests; 239 | productName = ScheduleTests; 240 | productReference = "Schedule::ScheduleTests::Product" /* ScheduleTests.xctest */; 241 | productType = "com.apple.product-type.bundle.unit-test"; 242 | }; 243 | "Schedule::SwiftPMPackageDescription" /* SchedulePackageDescription */ = { 244 | isa = PBXNativeTarget; 245 | buildConfigurationList = OBJ_63 /* Build configuration list for PBXNativeTarget "SchedulePackageDescription" */; 246 | buildPhases = ( 247 | OBJ_66 /* Sources */, 248 | ); 249 | buildRules = ( 250 | ); 251 | dependencies = ( 252 | ); 253 | name = SchedulePackageDescription; 254 | productName = SchedulePackageDescription; 255 | productType = "com.apple.product-type.framework"; 256 | }; 257 | /* End PBXNativeTarget section */ 258 | 259 | /* Begin PBXProject section */ 260 | OBJ_1 /* Project object */ = { 261 | isa = PBXProject; 262 | attributes = { 263 | LastSwiftMigration = 9999; 264 | LastUpgradeCheck = 9999; 265 | }; 266 | buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "Schedule" */; 267 | compatibilityVersion = "Xcode 3.2"; 268 | developmentRegion = English; 269 | hasScannedForEncodings = 0; 270 | knownRegions = ( 271 | English, 272 | en, 273 | ); 274 | mainGroup = OBJ_5 /* */; 275 | productRefGroup = OBJ_36 /* Products */; 276 | projectDirPath = ""; 277 | projectRoot = ""; 278 | targets = ( 279 | "Schedule::Schedule" /* Schedule */, 280 | "Schedule::SwiftPMPackageDescription" /* SchedulePackageDescription */, 281 | "Schedule::SchedulePackageTests::ProductTarget" /* SchedulePackageTests */, 282 | "Schedule::ScheduleTests" /* ScheduleTests */, 283 | ); 284 | }; 285 | /* End PBXProject section */ 286 | 287 | /* Begin PBXSourcesBuildPhase section */ 288 | OBJ_48 /* Sources */ = { 289 | isa = PBXSourcesBuildPhase; 290 | buildActionMask = 0; 291 | files = ( 292 | OBJ_49 /* Atomic.swift in Sources */, 293 | OBJ_50 /* Bag.swift in Sources */, 294 | OBJ_51 /* Extensions.swift in Sources */, 295 | OBJ_52 /* Interval.swift in Sources */, 296 | OBJ_53 /* Monthday.swift in Sources */, 297 | OBJ_54 /* Period.swift in Sources */, 298 | OBJ_55 /* Plan.swift in Sources */, 299 | OBJ_56 /* RunLoopTask.swift in Sources */, 300 | OBJ_57 /* Task.swift in Sources */, 301 | OBJ_58 /* TaskCenter.swift in Sources */, 302 | OBJ_59 /* Time.swift in Sources */, 303 | OBJ_60 /* Weekday.swift in Sources */, 304 | ); 305 | runOnlyForDeploymentPostprocessing = 0; 306 | }; 307 | OBJ_66 /* Sources */ = { 308 | isa = PBXSourcesBuildPhase; 309 | buildActionMask = 0; 310 | files = ( 311 | OBJ_67 /* Package.swift in Sources */, 312 | ); 313 | runOnlyForDeploymentPostprocessing = 0; 314 | }; 315 | OBJ_77 /* Sources */ = { 316 | isa = PBXSourcesBuildPhase; 317 | buildActionMask = 0; 318 | files = ( 319 | OBJ_78 /* AtomicTests.swift in Sources */, 320 | OBJ_79 /* BagTests.swift in Sources */, 321 | OBJ_80 /* ExtensionsTests.swift in Sources */, 322 | OBJ_81 /* Helpers.swift in Sources */, 323 | OBJ_82 /* IntervalTests.swift in Sources */, 324 | OBJ_83 /* MonthdayTests.swift in Sources */, 325 | OBJ_84 /* PeriodTests.swift in Sources */, 326 | OBJ_85 /* PlanTests.swift in Sources */, 327 | OBJ_86 /* TaskCenterTests.swift in Sources */, 328 | OBJ_87 /* TaskTests.swift in Sources */, 329 | OBJ_88 /* TimeTests.swift in Sources */, 330 | OBJ_89 /* WeekdayTests.swift in Sources */, 331 | OBJ_90 /* XCTestManifests.swift in Sources */, 332 | ); 333 | runOnlyForDeploymentPostprocessing = 0; 334 | }; 335 | /* End PBXSourcesBuildPhase section */ 336 | 337 | /* Begin PBXTargetDependency section */ 338 | OBJ_72 /* PBXTargetDependency */ = { 339 | isa = PBXTargetDependency; 340 | target = "Schedule::ScheduleTests" /* ScheduleTests */; 341 | targetProxy = 62CABDC822B9ECA400D8BDFE /* PBXContainerItemProxy */; 342 | }; 343 | OBJ_93 /* PBXTargetDependency */ = { 344 | isa = PBXTargetDependency; 345 | target = "Schedule::Schedule" /* Schedule */; 346 | targetProxy = 62CABDC722B9ECA400D8BDFE /* PBXContainerItemProxy */; 347 | }; 348 | /* End PBXTargetDependency section */ 349 | 350 | /* Begin XCBuildConfiguration section */ 351 | OBJ_3 /* Debug */ = { 352 | isa = XCBuildConfiguration; 353 | buildSettings = { 354 | CLANG_ENABLE_OBJC_ARC = YES; 355 | COMBINE_HIDPI_IMAGES = YES; 356 | COPY_PHASE_STRIP = NO; 357 | DEBUG_INFORMATION_FORMAT = dwarf; 358 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 359 | ENABLE_NS_ASSERTIONS = YES; 360 | GCC_OPTIMIZATION_LEVEL = 0; 361 | GCC_PREPROCESSOR_DEFINITIONS = ( 362 | "$(inherited)", 363 | "SWIFT_PACKAGE=1", 364 | "DEBUG=1", 365 | ); 366 | MACOSX_DEPLOYMENT_TARGET = 10.10; 367 | ONLY_ACTIVE_ARCH = YES; 368 | OTHER_SWIFT_FLAGS = "-DXcode"; 369 | PRODUCT_NAME = "$(TARGET_NAME)"; 370 | SDKROOT = macosx; 371 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 372 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE DEBUG"; 373 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 374 | USE_HEADERMAP = NO; 375 | }; 376 | name = Debug; 377 | }; 378 | OBJ_4 /* Release */ = { 379 | isa = XCBuildConfiguration; 380 | buildSettings = { 381 | CLANG_ENABLE_OBJC_ARC = YES; 382 | COMBINE_HIDPI_IMAGES = YES; 383 | COPY_PHASE_STRIP = YES; 384 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 385 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 386 | GCC_OPTIMIZATION_LEVEL = s; 387 | GCC_PREPROCESSOR_DEFINITIONS = ( 388 | "$(inherited)", 389 | "SWIFT_PACKAGE=1", 390 | ); 391 | MACOSX_DEPLOYMENT_TARGET = 10.10; 392 | OTHER_SWIFT_FLAGS = "-DXcode"; 393 | PRODUCT_NAME = "$(TARGET_NAME)"; 394 | SDKROOT = macosx; 395 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 396 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE"; 397 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 398 | USE_HEADERMAP = NO; 399 | }; 400 | name = Release; 401 | }; 402 | OBJ_46 /* Debug */ = { 403 | isa = XCBuildConfiguration; 404 | buildSettings = { 405 | CURRENT_PROJECT_VERSION = 1; 406 | ENABLE_TESTABILITY = YES; 407 | FRAMEWORK_SEARCH_PATHS = ( 408 | "$(inherited)", 409 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 410 | ); 411 | HEADER_SEARCH_PATHS = "$(inherited)"; 412 | INFOPLIST_FILE = Schedule.xcodeproj/Schedule_Info.plist; 413 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 414 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 415 | MACOSX_DEPLOYMENT_TARGET = 10.11; 416 | MARKETING_VERSION = 2.0.3; 417 | OTHER_CFLAGS = "$(inherited)"; 418 | OTHER_LDFLAGS = "$(inherited)"; 419 | OTHER_SWIFT_FLAGS = "$(inherited)"; 420 | PRODUCT_BUNDLE_IDENTIFIER = Schedule; 421 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 422 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 423 | SKIP_INSTALL = YES; 424 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 425 | SWIFT_VERSION = 5.0; 426 | TARGET_NAME = Schedule; 427 | TVOS_DEPLOYMENT_TARGET = 9.0; 428 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 429 | }; 430 | name = Debug; 431 | }; 432 | OBJ_47 /* Release */ = { 433 | isa = XCBuildConfiguration; 434 | buildSettings = { 435 | CURRENT_PROJECT_VERSION = 1; 436 | ENABLE_TESTABILITY = YES; 437 | FRAMEWORK_SEARCH_PATHS = ( 438 | "$(inherited)", 439 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 440 | ); 441 | HEADER_SEARCH_PATHS = "$(inherited)"; 442 | INFOPLIST_FILE = Schedule.xcodeproj/Schedule_Info.plist; 443 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 444 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 445 | MACOSX_DEPLOYMENT_TARGET = 10.11; 446 | MARKETING_VERSION = 2.0.3; 447 | OTHER_CFLAGS = "$(inherited)"; 448 | OTHER_LDFLAGS = "$(inherited)"; 449 | OTHER_SWIFT_FLAGS = "$(inherited)"; 450 | PRODUCT_BUNDLE_IDENTIFIER = Schedule; 451 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 452 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 453 | SKIP_INSTALL = YES; 454 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 455 | SWIFT_VERSION = 5.0; 456 | TARGET_NAME = Schedule; 457 | TVOS_DEPLOYMENT_TARGET = 9.0; 458 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 459 | }; 460 | name = Release; 461 | }; 462 | OBJ_64 /* Debug */ = { 463 | isa = XCBuildConfiguration; 464 | buildSettings = { 465 | LD = /usr/bin/true; 466 | OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk"; 467 | SWIFT_VERSION = 5.0; 468 | }; 469 | name = Debug; 470 | }; 471 | OBJ_65 /* Release */ = { 472 | isa = XCBuildConfiguration; 473 | buildSettings = { 474 | LD = /usr/bin/true; 475 | OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk"; 476 | SWIFT_VERSION = 5.0; 477 | }; 478 | name = Release; 479 | }; 480 | OBJ_70 /* Debug */ = { 481 | isa = XCBuildConfiguration; 482 | buildSettings = { 483 | }; 484 | name = Debug; 485 | }; 486 | OBJ_71 /* Release */ = { 487 | isa = XCBuildConfiguration; 488 | buildSettings = { 489 | }; 490 | name = Release; 491 | }; 492 | OBJ_75 /* Debug */ = { 493 | isa = XCBuildConfiguration; 494 | buildSettings = { 495 | CLANG_ENABLE_MODULES = YES; 496 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 497 | FRAMEWORK_SEARCH_PATHS = ( 498 | "$(inherited)", 499 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 500 | ); 501 | HEADER_SEARCH_PATHS = "$(inherited)"; 502 | INFOPLIST_FILE = Schedule.xcodeproj/ScheduleTests_Info.plist; 503 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 504 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; 505 | MACOSX_DEPLOYMENT_TARGET = 10.11; 506 | OTHER_CFLAGS = "$(inherited)"; 507 | OTHER_LDFLAGS = "$(inherited)"; 508 | OTHER_SWIFT_FLAGS = "$(inherited)"; 509 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 510 | SWIFT_VERSION = 5.0; 511 | TARGET_NAME = ScheduleTests; 512 | TVOS_DEPLOYMENT_TARGET = 9.0; 513 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 514 | }; 515 | name = Debug; 516 | }; 517 | OBJ_76 /* Release */ = { 518 | isa = XCBuildConfiguration; 519 | buildSettings = { 520 | CLANG_ENABLE_MODULES = YES; 521 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 522 | FRAMEWORK_SEARCH_PATHS = ( 523 | "$(inherited)", 524 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 525 | ); 526 | HEADER_SEARCH_PATHS = "$(inherited)"; 527 | INFOPLIST_FILE = Schedule.xcodeproj/ScheduleTests_Info.plist; 528 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 529 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; 530 | MACOSX_DEPLOYMENT_TARGET = 10.11; 531 | OTHER_CFLAGS = "$(inherited)"; 532 | OTHER_LDFLAGS = "$(inherited)"; 533 | OTHER_SWIFT_FLAGS = "$(inherited)"; 534 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 535 | SWIFT_VERSION = 5.0; 536 | TARGET_NAME = ScheduleTests; 537 | TVOS_DEPLOYMENT_TARGET = 9.0; 538 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 539 | }; 540 | name = Release; 541 | }; 542 | /* End XCBuildConfiguration section */ 543 | 544 | /* Begin XCConfigurationList section */ 545 | OBJ_2 /* Build configuration list for PBXProject "Schedule" */ = { 546 | isa = XCConfigurationList; 547 | buildConfigurations = ( 548 | OBJ_3 /* Debug */, 549 | OBJ_4 /* Release */, 550 | ); 551 | defaultConfigurationIsVisible = 0; 552 | defaultConfigurationName = Release; 553 | }; 554 | OBJ_45 /* Build configuration list for PBXNativeTarget "Schedule" */ = { 555 | isa = XCConfigurationList; 556 | buildConfigurations = ( 557 | OBJ_46 /* Debug */, 558 | OBJ_47 /* Release */, 559 | ); 560 | defaultConfigurationIsVisible = 0; 561 | defaultConfigurationName = Release; 562 | }; 563 | OBJ_63 /* Build configuration list for PBXNativeTarget "SchedulePackageDescription" */ = { 564 | isa = XCConfigurationList; 565 | buildConfigurations = ( 566 | OBJ_64 /* Debug */, 567 | OBJ_65 /* Release */, 568 | ); 569 | defaultConfigurationIsVisible = 0; 570 | defaultConfigurationName = Release; 571 | }; 572 | OBJ_69 /* Build configuration list for PBXAggregateTarget "SchedulePackageTests" */ = { 573 | isa = XCConfigurationList; 574 | buildConfigurations = ( 575 | OBJ_70 /* Debug */, 576 | OBJ_71 /* Release */, 577 | ); 578 | defaultConfigurationIsVisible = 0; 579 | defaultConfigurationName = Release; 580 | }; 581 | OBJ_74 /* Build configuration list for PBXNativeTarget "ScheduleTests" */ = { 582 | isa = XCConfigurationList; 583 | buildConfigurations = ( 584 | OBJ_75 /* Debug */, 585 | OBJ_76 /* Release */, 586 | ); 587 | defaultConfigurationIsVisible = 0; 588 | defaultConfigurationName = Release; 589 | }; 590 | /* End XCConfigurationList section */ 591 | }; 592 | rootObject = OBJ_1 /* Project object */; 593 | } 594 | -------------------------------------------------------------------------------- /Schedule.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /Schedule.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Schedule.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | -------------------------------------------------------------------------------- /Schedule.xcodeproj/xcshareddata/xcschemes/Schedule-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 63 | 64 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /Sources/Schedule/Atomic.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// An atomic box that can read and write the underlying value atomically. 4 | final class Atomic { 5 | 6 | private var val: T 7 | private let lock = NSLock() 8 | 9 | /// Create an atomic box with the given initial value. 10 | @inline(__always) 11 | init(_ value: T) { 12 | self.val = value 13 | } 14 | 15 | /// Reads the current value atomically. 16 | @inline(__always) 17 | func read(_ body: (T) -> U) -> U { 18 | return lock.withLock { body(val) } 19 | } 20 | 21 | /// Reads the current value atomically. 22 | @inline(__always) 23 | func readVoid(_ body: (T) -> Void) { 24 | lock.withLockVoid { body(val) } 25 | } 26 | 27 | /// Writes the current value atomically. 28 | @inline(__always) 29 | func write(_ body: (inout T) -> U) -> U { 30 | return lock.withLock { body(&val) } 31 | } 32 | 33 | /// Writes the current value atomically. 34 | @inline(__always) 35 | func writeVoid(_ body: (inout T) -> Void) { 36 | lock.withLockVoid { body(&val) } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Schedule/Bag.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A unique key for removing an element from a bag. 4 | struct BagKey: Equatable { 5 | 6 | private let i: UInt64 7 | 8 | /// A generator that can generate a sequence of unique `BagKey`. 9 | /// 10 | /// let k1 = gen.next() 11 | /// let k2 = gen.next() 12 | /// ... 13 | struct Gen { 14 | private var key = BagKey(i: 0) 15 | init() { } 16 | mutating func next() -> BagKey { 17 | defer { key = BagKey(i: key.i + 1) } 18 | return key 19 | } 20 | } 21 | } 22 | 23 | /// An ordered sequence. 24 | /// 25 | /// let k1 = bag.append(e1) 26 | /// let k2 = bag.append(e2) 27 | /// 28 | /// for e in bag { 29 | /// // -> e1 30 | /// // -> e2 31 | /// } 32 | /// 33 | /// bag.removeValue(for: k1) 34 | struct Bag { 35 | 36 | private typealias Entry = (key: BagKey, val: Element) 37 | 38 | private var keyGen = BagKey.Gen() 39 | private var entries: [Entry] = [] 40 | 41 | /// Appends a new element at the end of this bag. 42 | @discardableResult 43 | mutating func append(_ new: Element) -> BagKey { 44 | let key = keyGen.next() 45 | 46 | let entry = (key: key, val: new) 47 | entries.append(entry) 48 | 49 | return key 50 | } 51 | 52 | /// Returns the element associated with a given key. 53 | func value(for key: BagKey) -> Element? { 54 | return entries.first(where: { $0.key == key })?.val 55 | } 56 | 57 | /// Removes the given key and its associated element from this bag. 58 | @discardableResult 59 | mutating func removeValue(for key: BagKey) -> Element? { 60 | if let i = entries.firstIndex(where: { $0.key == key }) { 61 | return entries.remove(at: i).val 62 | } 63 | return nil 64 | } 65 | 66 | /// Removes all elements from this bag. 67 | mutating func removeAll() { 68 | entries.removeAll() 69 | } 70 | 71 | /// The number of elements in this bag. 72 | var count: Int { 73 | return entries.count 74 | } 75 | } 76 | 77 | extension Bag: Sequence { 78 | 79 | /// Returns an iterator over the elements of this bag. 80 | @inline(__always) 81 | func makeIterator() -> AnyIterator { 82 | var iterator = entries.makeIterator() 83 | return AnyIterator { 84 | return iterator.next()?.val 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/Schedule/Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Double { 4 | 5 | /// Returns a value of this number clamped to `Int.min...Int.max`. 6 | func clampedToInt() -> Int { 7 | if self >= Double(Int.max) { return Int.max } 8 | if self <= Double(Int.min) { return Int.min } 9 | return Int(self) 10 | } 11 | } 12 | 13 | extension Int { 14 | 15 | /// Returns the sum of the two given values, in case of any overflow, 16 | /// the result will be clamped to int. 17 | func clampedAdding(_ other: Int) -> Int { 18 | return (Double(self) + Double(other)).clampedToInt() 19 | } 20 | } 21 | 22 | extension Locale { 23 | 24 | static let posix = Locale(identifier: "en_US_POSIX") 25 | } 26 | 27 | extension Calendar { 28 | 29 | /// The gregorian calendar with `en_US_POSIX` locale. 30 | static let gregorian: Calendar = { 31 | var cal = Calendar(identifier: .gregorian) 32 | cal.locale = Locale.posix 33 | return cal 34 | }() 35 | } 36 | 37 | extension Date { 38 | 39 | /// Zero o'clock in the morning. 40 | var startOfToday: Date { 41 | return Calendar.gregorian.startOfDay(for: self) 42 | } 43 | } 44 | 45 | extension NSLocking { 46 | 47 | /// Executes a closure returning a value while acquiring the lock. 48 | @inline(__always) 49 | func withLock(_ body: () throws -> T) rethrows -> T { 50 | lock(); defer { unlock() } 51 | return try body() 52 | } 53 | 54 | /// Executes a closure while acquiring the lock. 55 | @inline(__always) 56 | func withLockVoid(_ body: () throws -> Void) rethrows { 57 | lock(); defer { unlock() } 58 | try body() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/Schedule/Interval.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Type used to represent a time-based amount of time, such as '34.5 seconds'. 4 | public struct Interval { 5 | 6 | /// The length of this interval in nanoseconds. 7 | public let nanoseconds: Double 8 | 9 | /// Creates an interval from the given number of nanoseconds. 10 | public init(nanoseconds: Double) { 11 | self.nanoseconds = nanoseconds 12 | } 13 | } 14 | 15 | extension Interval: Hashable { } 16 | 17 | extension Interval { 18 | 19 | /// A Boolean value indicating whether this interval is less than zero. 20 | /// 21 | /// 22 | /// An Interval represents a directed distance between two points 23 | /// on the time-line and can therefore be positive, zero or negative. 24 | public var isNegative: Bool { 25 | return nanoseconds < 0 26 | } 27 | 28 | /// A copy of this duration with a positive length. 29 | public var abs: Interval { 30 | return Interval(nanoseconds: Swift.abs(nanoseconds)) 31 | } 32 | 33 | /// A copy of this interval with the length negated. 34 | public var negated: Interval { 35 | return Interval(nanoseconds: -nanoseconds) 36 | } 37 | } 38 | 39 | extension Interval: CustomStringConvertible { 40 | 41 | /// A textual representation of this interval. 42 | /// 43 | /// "Interval: 1000 nanoseconds" 44 | public var description: String { 45 | return "Interval: \(nanoseconds.clampedToInt()) nanosecond(s)" 46 | } 47 | } 48 | 49 | extension Interval: CustomDebugStringConvertible { 50 | 51 | /// A textual representation of this interval for debugging. 52 | /// 53 | /// "Interval: 1000 nanoseconds" 54 | public var debugDescription: String { 55 | return description 56 | } 57 | } 58 | 59 | // MARK: - Comparing 60 | 61 | extension Interval: Comparable { 62 | 63 | /// Compares two intervals and returns a comparison result value 64 | /// that indicates the sort order of two intervals. 65 | /// 66 | /// A positive interval is always ordered ascending to a negative interval. 67 | public func compare(_ other: Interval) -> ComparisonResult { 68 | let d = nanoseconds - other.nanoseconds 69 | 70 | if d < 0 { return .orderedAscending } 71 | if d > 0 { return .orderedDescending } 72 | return .orderedSame 73 | } 74 | 75 | /// Returns a Boolean value indicating whether the first interval is 76 | /// less than the second interval. 77 | /// 78 | /// A negative interval is always less than a positive interval. 79 | public static func < (lhs: Interval, rhs: Interval) -> Bool { 80 | return lhs.compare(rhs) == .orderedAscending 81 | } 82 | 83 | /// Returns a Boolean value indicating whether this interval is longer 84 | /// than the given interval. 85 | public func isLonger(than other: Interval) -> Bool { 86 | return abs > other.abs 87 | } 88 | 89 | /// Returns a Boolean value indicating whether this interval is shorter 90 | /// than the given interval. 91 | public func isShorter(than other: Interval) -> Bool { 92 | return abs < other.abs 93 | } 94 | } 95 | 96 | // MARK: - Adding & Subtracting 97 | 98 | extension Interval { 99 | 100 | /// Returns a new interval by multipling this interval by the given number. 101 | /// 102 | /// 1.hour * 2 == 2.hours 103 | public func multiplying(by multiplier: Double) -> Interval { 104 | return Interval(nanoseconds: nanoseconds * multiplier) 105 | } 106 | 107 | /// Returns a new interval by adding the given interval to this interval. 108 | /// 109 | /// 1.hour + 1.hour == 2.hours 110 | public func adding(_ other: Interval) -> Interval { 111 | return Interval(nanoseconds: nanoseconds + other.nanoseconds) 112 | } 113 | } 114 | 115 | // MARK: - Operators 116 | extension Interval { 117 | 118 | /// Returns a new interval by multipling the left interval by the right number. 119 | /// 120 | /// 1.hour * 2 == 2.hours 121 | public static func * (lhs: Interval, rhs: Double) -> Interval { 122 | return lhs.multiplying(by: rhs) 123 | } 124 | 125 | /// Returns a new interval by adding the right interval to the left interval. 126 | /// 127 | /// 1.hour + 1.hour == 2.hours 128 | public static func + (lhs: Interval, rhs: Interval) -> Interval { 129 | return lhs.adding(rhs) 130 | } 131 | 132 | /// Returns a new interval by subtracting the right interval from the left interval. 133 | /// 134 | /// 2.hours - 1.hour == 1.hour 135 | public static func - (lhs: Interval, rhs: Interval) -> Interval { 136 | return lhs.adding(rhs.negated) 137 | } 138 | 139 | /// Adds two intervals and stores the result in the left interval. 140 | public static func += (lhs: inout Interval, rhs: Interval) { 141 | lhs = lhs.adding(rhs) 142 | } 143 | 144 | /// Returns the additive inverse of the specified interval. 145 | public prefix static func - (interval: Interval) -> Interval { 146 | return interval.negated 147 | } 148 | } 149 | 150 | // MARK: - Sugars 151 | 152 | extension Interval { 153 | 154 | /// The length of this interval in nanoseconds. 155 | public func asNanoseconds() -> Double { 156 | return nanoseconds 157 | } 158 | 159 | /// The length of this interval in microseconds. 160 | public func asMicroseconds() -> Double { 161 | return nanoseconds / pow(10, 3) 162 | } 163 | 164 | /// The length of this interval in milliseconds. 165 | public func asMilliseconds() -> Double { 166 | return nanoseconds / pow(10, 6) 167 | } 168 | 169 | /// The length of this interval in seconds. 170 | public func asSeconds() -> Double { 171 | return nanoseconds / pow(10, 9) 172 | } 173 | 174 | /// The length of this interval in minutes. 175 | public func asMinutes() -> Double { 176 | return asSeconds() / 60 177 | } 178 | 179 | /// The length of this interval in hours. 180 | public func asHours() -> Double { 181 | return asMinutes() / 60 182 | } 183 | 184 | /// The length of this interval in days. 185 | public func asDays() -> Double { 186 | return asHours() / 24 187 | } 188 | 189 | /// The length of this interval in weeks. 190 | public func asWeeks() -> Double { 191 | return asDays() / 7 192 | } 193 | } 194 | 195 | /// `IntervalConvertible` provides a set of intuitive apis for creating interval. 196 | public protocol IntervalConvertible { 197 | 198 | var nanoseconds: Interval { get } 199 | } 200 | 201 | extension Int: IntervalConvertible { 202 | 203 | /// Creates an interval from this amount of nanoseconds. 204 | public var nanoseconds: Interval { 205 | return Interval(nanoseconds: Double(self)) 206 | } 207 | } 208 | 209 | extension Double: IntervalConvertible { 210 | 211 | /// Creates an interval from this amount of nanoseconds. 212 | public var nanoseconds: Interval { 213 | return Interval(nanoseconds: self) 214 | } 215 | } 216 | 217 | extension IntervalConvertible { 218 | 219 | // Alias for `nanoseconds`. 220 | public var nanosecond: Interval { 221 | return nanoseconds 222 | } 223 | 224 | // Alias for `microseconds`. 225 | public var microsecond: Interval { 226 | return microseconds 227 | } 228 | 229 | /// Creates an interval from this amount of microseconds. 230 | public var microseconds: Interval { 231 | return nanoseconds * pow(10, 3) 232 | } 233 | 234 | /// Alias for `milliseconds`. 235 | public var millisecond: Interval { 236 | return milliseconds 237 | } 238 | 239 | /// Creates an interval from this amount of milliseconds. 240 | public var milliseconds: Interval { 241 | return microseconds * pow(10, 3) 242 | } 243 | 244 | /// Alias for `second`. 245 | public var second: Interval { 246 | return seconds 247 | } 248 | 249 | /// Creates an interval from this amount of seconds. 250 | public var seconds: Interval { 251 | return milliseconds * pow(10, 3) 252 | } 253 | 254 | /// Alias for `minute`. 255 | public var minute: Interval { 256 | return minutes 257 | } 258 | 259 | /// Creates an interval from this amount of minutes. 260 | public var minutes: Interval { 261 | return seconds * 60 262 | } 263 | 264 | /// Alias for `hours`. 265 | public var hour: Interval { 266 | return hours 267 | } 268 | 269 | /// Creates an interval from this amount of hours. 270 | public var hours: Interval { 271 | return minutes * 60 272 | } 273 | 274 | /// Alias for `days`. 275 | public var day: Interval { 276 | return days 277 | } 278 | 279 | /// Creates an interval from this amount of days. 280 | public var days: Interval { 281 | return hours * 24 282 | } 283 | 284 | /// Alias for `weeks`. 285 | public var week: Interval { 286 | return weeks 287 | } 288 | 289 | /// Creates an interval from this amount of weeks. 290 | public var weeks: Interval { 291 | return days * 7 292 | } 293 | } 294 | 295 | // MARK: - Date 296 | 297 | extension Date { 298 | 299 | /// The interval between this date and the current date and time. 300 | /// 301 | /// If this date is earlier than now, the interval will be negative. 302 | public var intervalSinceNow: Interval { 303 | return timeIntervalSinceNow.seconds 304 | } 305 | 306 | /// Returns the interval between this date and the given date. 307 | /// 308 | /// If this date is earlier than the given date, the interval will be negative. 309 | public func interval(since date: Date) -> Interval { 310 | return timeIntervalSince(date).seconds 311 | } 312 | 313 | /// Returns a new date by adding an interval to this date. 314 | public func adding(_ interval: Interval) -> Date { 315 | return addingTimeInterval(interval.asSeconds()) 316 | } 317 | 318 | /// Returns a new date by adding an interval to the date. 319 | public static func + (lhs: Date, rhs: Interval) -> Date { 320 | return lhs.adding(rhs) 321 | } 322 | } 323 | 324 | // MARK: - DispatchSourceTimer 325 | 326 | extension DispatchSourceTimer { 327 | 328 | /// Schedule this timer after the given interval. 329 | func schedule(after timeout: Interval) { 330 | if timeout.isNegative { return } 331 | let ns = timeout.nanoseconds.clampedToInt() 332 | schedule(wallDeadline: .now() + DispatchTimeInterval.nanoseconds(ns)) 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /Sources/Schedule/Monthday.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `Monthday` represents the combination of a month and day-of-month. 4 | public enum Monthday { 5 | 6 | case january(Int) 7 | 8 | case february(Int) 9 | 10 | case march(Int) 11 | 12 | case april(Int) 13 | 14 | case may(Int) 15 | 16 | case june(Int) 17 | 18 | case july(Int) 19 | 20 | case august(Int) 21 | 22 | case september(Int) 23 | 24 | case october(Int) 25 | 26 | case november(Int) 27 | 28 | case december(Int) 29 | 30 | /// Returns a dateComponenets of this monthday, using gregorian calender and 31 | /// current time zone. 32 | public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents { 33 | var month, day: Int 34 | switch self { 35 | case .january(let n): month = 1; day = n 36 | case .february(let n): month = 2; day = n 37 | case .march(let n): month = 3; day = n 38 | case .april(let n): month = 4; day = n 39 | case .may(let n): month = 5; day = n 40 | case .june(let n): month = 6; day = n 41 | case .july(let n): month = 7; day = n 42 | case .august(let n): month = 8; day = n 43 | case .september(let n): month = 9; day = n 44 | case .october(let n): month = 10; day = n 45 | case .november(let n): month = 11; day = n 46 | case .december(let n): month = 12; day = n 47 | } 48 | return DateComponents( 49 | calendar: Calendar.gregorian, 50 | timeZone: timeZone, 51 | month: month, 52 | day: day) 53 | } 54 | } 55 | 56 | extension Date { 57 | 58 | /// Returns a Boolean value indicating whether this date is the monthday in current time zone.. 59 | public func `is`(_ monthday: Monthday, in timeZone: TimeZone = .current) -> Bool { 60 | let components = monthday.asDateComponents(timeZone) 61 | 62 | let m = Calendar.gregorian.component(.month, from: self) 63 | let d = Calendar.gregorian.component(.day, from: self) 64 | return m == components.month && d == components.day 65 | } 66 | } 67 | 68 | extension Monthday: CustomStringConvertible { 69 | 70 | /// A textual representation of this monthday. 71 | /// 72 | /// "Monthday: May 1st" 73 | public var description: String { 74 | let components = asDateComponents() 75 | 76 | let m = components.month! 77 | let d = components.day! 78 | 79 | let ms = Calendar.gregorian.monthSymbols[m - 1] 80 | 81 | let fmt = NumberFormatter() 82 | fmt.locale = Locale.posix 83 | fmt.numberStyle = .ordinal 84 | let ds = fmt.string(from: NSNumber(value: d))! 85 | 86 | return "Monthday: \(ms) \(ds)" 87 | } 88 | } 89 | 90 | extension Monthday: CustomDebugStringConvertible { 91 | 92 | /// A textual representation of this monthday for debugging. 93 | /// 94 | /// "Monthday: May 1st" 95 | public var debugDescription: String { 96 | return description 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/Schedule/Period.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Type used to represent a date-based amount of time in the ISO-8601 calendar system, 4 | /// such as '2 years, 3 months and 4 days'. 5 | /// 6 | /// It's a little different from `Interval`: 7 | /// 8 | /// - If you add a period `1.month` to January 1st, 9 | /// you will get February 1st. 10 | /// 11 | /// - If you add the same period to February 1st, 12 | /// you will get March 1st. 13 | /// 14 | /// But the intervals(`31.days` in case 1, `28.days` or `29.days` in case 2) 15 | /// in these two cases are quite different. 16 | public struct Period { 17 | 18 | public private(set) var years: Int 19 | 20 | public private(set) var months: Int 21 | 22 | public private(set) var days: Int 23 | 24 | public private(set) var hours: Int 25 | 26 | public private(set) var minutes: Int 27 | 28 | public private(set) var seconds: Int 29 | 30 | public private(set) var nanoseconds: Int 31 | 32 | /// Initializes a period value, optional sepcifying values for its fields. 33 | public init(years: Int = 0, months: Int = 0, days: Int = 0, 34 | hours: Int = 0, minutes: Int = 0, seconds: Int = 0, 35 | nanoseconds: Int = 0) { 36 | self.years = years 37 | self.months = months 38 | self.days = days 39 | self.hours = hours 40 | self.minutes = minutes 41 | self.seconds = seconds 42 | self.nanoseconds = nanoseconds 43 | } 44 | 45 | private static let quantifiers: Atomic<[String: Int]> = Atomic([ 46 | "one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6, 47 | "seven": 7, "eight": 8, "nine": 9, "ten": 10, "eleven": 11, "twelve": 12 48 | ]) 49 | 50 | /// Registers your own quantifier. 51 | /// 52 | /// Period.registerQuantifier("fifty", for: 15) 53 | /// let period = Period("fifty minutes") 54 | public static func registerQuantifier(_ word: String, for number: Int) { 55 | quantifiers.writeVoid { $0[word] = number } 56 | } 57 | 58 | /// Initializes a period from a natural expression. 59 | /// 60 | /// Period("one second") -> Period(seconds: 1) 61 | /// Period("two hours and ten minutes") -> Period(hours: 2, minutes: 10) 62 | /// Period("1 year, 2 months and 3 days") -> Period(years: 1, months: 2, days: 3) 63 | public init?(_ string: String) { 64 | var str = string 65 | for (word, number) in Period.quantifiers.read({ $0 }) { 66 | str = str.replacingOccurrences(of: word, with: "\(number)") 67 | } 68 | 69 | // swiftlint:disable force_try 70 | let regexp = try! NSRegularExpression(pattern: "( and |, )") 71 | 72 | let mark: Character = "秋" 73 | str = regexp.stringByReplacingMatches( 74 | in: str, 75 | range: NSRange(str.startIndex..., in: str), 76 | withTemplate: String(mark) 77 | ) 78 | 79 | var period = 0.year 80 | for pair in str.split(separator: mark).map({ $0.split(separator: " ") }) { 81 | guard 82 | pair.count == 2, 83 | let number = Int(pair[0]) 84 | else { 85 | return nil 86 | } 87 | 88 | var unit = pair[1] 89 | if unit.last == "s" { unit.removeLast() } 90 | switch unit { 91 | case "year": period = period + number.years 92 | case "month": period = period + number.months 93 | case "day": period = period + number.days 94 | case "week": period = period + (number * 7).days 95 | case "hour": period = period + number.hours 96 | case "minute": period = period + number.minutes 97 | case "second": period = period + number.second 98 | case "nanosecond": period = period + number.nanosecond 99 | default: break 100 | } 101 | } 102 | self = period 103 | } 104 | 105 | /// Returns a new period by adding the given period to this period. 106 | public func adding(_ other: Period) -> Period { 107 | return Period( 108 | years: years.clampedAdding(other.years), 109 | months: months.clampedAdding(other.months), 110 | days: days.clampedAdding(other.days), 111 | hours: hours.clampedAdding(other.hours), 112 | minutes: minutes.clampedAdding(other.minutes), 113 | seconds: seconds.clampedAdding(other.seconds), 114 | nanoseconds: nanoseconds.clampedAdding(other.nanoseconds)) 115 | } 116 | 117 | /// Returns a new period by adding an interval to the period. 118 | /// 119 | /// The return value will be tidied to `day` aotumatically. 120 | public func adding(_ interval: Interval) -> Period { 121 | return Period( 122 | years: years, months: months, days: days, 123 | hours: hours, minutes: minutes, seconds: seconds, 124 | nanoseconds: nanoseconds.clampedAdding(interval.nanoseconds.clampedToInt())) 125 | .tidied(to: .day) 126 | } 127 | 128 | /// Returns a new period by adding the right period to the left period. 129 | /// 130 | /// Period(days: 1) + Period(days: 1) -> Period(days: 2) 131 | public static func + (lhs: Period, rhs: Period) -> Period { 132 | return lhs.adding(rhs) 133 | } 134 | 135 | /// Returns a new period by adding an interval to the period. 136 | /// 137 | /// The return value will be tidied to `day` aotumatically. 138 | public static func + (lhs: Period, rhs: Interval) -> Period { 139 | return lhs.adding(rhs) 140 | } 141 | 142 | /// Represents the tidy level. 143 | public enum TideLevel { 144 | case day, hour, minute, second, nanosecond 145 | } 146 | 147 | /// Returns the tidied period. 148 | /// 149 | /// Period(hours: 25).tidied(to .day) => Period(days: 1, hours: 1) 150 | public func tidied(to level: TideLevel) -> Period { 151 | var period = self 152 | 153 | if case .nanosecond = level { return period } 154 | 155 | if period.nanoseconds.magnitude >= UInt(1.second.nanoseconds) { 156 | period.seconds += period.nanoseconds / Int(1.second.nanoseconds) 157 | period.nanoseconds %= Int(1.second.nanoseconds) 158 | } 159 | if case .second = level { return period } 160 | 161 | if period.seconds.magnitude >= 60 { 162 | period.minutes += period.seconds / 60 163 | period.seconds %= 60 164 | } 165 | if case .minute = level { return period } 166 | 167 | if period.minutes.magnitude >= 60 { 168 | period.hours += period.minutes / 60 169 | period.minutes %= 60 170 | } 171 | if case .hour = level { return period } 172 | 173 | if period.hours.magnitude >= 24 { 174 | period.days += period.hours / 24 175 | period.hours %= 24 176 | } 177 | return period 178 | } 179 | 180 | /// Returns a dateComponenets of this period, using gregorian calender and 181 | /// current time zone. 182 | public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents { 183 | return DateComponents( 184 | calendar: Calendar.gregorian, 185 | timeZone: timeZone, 186 | year: years, 187 | month: months, 188 | day: days, 189 | hour: hours, 190 | minute: minutes, 191 | second: seconds, 192 | nanosecond: nanoseconds 193 | ) 194 | } 195 | } 196 | 197 | extension Date { 198 | 199 | /// Returns a new date by adding a period to this date. 200 | public func adding(_ period: Period) -> Date { 201 | return Calendar.gregorian.date(byAdding: period.asDateComponents(), to: self) ?? .distantFuture 202 | } 203 | 204 | /// Returns a new date by adding a period to this date. 205 | public static func + (lhs: Date, rhs: Period) -> Date { 206 | return lhs.adding(rhs) 207 | } 208 | } 209 | 210 | extension Int { 211 | 212 | /// Creates a period from this amount of years. 213 | public var years: Period { 214 | return Period(years: self) 215 | } 216 | 217 | /// Alias for `years`. 218 | public var year: Period { 219 | return years 220 | } 221 | 222 | /// Creates a period from this amount of month. 223 | public var months: Period { 224 | return Period(months: self) 225 | } 226 | 227 | /// Alias for `month`. 228 | public var month: Period { 229 | return months 230 | } 231 | } 232 | 233 | extension Period: CustomStringConvertible { 234 | 235 | /// A textual representation of this period. 236 | /// 237 | /// "Period: 1 year(s) 2 month(s) 3 day(s)" 238 | public var description: String { 239 | let period = tidied(to: .day) 240 | var desc = "Period:" 241 | if period.years != 0 { desc += " \(period.years) year(s)" } 242 | if period.months != 0 { desc += " \(period.months) month(s)" } 243 | if period.days != 0 { desc += " \(period.days) day(s)" } 244 | if period.hours != 0 { desc += " \(period.hours) hour(s)" } 245 | if period.minutes != 0 { desc += " \(period.minutes) minute(s)" } 246 | if period.seconds != 0 { desc += " \(period.seconds) second(s)" } 247 | if period.nanoseconds != 0 { desc += " \(period.nanoseconds) nanosecond(s)" } 248 | return desc 249 | } 250 | } 251 | 252 | extension Period: CustomDebugStringConvertible { 253 | 254 | /// A textual representation of this period for debugging. 255 | /// 256 | /// "Period: 1 year(s) 2 month(s) 3 day(s)" 257 | public var debugDescription: String { 258 | return description 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /Sources/Schedule/Plan.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `Plan` represents a sequence of times at which a task should be 4 | /// executed. 5 | /// 6 | /// `Plan` is `Interval` based. 7 | public struct Plan: Sequence { 8 | 9 | private var seq: AnySequence 10 | 11 | private init(_ sequence: S) where S: Sequence, S.Element == Interval { 12 | seq = AnySequence(sequence) 13 | } 14 | 15 | /// Returns an iterator over the interval of this sequence. 16 | public func makeIterator() -> AnyIterator { 17 | return seq.makeIterator() 18 | } 19 | 20 | /// Schedules a task with this plan. 21 | /// 22 | /// - Parameters: 23 | /// - queue: The dispatch queue to which the action should be dispatched. 24 | /// - action: A block to be executed when time is up. 25 | /// - Returns: The task just created. 26 | public func `do`( 27 | queue: DispatchQueue, 28 | action: @escaping (Task) -> Void 29 | ) -> Task { 30 | return Task(plan: self, queue: queue, action: action) 31 | } 32 | 33 | /// Schedules a task with this plan. 34 | /// 35 | /// - Parameters: 36 | /// - queue: The dispatch queue to which the action should be dispatched. 37 | /// - action: A block to be executed when time is up. 38 | /// - Returns: The task just created. 39 | public func `do`( 40 | queue: DispatchQueue, 41 | action: @escaping () -> Void 42 | ) -> Task { 43 | return self.do(queue: queue, action: { (_) in action() }) 44 | } 45 | } 46 | 47 | extension Plan { 48 | 49 | /// Creates a plan whose `makeIterator()` method forwards to makeUnderlyingIterator. 50 | /// 51 | /// The task will be executed after each interval. 52 | /// 53 | /// For example: 54 | /// 55 | /// let plan = Plan.make { 56 | /// var i = 0 57 | /// return AnyIterator { 58 | /// i += 1 59 | /// return i // 1, 2, 3, ... 60 | /// } 61 | /// } 62 | /// plan.do { 63 | /// logTimestamp() 64 | /// } 65 | /// 66 | /// > "2001-01-01 00:00:00" 67 | /// > "2001-01-01 00:00:01" 68 | /// > "2001-01-01 00:00:03" 69 | /// > "2001-01-01 00:00:06" 70 | /// ... 71 | public static func make( 72 | _ makeUnderlyingIterator: @escaping () -> I 73 | ) -> Plan where I: IteratorProtocol, I.Element == Interval { 74 | return Plan(AnySequence(makeUnderlyingIterator)) 75 | } 76 | 77 | /// Creates a plan from a list of intervals. 78 | /// 79 | /// The task will be executed after each interval in the array. 80 | public static func of(_ intervals: Interval...) -> Plan { 81 | return Plan.of(intervals) 82 | } 83 | 84 | /// Creates a plan from a list of intervals. 85 | /// 86 | /// The task will be executed after each interval in the array. 87 | public static func of(_ intervals: S) -> Plan where S: Sequence, S.Element == Interval { 88 | return Plan(intervals) 89 | } 90 | } 91 | 92 | extension Plan { 93 | 94 | /// Creates a plan whose `makeIterator()` method forwards to makeUnderlyingIterator. 95 | /// 96 | /// The task will be executed at each date. 97 | /// 98 | /// For example: 99 | /// 100 | /// let plan = Plan.make { 101 | /// return AnyIterator { 102 | /// return Date().addingTimeInterval(3) 103 | /// } 104 | /// } 105 | /// 106 | /// plan.do { 107 | /// logTimestamp() 108 | /// } 109 | /// 110 | /// > "2001-01-01 00:00:00" 111 | /// > "2001-01-01 00:00:03" 112 | /// > "2001-01-01 00:00:06" 113 | /// > "2001-01-01 00:00:09" 114 | /// ... 115 | /// 116 | /// You should not return `Date()` in making iterator. 117 | /// If you want to execute a task immediately, use `Plan.now`. 118 | public static func make( 119 | _ makeUnderlyingIterator: @escaping () -> I 120 | ) -> Plan where I: IteratorProtocol, I.Element == Date { 121 | return Plan.make { () -> AnyIterator in 122 | var iterator = makeUnderlyingIterator() 123 | var prev: Date! 124 | return AnyIterator { 125 | prev = prev ?? Date() 126 | guard let next = iterator.next() else { return nil } 127 | defer { prev = next } 128 | return next.interval(since: prev) 129 | } 130 | } 131 | } 132 | 133 | /// Creates a plan from a list of dates. 134 | /// 135 | /// The task will be executed at each date in the array. 136 | public static func of(_ dates: Date...) -> Plan { 137 | return Plan.of(dates) 138 | } 139 | 140 | /// Creates a plan from a list of dates. 141 | /// 142 | /// The task will be executed at each date in the array. 143 | public static func of(_ sequence: S) -> Plan where S: Sequence, S.Element == Date { 144 | return Plan.make(sequence.makeIterator) 145 | } 146 | 147 | /// A dates sequence corresponding to this plan. 148 | public var dates: AnySequence { 149 | return AnySequence { () -> AnyIterator in 150 | let iterator = self.makeIterator() 151 | var prev: Date! 152 | return AnyIterator { 153 | prev = prev ?? Date() 154 | guard let interval = iterator.next() else { return nil } 155 | // swiftlint:disable shorthand_operator 156 | prev = prev + interval 157 | return prev 158 | } 159 | } 160 | } 161 | } 162 | 163 | extension Plan { 164 | 165 | /// A plan of a distant past date. 166 | public static var distantPast: Plan { 167 | return Plan.of(Date.distantPast) 168 | } 169 | 170 | /// A plan of a distant future date. 171 | public static var distantFuture: Plan { 172 | return Plan.of(Date.distantFuture) 173 | } 174 | 175 | /// A plan that will never happen. 176 | public static var never: Plan { 177 | return Plan.make { 178 | AnyIterator { nil } 179 | } 180 | } 181 | } 182 | 183 | extension Plan { 184 | 185 | /// Returns a new plan by concatenating the given plan to this plan. 186 | /// 187 | /// For example: 188 | /// 189 | /// let s0 = Plan.of(1.second, 2.seconds, 3.seconds) 190 | /// let s1 = Plan.of(4.seconds, 4.seconds, 4.seconds) 191 | /// let s2 = s0.concat(s1) 192 | /// 193 | /// > s2 194 | /// > 1.second, 2.seconds, 3.seconds, 4.seconds, 4.seconds, 4.seconds 195 | public func concat(_ plan: Plan) -> Plan { 196 | return Plan.make { () -> AnyIterator in 197 | let i0 = self.makeIterator() 198 | let i1 = plan.makeIterator() 199 | return AnyIterator { 200 | if let interval = i0.next() { return interval } 201 | return i1.next() 202 | } 203 | } 204 | } 205 | 206 | /// Returns a new plan by merging the given plan to this plan. 207 | /// 208 | /// For example: 209 | /// 210 | /// let s0 = Plan.of(1.second, 3.seconds, 5.seconds) 211 | /// let s1 = Plan.of(2.seconds, 4.seconds, 6.seconds) 212 | /// let s2 = s0.merge(s1) 213 | /// > s2 214 | /// > 1.second, 1.seconds, 2.seconds, 2.seconds, 3.seconds, 3.seconds 215 | public func merge(_ plan: Plan) -> Plan { 216 | return Plan.make { () -> AnyIterator in 217 | let i0 = self.dates.makeIterator() 218 | let i1 = plan.dates.makeIterator() 219 | 220 | var buf0: Date! 221 | var buf1: Date! 222 | 223 | return AnyIterator { 224 | if buf0 == nil { buf0 = i0.next() } 225 | if buf1 == nil { buf1 = i1.next() } 226 | 227 | var d: Date! 228 | if let d0 = buf0, let d1 = buf1 { 229 | d = Swift.min(d0, d1) 230 | } else { 231 | d = buf0 ?? buf1 232 | } 233 | 234 | if d == nil { return d } 235 | 236 | if d == buf0 { buf0 = nil; return d } 237 | if d == buf1 { buf1 = nil } 238 | return d 239 | } 240 | } 241 | } 242 | 243 | /// Returns a new plan by taking the first specific number of intervals from this plan. 244 | /// 245 | /// For example: 246 | /// 247 | /// let s0 = Plan.every(1.second) 248 | /// let s1 = s0.first(3) 249 | /// > s1 250 | /// 1.second, 1.second, 1.second 251 | public func first(_ count: Int) -> Plan { 252 | return Plan.make { () -> AnyIterator in 253 | let iterator = self.makeIterator() 254 | var num = 0 255 | return AnyIterator { 256 | guard num < count, let interval = iterator.next() else { return nil } 257 | num += 1 258 | return interval 259 | } 260 | } 261 | } 262 | 263 | /// Returns a new plan by taking the part before the given date. 264 | public func until(_ date: Date) -> Plan { 265 | return Plan.make { () -> AnyIterator in 266 | let iterator = self.dates.makeIterator() 267 | return AnyIterator { 268 | guard let next = iterator.next(), next < date else { 269 | return nil 270 | } 271 | return next 272 | } 273 | } 274 | } 275 | 276 | /// Creates a plan that executes the task immediately. 277 | public static var now: Plan { 278 | return Plan.of(0.nanosecond) 279 | } 280 | 281 | /// Creates a plan that executes the task after the given interval. 282 | public static func after(_ delay: Interval) -> Plan { 283 | return Plan.of(delay) 284 | } 285 | 286 | /// Creates a plan that executes the task after the given interval then repeat the execution. 287 | public static func after(_ delay: Interval, repeating interval: Interval) -> Plan { 288 | return Plan.after(delay).concat(Plan.every(interval)) 289 | } 290 | 291 | /// Creates a plan that executes the task at the given date. 292 | public static func at(_ date: Date) -> Plan { 293 | return Plan.of(date) 294 | } 295 | 296 | /// Creates a plan that executes the task every given interval. 297 | public static func every(_ interval: Interval) -> Plan { 298 | return Plan.make { 299 | AnyIterator { interval } 300 | } 301 | } 302 | 303 | /// Creates a plan that executes the task every given period. 304 | public static func every(_ period: Period) -> Plan { 305 | return Plan.make { () -> AnyIterator in 306 | let calendar = Calendar.gregorian 307 | var prev: Date! 308 | return AnyIterator { 309 | prev = prev ?? Date() 310 | guard 311 | let next = calendar.date( 312 | byAdding: period.asDateComponents(), 313 | to: prev) 314 | else { 315 | return nil 316 | } 317 | defer { prev = next } 318 | return next.interval(since: prev) 319 | } 320 | } 321 | } 322 | 323 | /// Creates a plan that executes the task every period. 324 | /// 325 | /// See Period's constructor: `init?(_ string: String)`. 326 | public static func every(_ period: String) -> Plan { 327 | guard let p = Period(period) else { 328 | return Plan.never 329 | } 330 | return Plan.every(p) 331 | } 332 | } 333 | 334 | extension Plan { 335 | 336 | /// `DateMiddleware` represents a middleware that wraps a plan 337 | /// which was only specified with date without time. 338 | /// 339 | /// You should call `at` method to specified time of the plan. 340 | public struct DateMiddleware { 341 | 342 | fileprivate let plan: Plan 343 | 344 | /// Creates a plan with time specified. 345 | public func at(_ time: Time) -> Plan { 346 | if plan.isNever() { return .never } 347 | 348 | var interval = time.intervalSinceStartOfDay 349 | return Plan.make { () -> AnyIterator in 350 | let it = self.plan.makeIterator() 351 | return AnyIterator { 352 | if let next = it.next() { 353 | defer { interval = 0.nanoseconds } 354 | return next + interval 355 | } 356 | return nil 357 | } 358 | } 359 | } 360 | 361 | /// Creates a plan with time specified. 362 | /// 363 | /// See Time's constructor: `init?(_ string: String)`. 364 | public func at(_ time: String) -> Plan { 365 | if plan.isNever() { return .never } 366 | guard let time = Time(time) else { 367 | return .never 368 | } 369 | return at(time) 370 | } 371 | 372 | /// Creates a plan with time specified. 373 | /// 374 | /// .at(1) => 01 375 | /// .at(1, 2) => 01:02 376 | /// .at(1, 2, 3) => 01:02:03 377 | /// .at(1, 2, 3, 456) => 01:02:03.456 378 | public func at(_ time: Int...) -> Plan { 379 | return self.at(time) 380 | } 381 | 382 | /// Creates a plan with time specified. 383 | /// 384 | /// .at([1]) => 01 385 | /// .at([1, 2]) => 01:02 386 | /// .at([1, 2, 3]) => 01:02:03 387 | /// .at([1, 2, 3, 456]) => 01:02:03.456 388 | public func at(_ time: [Int]) -> Plan { 389 | if plan.isNever() || time.isEmpty { return .never } 390 | 391 | let hour = time[0] 392 | let minute = time.count > 1 ? time[1] : 0 393 | let second = time.count > 2 ? time[2] : 0 394 | let nanosecond = time.count > 3 ? time[3]: 0 395 | 396 | guard let time = Time( 397 | hour: hour, 398 | minute: minute, 399 | second: second, 400 | nanosecond: nanosecond 401 | ) else { 402 | return Plan.never 403 | } 404 | return at(time) 405 | } 406 | } 407 | 408 | /// Creates a date middleware that executes the task on every specific week day. 409 | public static func every(_ weekday: Weekday) -> DateMiddleware { 410 | let plan = Plan.make { () -> AnyIterator in 411 | let calendar = Calendar.gregorian 412 | var date: Date? 413 | return AnyIterator { 414 | if let d = date { 415 | date = calendar.date(byAdding: .day, value: 7, to: d) 416 | } else if Date().is(weekday) { 417 | date = Date().startOfToday 418 | } else { 419 | let components = weekday.asDateComponents() 420 | date = calendar.nextDate(after: Date(), matching: components, matchingPolicy: .strict) 421 | } 422 | return date 423 | } 424 | } 425 | return DateMiddleware(plan: plan) 426 | } 427 | 428 | /// Creates a date middleware that executes the task on every specific week day. 429 | public static func every(_ weekdays: Weekday...) -> DateMiddleware { 430 | return Plan.every(weekdays) 431 | } 432 | 433 | /// Creates a date middleware that executes the task on every specific week day. 434 | public static func every(_ weekdays: [Weekday]) -> DateMiddleware { 435 | guard !weekdays.isEmpty else { return .init(plan: .never) } 436 | 437 | var plan = every(weekdays[0]).plan 438 | for weekday in weekdays.dropFirst() { 439 | plan = plan.merge(Plan.every(weekday).plan) 440 | } 441 | return DateMiddleware(plan: plan) 442 | } 443 | 444 | /// Creates a date middleware that executes the task on every specific month day. 445 | public static func every(_ monthday: Monthday) -> DateMiddleware { 446 | let plan = Plan.make { () -> AnyIterator in 447 | let calendar = Calendar.gregorian 448 | var date: Date? 449 | return AnyIterator { 450 | if let d = date { 451 | date = calendar.date(byAdding: .year, value: 1, to: d) 452 | } else if Date().is(monthday) { 453 | date = Date().startOfToday 454 | } else { 455 | let components = monthday.asDateComponents() 456 | date = calendar.nextDate(after: Date(), matching: components, matchingPolicy: .strict) 457 | } 458 | return date 459 | } 460 | } 461 | return DateMiddleware(plan: plan) 462 | } 463 | 464 | /// Creates a date middleware that executes the task on every specific month day. 465 | public static func every(_ mondays: Monthday...) -> DateMiddleware { 466 | return Plan.every(mondays) 467 | } 468 | 469 | /// Creates a date middleware that executes the task on every specific month day. 470 | public static func every(_ mondays: [Monthday]) -> DateMiddleware { 471 | guard !mondays.isEmpty else { return .init(plan: .never) } 472 | 473 | var plan = every(mondays[0]).plan 474 | for monday in mondays.dropFirst() { 475 | plan = plan.merge(Plan.every(monday).plan) 476 | } 477 | return DateMiddleware(plan: plan) 478 | } 479 | } 480 | 481 | extension Plan { 482 | 483 | /// Returns a Boolean value indicating whether this plan is empty. 484 | public func isNever() -> Bool { 485 | return seq.makeIterator().next() == nil 486 | } 487 | } 488 | 489 | extension Plan { 490 | 491 | /// Creates a new plan that is offset by the specified interval in the 492 | /// closure body. 493 | /// 494 | /// The closure is evaluated each time the next-run date is evaluated, 495 | /// so the interval can be calculated based on dynamic factors. 496 | /// 497 | /// If the returned interval offset is `nil`, then no offset is added 498 | /// to that next-run date. 499 | public func offset(by interval: @autoclosure @escaping () -> Interval?) -> Plan { 500 | return Plan.make { () -> AnyIterator in 501 | let it = self.makeIterator() 502 | return AnyIterator { 503 | if let next = it.next() { 504 | return next + (interval() ?? 0.second) 505 | } 506 | return nil 507 | } 508 | } 509 | } 510 | } 511 | -------------------------------------------------------------------------------- /Sources/Schedule/RunLoopTask.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Plan { 4 | 5 | /// Schedules a task with this plan. 6 | /// 7 | /// When time is up, the task will be executed on current thread. It behaves 8 | /// like a `Timer`, so you need to make sure that the current thread has a 9 | /// available runloop. 10 | /// 11 | /// Since this method relies on run loop, it is remove recommended to use 12 | /// `do(queue: _, onElapse: _)`. 13 | /// 14 | /// - Parameters: 15 | /// - mode: The mode to which the action should be added. 16 | /// - action: A block to be executed when time is up. 17 | /// - Returns: The task just created. 18 | public func `do`( 19 | mode: RunLoop.Mode = .common, 20 | action: @escaping (Task) -> Void 21 | ) -> Task { 22 | return RunLoopTask(plan: self, mode: mode, action: action) 23 | } 24 | 25 | /// Schedules a task with this plan. 26 | /// 27 | /// When time is up, the task will be executed on current thread. It behaves 28 | /// like a `Timer`, so you need to make sure that the current thread has a 29 | /// available runloop. 30 | /// 31 | /// Since this method relies on run loop, it is remove recommended to use 32 | /// `do(queue: _, onElapse: _)`. 33 | /// 34 | /// - Parameters: 35 | /// - mode: The mode to which the action should be added. 36 | /// - action: A block to be executed when time is up. 37 | /// - Returns: The task just created. 38 | public func `do`( 39 | mode: RunLoop.Mode = .common, 40 | action: @escaping () -> Void 41 | ) -> Task { 42 | return self.do(mode: mode) { _ in 43 | action() 44 | } 45 | } 46 | } 47 | 48 | private final class RunLoopTask: Task { 49 | 50 | var timer: Timer! 51 | 52 | init( 53 | plan: Plan, 54 | mode: RunLoop.Mode, 55 | action: @escaping (Task) -> Void 56 | ) { 57 | super.init(plan: plan, queue: nil) { (task) in 58 | guard let task = task as? RunLoopTask, let timer = task.timer else { return } 59 | timer.fireDate = Date() 60 | } 61 | 62 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 63 | 64 | timer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, Date.distantFuture.timeIntervalSinceReferenceDate, .greatestFiniteMagnitude, 0, 0, { [weak self] _ in 65 | guard let self = self else { return } 66 | action(self) 67 | }) 68 | 69 | #elseif os(Linux) 70 | 71 | timer = Timer(fire: Date.distantFuture, interval: .greatestFiniteMagnitude, repeats: true) { [weak self] _ in 72 | guard let self = self else { return } 73 | action(self) 74 | } 75 | 76 | #endif 77 | 78 | RunLoop.current.add(timer, forMode: mode) 79 | } 80 | 81 | deinit { 82 | timer.invalidate() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/Schedule/Task.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `ActionKey` represents a token that can be used to remove the action. 4 | public struct ActionKey { 5 | 6 | fileprivate let bagKey: BagKey 7 | } 8 | 9 | extension BagKey { 10 | 11 | fileprivate func asActionKey() -> ActionKey { 12 | return ActionKey(bagKey: self) 13 | } 14 | } 15 | 16 | /// `Task` represents a timing task. 17 | open class Task { 18 | 19 | // MARK: - Private properties 20 | 21 | private let _lock = NSLock() 22 | 23 | private var _iterator: AnyIterator 24 | private let _timer: DispatchSourceTimer 25 | 26 | private var _actions = Bag() 27 | 28 | private var _suspensionCount = 0 29 | private var _executionCount = 0 30 | 31 | private var _executionDates: [Date]? 32 | private var _estimatedNextExecutionDate: Date? 33 | 34 | private var _taskCenter: TaskCenter? 35 | private var _tags: Set = [] 36 | 37 | // MARK: - Public properties 38 | 39 | /// The unique id of this task. 40 | public let id = UUID() 41 | 42 | public typealias Action = (Task) -> Void 43 | 44 | /// The date of creation. 45 | public let creationDate = Date() 46 | 47 | /// The date of first execution. 48 | open var firstExecutionDate: Date? { 49 | return _lock.withLock { _executionDates?.first } 50 | } 51 | 52 | /// The date of last execution. 53 | open var lastExecutionDate: Date? { 54 | return _lock.withLock { _executionDates?.last } 55 | } 56 | 57 | /// Histories of executions. 58 | open var executionDates: [Date]? { 59 | return _lock.withLock { _executionDates } 60 | } 61 | 62 | /// The date of estimated next execution. 63 | open var estimatedNextExecutionDate: Date? { 64 | return _lock.withLock { _estimatedNextExecutionDate } 65 | } 66 | 67 | /// The number of task executions. 68 | public var executionCount: Int { 69 | return _lock.withLock { 70 | _executionCount 71 | } 72 | } 73 | 74 | /// The number of task suspensions. 75 | public var suspensionCount: Int { 76 | return _lock.withLock { 77 | _suspensionCount 78 | } 79 | } 80 | 81 | /// The number of actions in this task. 82 | public var actionCount: Int { 83 | return _lock.withLock { 84 | _actions.count 85 | } 86 | } 87 | 88 | /// A Boolean indicating whether the task was canceled. 89 | public var isCancelled: Bool { 90 | return _lock.withLock { 91 | _timer.isCancelled 92 | } 93 | } 94 | 95 | /// The task center to which this task currently belongs. 96 | open var taskCenter: TaskCenter? { 97 | return _lock.withLock { _taskCenter } 98 | } 99 | 100 | 101 | // MARK: - Init 102 | 103 | /// Initializes a timing task. 104 | /// 105 | /// - Parameters: 106 | /// - plan: The plan. 107 | /// - queue: The dispatch queue to which the action should be dispatched. 108 | /// - action: A block to be executed when time is up. 109 | init( 110 | plan: Plan, 111 | queue: DispatchQueue?, 112 | action: @escaping (Task) -> Void 113 | ) { 114 | _iterator = plan.makeIterator() 115 | _timer = DispatchSource.makeTimerSource(queue: queue) 116 | 117 | _actions.append(action) 118 | 119 | _timer.setEventHandler { [weak self] in 120 | guard let self = self else { return } 121 | self.elapse() 122 | } 123 | 124 | if let interval = _iterator.next(), !interval.isNegative { 125 | _timer.schedule(after: interval) 126 | _estimatedNextExecutionDate = Date().adding(interval) 127 | } 128 | 129 | _timer.resume() 130 | 131 | TaskCenter.default.add(self) 132 | } 133 | 134 | deinit { 135 | while _suspensionCount > 0 { 136 | _timer.resume() 137 | _suspensionCount -= 1 138 | } 139 | 140 | self.removeFromTaskCenter() 141 | } 142 | 143 | private func elapse() { 144 | scheduleNextExecution() 145 | executeNow() 146 | } 147 | 148 | private func scheduleNextExecution() { 149 | _lock.withLockVoid { 150 | let now = Date() 151 | var estimated = _estimatedNextExecutionDate ?? now 152 | repeat { 153 | guard let interval = _iterator.next(), !interval.isNegative else { 154 | _estimatedNextExecutionDate = nil 155 | return 156 | } 157 | estimated = estimated.adding(interval) 158 | } while (estimated < now) 159 | 160 | _estimatedNextExecutionDate = estimated 161 | _timer.schedule(after: _estimatedNextExecutionDate!.interval(since: now)) 162 | } 163 | } 164 | 165 | /// Execute this task now, without interrupting its plan. 166 | open func executeNow() { 167 | let actions = _lock.withLock { () -> Bag in 168 | let now = Date() 169 | if _executionDates == nil { 170 | _executionDates = [now] 171 | } else { 172 | _executionDates?.append(now) 173 | } 174 | _executionCount += 1 175 | return _actions 176 | } 177 | actions.forEach { $0(self) } 178 | } 179 | 180 | // MARK: - Features 181 | 182 | /// Reschedules this task with the new plan. 183 | public func reschedule(_ new: Plan) { 184 | _lock.lock() 185 | if _timer.isCancelled { 186 | _lock.unlock() 187 | return 188 | } 189 | 190 | _iterator = new.makeIterator() 191 | _lock.unlock() 192 | scheduleNextExecution() 193 | } 194 | 195 | /// Suspends this task. 196 | public func suspend() { 197 | _lock.withLockVoid { 198 | if _timer.isCancelled { return } 199 | 200 | if _suspensionCount < UInt64.max { 201 | _timer.suspend() 202 | _suspensionCount += 1 203 | } 204 | } 205 | } 206 | 207 | /// Resumes this task. 208 | public func resume() { 209 | _lock.withLockVoid { 210 | if _timer.isCancelled { return } 211 | 212 | if _suspensionCount > 0 { 213 | _timer.resume() 214 | _suspensionCount -= 1 215 | } 216 | } 217 | } 218 | 219 | /// Cancels this task. 220 | public func cancel() { 221 | _lock.withLockVoid { 222 | _timer.cancel() 223 | _suspensionCount = 0 224 | } 225 | } 226 | 227 | /// Adds action to this task. 228 | @discardableResult 229 | public func addAction(_ action: @escaping (Task) -> Void) -> ActionKey { 230 | return _lock.withLock { 231 | return _actions.append(action).asActionKey() 232 | } 233 | } 234 | 235 | /// Removes action by key from this task. 236 | public func removeAction(byKey key: ActionKey) { 237 | _lock.withLockVoid { 238 | _ = _actions.removeValue(for: key.bagKey) 239 | } 240 | } 241 | 242 | /// Removes all actions from this task. 243 | public func removeAllActions() { 244 | _lock.withLockVoid { 245 | _actions.removeAll() 246 | } 247 | } 248 | 249 | /// Adds this task to the given task center. 250 | func addToTaskCenter(_ center: TaskCenter) { 251 | _lock.lock(); defer { _lock.unlock() } 252 | 253 | if _taskCenter === center { return } 254 | 255 | let c = _taskCenter 256 | _taskCenter = center 257 | c?.removeSimply(self) 258 | center.addSimply(self) 259 | } 260 | 261 | /// Removes this task from the given task center. 262 | public func removeFromTaskCenter() { 263 | _lock.lock(); defer { _lock.unlock() } 264 | 265 | guard let center = self._taskCenter else { 266 | return 267 | } 268 | _taskCenter = nil 269 | center.removeSimply(self) 270 | } 271 | } 272 | 273 | extension Task: Hashable { 274 | 275 | /// Hashes the essential components of this value by feeding them into the given hasher. 276 | public func hash(into hasher: inout Hasher) { 277 | hasher.combine(id) 278 | } 279 | 280 | /// Returns a boolean value indicating whether two tasks are equal. 281 | public static func == (lhs: Task, rhs: Task) -> Bool { 282 | return lhs.id == rhs.id 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /Sources/Schedule/TaskCenter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension TaskCenter { 4 | 5 | private class TaskBox: Hashable { 6 | 7 | weak var task: Task? 8 | 9 | // Used to find slot in dictionary/set 10 | let hash: Int 11 | 12 | init(_ task: Task) { 13 | self.task = task 14 | self.hash = task.hashValue 15 | } 16 | 17 | func hash(into hasher: inout Hasher) { 18 | hasher.combine(hash) 19 | } 20 | 21 | // Used to find task in a slot in dictionary/set 22 | static func == (lhs: TaskBox, rhs: TaskBox) -> Bool { 23 | return lhs.task == rhs.task 24 | } 25 | } 26 | } 27 | 28 | private let _default = TaskCenter() 29 | 30 | /// A task center that enables batch operation. 31 | open class TaskCenter { 32 | 33 | private let lock = NSLock() 34 | 35 | private var tags: [String: Set] = [:] 36 | private var tasks: [TaskBox: Set] = [:] 37 | 38 | /// Default task center. 39 | open class var `default`: TaskCenter { 40 | return _default 41 | } 42 | 43 | public init() { } 44 | 45 | /// Adds the given task to this center. 46 | /// 47 | /// Please note: task center will not retain tasks. 48 | open func add(_ task: Task) { 49 | task.addToTaskCenter(self) 50 | } 51 | 52 | func addSimply(_ task: Task) { 53 | lock.withLockVoid { 54 | let box = TaskBox(task) 55 | self.tasks[box] = [] 56 | } 57 | } 58 | 59 | func removeSimply(_ task: Task) { 60 | lock.withLockVoid { 61 | let box = TaskBox(task) 62 | guard let tags = self.tasks[box] else { 63 | return 64 | } 65 | 66 | self.tasks[box] = nil 67 | for tag in tags { 68 | self.tags[tag]?.remove(box) 69 | if self.tags[tag]?.count == 0 { 70 | self.tags[tag] = nil 71 | } 72 | } 73 | } 74 | } 75 | 76 | /// Adds a tag to the task. 77 | /// 78 | /// If the task is not in this center, do nothing. 79 | open func addTag(_ tag: String, to task: Task) { 80 | addTags([tag], to: task) 81 | } 82 | 83 | /// Adds tags to the task. 84 | /// 85 | /// If the task is not in this center, do nothing. 86 | open func addTags(_ tags: [String], to task: Task) { 87 | lock.withLockVoid { 88 | let box = TaskBox(task) 89 | guard self.tasks[box] != nil else { 90 | return 91 | } 92 | 93 | for tag in tags { 94 | self.tasks[box]?.insert(tag) 95 | if self.tags[tag] == nil { 96 | self.tags[tag] = [] 97 | } 98 | self.tags[tag]?.insert(box) 99 | } 100 | } 101 | } 102 | 103 | /// Removes a tag from the task. 104 | /// 105 | /// If the task is not in this center, do nothing. 106 | open func removeTag(_ tag: String, from task: Task) { 107 | removeTags([tag], from: task) 108 | } 109 | 110 | /// Removes tags from the task. 111 | /// 112 | /// If the task is not in this center, do nothing. 113 | open func removeTags(_ tags: [String], from task: Task) { 114 | lock.withLockVoid { 115 | let box = TaskBox(task) 116 | guard self.tasks[box] != nil else { 117 | return 118 | } 119 | 120 | for tag in tags { 121 | self.tasks[box]?.remove(tag) 122 | self.tags[tag]?.remove(box) 123 | if self.tags[tag]?.count == 0 { 124 | self.tags[tag] = nil 125 | } 126 | } 127 | } 128 | } 129 | 130 | /// Returns all tags for the task. 131 | /// 132 | /// If the task is not in this center, return an empty array. 133 | open func tags(forTask task: Task) -> [String] { 134 | return lock.withLock { 135 | Array(tasks[TaskBox(task)] ?? []) 136 | } 137 | } 138 | 139 | /// Returns all tasks for the tag. 140 | open func tasks(forTag tag: String) -> [Task] { 141 | return lock.withLock { 142 | tags[tag]?.compactMap { $0.task } ?? [] 143 | } 144 | } 145 | 146 | /// Returns all tasks in this center. 147 | open var allTasks: [Task] { 148 | return lock.withLock { 149 | tasks.compactMap { $0.key.task } 150 | } 151 | } 152 | 153 | /// Returns all tags in this center. 154 | open var allTags: [String] { 155 | return lock.withLock { 156 | tags.map { $0.key } 157 | } 158 | } 159 | 160 | /// Removes all tasks from this center. 161 | open func removeAll() { 162 | allTasks.forEach { 163 | $0.removeFromTaskCenter() 164 | } 165 | } 166 | 167 | /// Suspends all tasks by tag. 168 | open func suspend(byTag tag: String) { 169 | tasks(forTag: tag).forEach { $0.suspend() } 170 | } 171 | 172 | /// Resumes all tasks by tag. 173 | open func resume(byTag tag: String) { 174 | tasks(forTag: tag).forEach { $0.resume() } 175 | } 176 | 177 | /// Cancels all tasks by tag. 178 | open func cancel(byTag tag: String) { 179 | tasks(forTag: tag).forEach { $0.cancel() } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /Sources/Schedule/Time.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `Time` represents a time without a date. 4 | public struct Time { 5 | 6 | /// Hour of day. 7 | public let hour: Int 8 | 9 | /// Minute of hour. 10 | public let minute: Int 11 | 12 | /// Second of minute. 13 | public let second: Int 14 | 15 | /// Nanosecond of second. 16 | public let nanosecond: Int 17 | 18 | /// Initializes a time with `hour`, `minute`, `second` and `nanosecond`. 19 | /// 20 | /// Time(hour: 11, minute: 11) => "11:11:00.000" 21 | /// Time(hour: 25) => nil 22 | /// Time(hour: 1, minute: 61) => nil 23 | public init?(hour: Int, minute: Int = 0, second: Int = 0, nanosecond: Int = 0) { 24 | guard 25 | (0..<24).contains(hour), 26 | (0..<60).contains(minute), 27 | (0..<60).contains(second), 28 | (0.. Time(hour: 11) 40 | /// Time("11:12") -> Time(hour: 11, minute: 12) 41 | /// Time("11:12:13") -> Time(hour: 11, minute: 12, second: 13) 42 | /// Time("11:12:13.123") -> Time(hour: 11, minute: 12, second: 13, nanosecond: 123000000) 43 | /// 44 | /// Time("-1.0") == nil 45 | /// 46 | /// Time("11 pm") == Time(hour: 23) 47 | /// Time("11:12:13 PM") == Time(hour: 23, minute: 12, second: 13) 48 | public init?(_ string: String) { 49 | let pattern = "^(\\d{1,2})(:(\\d{1,2})(:(\\d{1,2})(.(\\d{1,3}))?)?)?( (am|AM|pm|PM))?$" 50 | 51 | // swiftlint:disable force_try 52 | let regexp = try! NSRegularExpression(pattern: pattern, options: []) 53 | let nsString = NSString(string: string) 54 | guard let matches = regexp.matches( 55 | in: string, 56 | options: [], 57 | range: NSRange(location: 0, length: nsString.length)).first 58 | else { 59 | return nil 60 | } 61 | 62 | var hasAM = false 63 | var hasPM = false 64 | var values: [Int] = [] 65 | values.reserveCapacity(matches.numberOfRanges) 66 | 67 | for i in 0.. 0 else { return nil } 78 | 79 | if hasAM && values[0] == 12 { values[0] = 0 } 80 | if hasPM && values[0] < 12 { values[0] += 12 } 81 | switch values.count { 82 | case 1: self.init(hour: values[0]) 83 | case 2: self.init(hour: values[0], minute: values[1]) 84 | case 3: self.init(hour: values[0], minute: values[1], second: values[2]) 85 | case 4: 86 | let ns = Double("0.\(values[3])")?.second.nanoseconds 87 | self.init(hour: values[0], minute: values[1], second: values[2], nanosecond: Int(ns ?? 0)) 88 | default: return nil 89 | } 90 | } 91 | 92 | /// The interval between this time and start of today 93 | public var intervalSinceStartOfDay: Interval { 94 | return hour.hours + minute.minutes + second.seconds + nanosecond.nanoseconds 95 | } 96 | 97 | /// Returns a dateComponenets of the time, using gregorian calender and 98 | /// current time zone. 99 | public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents { 100 | return DateComponents(calendar: Calendar.gregorian, 101 | timeZone: timeZone, 102 | hour: hour, 103 | minute: minute, 104 | second: second, 105 | nanosecond: nanosecond) 106 | } 107 | } 108 | 109 | extension Time: CustomStringConvertible { 110 | 111 | /// A textual representation of this time. 112 | /// 113 | /// "Time: 11:11:11.111" 114 | public var description: String { 115 | let h = "\(hour)".padding(toLength: 2, withPad: "0", startingAt: 0) 116 | let m = "\(minute)".padding(toLength: 2, withPad: "0", startingAt: 0) 117 | let s = "\(second)".padding(toLength: 2, withPad: "0", startingAt: 0) 118 | let ns = "\(nanosecond / 1_000_000)".padding(toLength: 3, withPad: "0", startingAt: 0) 119 | return "Time: \(h):\(m):\(s).\(ns)" 120 | } 121 | } 122 | 123 | extension Time: CustomDebugStringConvertible { 124 | 125 | /// A textual representation of this time for debugging. 126 | /// 127 | /// "Time: 11:11:11.111" 128 | public var debugDescription: String { 129 | return description 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Sources/Schedule/Weekday.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `Weekday` represents a day of a week. 4 | public enum Weekday: Int { 5 | 6 | case sunday = 1, monday, tuesday, wednesday, thursday, friday, saturday 7 | 8 | /// Returns dateComponenets of the weekday, using gregorian calender and 9 | /// current time zone. 10 | public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents { 11 | return DateComponents( 12 | calendar: Calendar.gregorian, 13 | timeZone: timeZone, 14 | weekday: rawValue) 15 | } 16 | } 17 | 18 | extension Date { 19 | 20 | /// Returns a Boolean value indicating whether this date is the weekday in current time zone. 21 | public func `is`(_ weekday: Weekday, in timeZone: TimeZone = .current) -> Bool { 22 | var cal = Calendar.gregorian 23 | cal.timeZone = timeZone 24 | return cal.component(.weekday, from: self) == weekday.rawValue 25 | } 26 | } 27 | 28 | extension Weekday: CustomStringConvertible { 29 | 30 | /// A textual representation of this weekday. 31 | /// 32 | /// "Weekday: Friday" 33 | public var description: String { 34 | return "Weekday: \(Calendar.gregorian.weekdaySymbols[rawValue - 1])" 35 | } 36 | } 37 | 38 | extension Weekday: CustomDebugStringConvertible { 39 | 40 | /// A textual representation of this weekday for debugging. 41 | /// 42 | /// "Weekday: Friday" 43 | public var debugDescription: String { 44 | return description 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import ScheduleTests 3 | 4 | var tests = [XCTestCaseEntry]() 5 | tests += ScheduleTests.allTests() 6 | XCTMain(tests) 7 | -------------------------------------------------------------------------------- /Tests/ScheduleTests/AtomicTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Schedule 3 | 4 | final class AtomicTests: XCTestCase { 5 | 6 | func testRead() { 7 | let i = Atomic(1) 8 | let val = i.read { $0 } 9 | XCTAssertEqual(val, 1) 10 | } 11 | 12 | func testReadVoid() { 13 | let i = Atomic(1) 14 | var val = 0 15 | i.readVoid { val = $0 } 16 | XCTAssertEqual(val, 1) 17 | } 18 | 19 | func testWrite() { 20 | let i = Atomic(1) 21 | let val = i.write { v -> Int in 22 | v += 1 23 | return v 24 | } 25 | XCTAssertEqual(i.read { $0 }, val) 26 | } 27 | 28 | func testWriteVoid() { 29 | let i = Atomic(1) 30 | var val = 0 31 | i.writeVoid { 32 | $0 += 1 33 | val = $0 34 | } 35 | XCTAssertEqual(i.read { $0 }, val) 36 | } 37 | 38 | static var allTests = [ 39 | ("testRead", testRead), 40 | ("testReadVoid", testReadVoid), 41 | ("testWrite", testWrite), 42 | ("testWriteVoid", testWriteVoid) 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /Tests/ScheduleTests/BagTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Schedule 3 | 4 | final class BagTests: XCTestCase { 5 | 6 | typealias Fn = () -> Int 7 | 8 | func testBagKey() { 9 | var g = BagKey.Gen() 10 | let k1 = g.next() 11 | let k2 = g.next() 12 | XCTAssertNotNil(k1) 13 | XCTAssertNotNil(k2) 14 | XCTAssertNotEqual(k1, k2) 15 | } 16 | 17 | func testAppend() { 18 | var bag = Bag() 19 | bag.append { 1 } 20 | bag.append { 2 } 21 | 22 | XCTAssertEqual(bag.count, 2) 23 | } 24 | 25 | func testValueForKey() { 26 | var bag = Bag() 27 | let k1 = bag.append { 1 } 28 | let k2 = bag.append { 2 } 29 | 30 | let fn1 = bag.value(for: k1) 31 | XCTAssertNotNil(fn1) 32 | 33 | let fn2 = bag.value(for: k2) 34 | XCTAssertNotNil(fn2) 35 | 36 | guard let _fn1 = fn1, let _fn2 = fn2 else { return } 37 | 38 | XCTAssertEqual(_fn1(), 1) 39 | XCTAssertEqual(_fn2(), 2) 40 | } 41 | 42 | func testRemoveValueForKey() { 43 | var bag = Bag() 44 | 45 | let k1 = bag.append { 1 } 46 | let k2 = bag.append { 2 } 47 | 48 | let fn1 = bag.removeValue(for: k1) 49 | XCTAssertNotNil(fn1) 50 | XCTAssertNil(bag.value(for: k1)) 51 | 52 | let fn2 = bag.removeValue(for: k2) 53 | XCTAssertNotNil(fn2) 54 | XCTAssertNil(bag.removeValue(for: k2)) 55 | 56 | guard let _fn1 = fn1, let _fn2 = fn2 else { return } 57 | 58 | XCTAssertEqual(_fn1(), 1) 59 | XCTAssertEqual(_fn2(), 2) 60 | } 61 | 62 | func testCount() { 63 | var bag = Bag() 64 | 65 | let k1 = bag.append { 1 } 66 | let k2 = bag.append { 2 } 67 | 68 | XCTAssertEqual(bag.count, 2) 69 | 70 | bag.removeValue(for: k1) 71 | bag.removeValue(for: k2) 72 | 73 | XCTAssertEqual(bag.count, 0) 74 | } 75 | 76 | func testRemoveAll() { 77 | var bag = Bag() 78 | 79 | bag.append { 1 } 80 | bag.append { 2 } 81 | 82 | bag.removeAll() 83 | XCTAssertEqual(bag.count, 0) 84 | } 85 | 86 | func testSequence() { 87 | var bag = Bag() 88 | bag.append { 0 } 89 | bag.append { 1 } 90 | bag.append { 2 } 91 | 92 | var i = 0 93 | for fn in bag { 94 | XCTAssertEqual(fn(), i) 95 | i += 1 96 | } 97 | } 98 | 99 | static var allTests = [ 100 | ("testBagKey", testBagKey), 101 | ("testAppend", testAppend), 102 | ("testValueForKey", testValueForKey), 103 | ("testRemoveValueForKey", testRemoveValueForKey), 104 | ("testCount", testCount), 105 | ("testRemoveAll", testRemoveAll), 106 | ("testSequence", testSequence) 107 | ] 108 | } 109 | -------------------------------------------------------------------------------- /Tests/ScheduleTests/ExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Schedule 3 | 4 | final class ExtensionsTests: XCTestCase { 5 | 6 | func testClampedToInt() { 7 | let a = Double(Int.max) + 1 8 | XCTAssertEqual(a.clampedToInt(), Int.max) 9 | 10 | let b = Double(Int.min) - 1 11 | XCTAssertEqual(b.clampedToInt(), Int.min) 12 | } 13 | 14 | func testClampedAdding() { 15 | let i = Int.max 16 | XCTAssertEqual(i.clampedAdding(1), Int.max) 17 | } 18 | 19 | func testStartOfToday() { 20 | let components = Date().startOfToday.dateComponents 21 | 22 | let h = components.hour 23 | let m = components.minute 24 | let s = components.second 25 | XCTAssertNotNil(h) 26 | XCTAssertNotNil(m) 27 | XCTAssertNotNil(s) 28 | 29 | XCTAssertEqual(h, 0) 30 | XCTAssertEqual(m, 0) 31 | XCTAssertEqual(s, 0) 32 | } 33 | 34 | static var allTests = [ 35 | ("testClampedToInt", testClampedToInt), 36 | ("testClampedAdding", testClampedAdding), 37 | ("testStartOfToday", testStartOfToday) 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /Tests/ScheduleTests/Helpers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import Schedule 3 | 4 | extension Date { 5 | 6 | var dateComponents: DateComponents { 7 | return Calendar.gregorian.dateComponents(in: TimeZone.current, from: self) 8 | } 9 | 10 | init( 11 | year: Int, month: Int, day: Int, 12 | hour: Int = 0, minute: Int = 0, second: Int = 0, 13 | nanosecond: Int = 0 14 | ) { 15 | let components = DateComponents( 16 | calendar: Calendar.gregorian, 17 | timeZone: TimeZone.current, 18 | year: year, month: month, day: day, 19 | hour: hour, minute: minute, second: second, 20 | nanosecond: nanosecond 21 | ) 22 | self = components.date ?? Date.distantPast 23 | } 24 | } 25 | 26 | extension Interval { 27 | 28 | func isAlmostEqual(to interval: Interval, leeway: Interval) -> Bool { 29 | return (interval - self).abs <= leeway.abs 30 | } 31 | } 32 | 33 | extension Double { 34 | 35 | func isAlmostEqual(to double: Double, leeway: Double) -> Bool { 36 | return (double - self).magnitude <= leeway 37 | } 38 | } 39 | 40 | extension Sequence where Element == Interval { 41 | 42 | func isAlmostEqual(to sequence: S, leeway: Interval) -> Bool where S: Sequence, S.Element == Element { 43 | var it0 = self.makeIterator() 44 | var it1 = sequence.makeIterator() 45 | 46 | while let l = it0.next(), let r = it1.next() { 47 | if l.isAlmostEqual(to: r, leeway: leeway) { 48 | continue 49 | } else { 50 | return false 51 | } 52 | } 53 | return it0.next() == it1.next() 54 | } 55 | } 56 | 57 | extension Plan { 58 | 59 | func isAlmostEqual(to plan: Plan, leeway: Interval) -> Bool { 60 | return makeIterator().isAlmostEqual(to: plan.makeIterator(), leeway: leeway) 61 | } 62 | } 63 | 64 | extension DispatchQueue { 65 | 66 | func async(after interval: Interval, execute body: @escaping () -> Void) { 67 | asyncAfter(wallDeadline: .now() + interval.asSeconds(), execute: body) 68 | } 69 | 70 | static func `is`(_ queue: DispatchQueue) -> Bool { 71 | let key = DispatchSpecificKey<()>() 72 | 73 | queue.setSpecific(key: key, value: ()) 74 | defer { queue.setSpecific(key: key, value: nil) } 75 | 76 | return DispatchQueue.getSpecific(key: key) != nil 77 | } 78 | } 79 | 80 | extension TimeZone { 81 | 82 | static let shanghai = TimeZone(identifier: "Asia/Shanghai")! 83 | } 84 | -------------------------------------------------------------------------------- /Tests/ScheduleTests/IntervalTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntervalTests.swift 3 | // ScheduleTests 4 | // 5 | // Created by Quentin MED on 2019/4/4. 6 | // 7 | 8 | import XCTest 9 | @testable import Schedule 10 | 11 | final class IntervalTests: XCTestCase { 12 | 13 | private let leeway = 0.01.second 14 | 15 | func testEquatable() { 16 | XCTAssertEqual(1.second, 1.second) 17 | XCTAssertEqual(1.week, 7.days) 18 | } 19 | 20 | func testIsNegative() { 21 | XCTAssertFalse(1.second.isNegative) 22 | XCTAssertTrue((-1).second.isNegative) 23 | } 24 | 25 | func testAbs() { 26 | XCTAssertEqual(1.second, (-1).second.abs) 27 | } 28 | 29 | func testNegated() { 30 | XCTAssertEqual(1.second.negated, (-1).second) 31 | XCTAssertEqual(1.second.negated.negated, 1.second) 32 | } 33 | 34 | func testCompare() { 35 | XCTAssertEqual((-1).second.compare(1.second), ComparisonResult.orderedAscending) 36 | XCTAssertEqual(8.days.compare(1.week), ComparisonResult.orderedDescending) 37 | XCTAssertEqual(1.day.compare(24.hours), ComparisonResult.orderedSame) 38 | 39 | XCTAssertTrue(23.hours < 1.day) 40 | XCTAssertTrue(25.hours > 1.day) 41 | } 42 | 43 | func testLongerShorter() { 44 | XCTAssertTrue((-25).hour.isLonger(than: 1.day)) 45 | XCTAssertTrue(1.week.isShorter(than: 8.days)) 46 | } 47 | 48 | func testMultiplying() { 49 | XCTAssertEqual(7.days * 2, 2.week) 50 | } 51 | 52 | func testAdding() { 53 | XCTAssertEqual(6.days + 1.day, 1.week) 54 | 55 | XCTAssertEqual(1.1.weeks, 1.week + 0.1.weeks) 56 | } 57 | 58 | func testOperators() { 59 | XCTAssertEqual(1.week - 6.days, 1.day) 60 | 61 | var i = 6.days 62 | i += 1.day 63 | XCTAssertEqual(i, 1.week) 64 | 65 | XCTAssertEqual(-(7.days), (-1).week) 66 | } 67 | 68 | func testAs() { 69 | XCTAssertEqual(1.millisecond.asNanoseconds(), 1.microsecond.asNanoseconds() * pow(10, 3)) 70 | 71 | XCTAssertEqual(1.second.asNanoseconds(), pow(10, 9)) 72 | XCTAssertEqual(1.second.asMicroseconds(), pow(10, 6)) 73 | XCTAssertEqual(1.second.asMilliseconds(), pow(10, 3)) 74 | 75 | XCTAssertEqual(1.minute.asSeconds(), 60) 76 | XCTAssertEqual(1.hour.asMinutes(), 60) 77 | XCTAssertEqual(1.day.asHours(), 24) 78 | XCTAssertEqual(1.week.asDays(), 7) 79 | XCTAssertEqual(7.days.asWeeks(), 1) 80 | } 81 | 82 | func testDate() { 83 | let date0 = Date() 84 | let date1 = date0.addingTimeInterval(100) 85 | 86 | XCTAssertTrue(date1.intervalSinceNow.isAlmostEqual(to: 100.seconds, leeway: leeway)) 87 | 88 | XCTAssertEqual(date0.interval(since: date1), date0.timeIntervalSince(date1).seconds) 89 | 90 | XCTAssertEqual(date0.adding(1.seconds), date0.addingTimeInterval(1)) 91 | XCTAssertEqual(date0 + 1.seconds, date0.addingTimeInterval(1)) 92 | } 93 | 94 | func testDescription() { 95 | XCTAssertEqual(1.nanosecond.debugDescription, "Interval: 1 nanosecond(s)") 96 | } 97 | 98 | static var allTests = [ 99 | ("testEquatable", testEquatable), 100 | ("testIsNegative", testIsNegative), 101 | ("testAbs", testAbs), 102 | ("testNegated", testNegated), 103 | ("testCompare", testCompare), 104 | ("testLongerShorter", testLongerShorter), 105 | ("testMultiplying", testMultiplying), 106 | ("testAdding", testAdding), 107 | ("testOperators", testOperators), 108 | ("testAs", testAs), 109 | ("testDate", testDate) 110 | ] 111 | } 112 | -------------------------------------------------------------------------------- /Tests/ScheduleTests/MonthdayTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Schedule 3 | 4 | final class MonthdayTests: XCTestCase { 5 | 6 | func testIs() { 7 | let d = Date(year: 2019, month: 1, day: 1) 8 | XCTAssertTrue(d.is(.january(1), in: TimeZone.shanghai)) 9 | } 10 | 11 | func testAsDateComponents() { 12 | let comps = Monthday.april(1).asDateComponents() 13 | XCTAssertEqual(comps.month, 4) 14 | XCTAssertEqual(comps.day, 1) 15 | } 16 | 17 | func testDescription() { 18 | let md = Monthday.april(1) 19 | XCTAssertEqual(md.debugDescription, "Monthday: April 1st") 20 | } 21 | 22 | static var allTests = [ 23 | ("testIs", testIs), 24 | ("testAsDateComponents", testAsDateComponents), 25 | ("testDescription", testDescription) 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /Tests/ScheduleTests/PeriodTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Schedule 3 | 4 | final class PeriodTests: XCTestCase { 5 | 6 | func testPeriod() { 7 | let period = (1.year + 2.years + 1.month + 2.months + 3.days) 8 | XCTAssertEqual(period.years, 3) 9 | XCTAssertEqual(period.months, 3) 10 | XCTAssertEqual(period.days, 3) 11 | } 12 | 13 | func testInitWithString() { 14 | let p1 = Period("one second") 15 | XCTAssertNotNil(p1) 16 | XCTAssertEqual(p1!.seconds, 1) 17 | 18 | let p2 = Period("two hours and ten minutes") 19 | XCTAssertNotNil(p2) 20 | XCTAssertEqual(p2!.hours, 2) 21 | XCTAssertEqual(p2!.minutes, 10) 22 | 23 | let p3 = Period("1 year, 2 months and 3 days") 24 | XCTAssertNotNil(p3) 25 | XCTAssertEqual(p3!.years, 1) 26 | XCTAssertEqual(p3!.months, 2) 27 | XCTAssertEqual(p3!.days, 3) 28 | 29 | Period.registerQuantifier("many", for: 100 * 1000) 30 | let p4 = Period("many days") 31 | XCTAssertEqual(p4!.days, 100 * 1000) 32 | 33 | let p5 = Period("hi, 😈") 34 | XCTAssertNil(p5) 35 | } 36 | 37 | func testAdd() { 38 | XCTAssertEqual(1.month.adding(1.month).months, 2) 39 | XCTAssertEqual(Period(days: 1).adding(1.day).days, 2) 40 | } 41 | 42 | func testTidy() { 43 | let period = 1.month.adding(25.hour).tidied(to: .day) 44 | XCTAssertEqual(period.days, 1) 45 | } 46 | 47 | func testAsDateComponents() { 48 | let period = Period(years: 1, months: 2, days: 3, hours: 4, minutes: 5, seconds: 6, nanoseconds: 7) 49 | let comps = period.asDateComponents() 50 | XCTAssertEqual(comps.year, 1) 51 | XCTAssertEqual(comps.month, 2) 52 | XCTAssertEqual(comps.day, 3) 53 | XCTAssertEqual(comps.hour, 4) 54 | XCTAssertEqual(comps.minute, 5) 55 | XCTAssertEqual(comps.second, 6) 56 | XCTAssertEqual(comps.nanosecond, 7) 57 | } 58 | 59 | func testDate() { 60 | let d = Date(year: 1989, month: 6, day: 4) + 1.year 61 | let year = d.dateComponents.year 62 | XCTAssertEqual(year, 1990) 63 | } 64 | 65 | func testDescription() { 66 | let period = Period(years: 1, nanoseconds: 1) 67 | XCTAssertEqual(period.debugDescription, "Period: 1 year(s) 1 nanosecond(s)") 68 | } 69 | 70 | static var allTests = [ 71 | ("testPeriod", testPeriod), 72 | ("testInitWithString", testInitWithString), 73 | ("testAdd", testAdd), 74 | ("testTidy", testTidy), 75 | ("testAsDateComponents", testAsDateComponents), 76 | ("testDate", testDate), 77 | ("testDescription", testDescription) 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /Tests/ScheduleTests/PlanTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Schedule 3 | 4 | final class PlanTests: XCTestCase { 5 | 6 | private let leeway = 0.01.seconds 7 | 8 | func testOfIntervals() { 9 | let ints = [1.second, 2.hours, 3.days, 4.weeks] 10 | let p = Plan.of(ints) 11 | XCTAssertTrue(p.makeIterator().isAlmostEqual(to: ints, leeway: leeway)) 12 | } 13 | 14 | func testOfDates() { 15 | let ints = [1.second, 2.hours, 3.days, 4.weeks] 16 | 17 | let d0 = Date() + ints[0] 18 | let d1 = d0 + ints[1] 19 | let d2 = d1 + ints[2] 20 | let d3 = d2 + ints[3] 21 | 22 | let p = Plan.of(d0, d1, d2, d3) 23 | XCTAssertTrue(p.makeIterator().isAlmostEqual(to: ints, leeway: leeway)) 24 | } 25 | 26 | func testDates() { 27 | let dates = Plan.of(1.days, 2.weeks).dates.makeIterator() 28 | 29 | var n = dates.next() 30 | XCTAssertNotNil(n) 31 | XCTAssertTrue(n!.intervalSinceNow.isAlmostEqual(to: 1.days, leeway: leeway)) 32 | 33 | n = dates.next() 34 | XCTAssertNotNil(n) 35 | XCTAssertTrue(n!.intervalSinceNow.isAlmostEqual(to: 2.weeks + 1.days, leeway: leeway)) 36 | } 37 | 38 | func testDistant() { 39 | let distantPast = Plan.distantPast.makeIterator().next() 40 | XCTAssertNotNil(distantPast) 41 | XCTAssertTrue(distantPast!.isAlmostEqual(to: Date.distantPast.intervalSinceNow, leeway: leeway)) 42 | 43 | let distantFuture = Plan.distantFuture.makeIterator().next() 44 | XCTAssertNotNil(distantFuture) 45 | XCTAssertTrue(distantFuture!.isAlmostEqual(to: Date.distantFuture.intervalSinceNow, leeway: leeway)) 46 | } 47 | 48 | func testNever() { 49 | XCTAssertNil(Plan.never.makeIterator().next()) 50 | } 51 | 52 | func testConcat() { 53 | let p0: [Interval] = [1.second, 2.minutes, 3.hours] 54 | let p1: [Interval] = [4.days, 5.weeks] 55 | let p2 = Plan.of(p0).concat(Plan.of(p1)) 56 | let p3 = Plan.of(p0 + p1) 57 | XCTAssertTrue(p2.isAlmostEqual(to: p3, leeway: leeway)) 58 | } 59 | 60 | func testMerge() { 61 | let ints0: [Interval] = [1.second, 2.minutes, 1.hour] 62 | let ints1: [Interval] = [2.seconds, 1.minutes, 1.seconds] 63 | let p0 = Plan.of(ints0).merge(Plan.of(ints1)) 64 | let p1 = Plan.of(1.second, 1.second, 1.minutes, 1.seconds, 58.seconds, 1.hour) 65 | XCTAssertTrue(p0.isAlmostEqual(to: p1, leeway: leeway)) 66 | } 67 | 68 | func testFirst() { 69 | var count = 10 70 | let p = Plan.every(1.second).first(count) 71 | let i = p.makeIterator() 72 | while count > 0 { 73 | XCTAssertNotNil(i.next()) 74 | count -= 1 75 | } 76 | XCTAssertNil(i.next()) 77 | } 78 | 79 | func testUntil() { 80 | let until = Date() + 10.seconds 81 | let p = Plan.every(1.second).until(until).dates 82 | let i = p.makeIterator() 83 | while let date = i.next() { 84 | XCTAssertLessThan(date, until) 85 | } 86 | } 87 | 88 | func testNow() { 89 | let p0 = Plan.now 90 | let p1 = Plan.of(Date()) 91 | XCTAssertTrue(p0.isAlmostEqual(to: p1, leeway: leeway)) 92 | } 93 | 94 | func testAt() { 95 | let p = Plan.at(Date() + 1.second) 96 | let next = p.makeIterator().next() 97 | XCTAssertNotNil(next) 98 | XCTAssertTrue(next!.isAlmostEqual(to: 1.second, leeway: leeway)) 99 | } 100 | 101 | func testAfterAndRepeating() { 102 | let p0 = Plan.after(1.day, repeating: 1.hour).first(3) 103 | let p1 = Plan.of(1.day, 1.hour, 1.hour) 104 | XCTAssertTrue(p0.isAlmostEqual(to: p1, leeway: leeway)) 105 | } 106 | 107 | func testEveryPeriod() { 108 | let p = Plan.every("1 year").first(10) 109 | var date = Date() 110 | for i in p.dates { 111 | XCTAssertEqual(i.dateComponents.year!, date.dateComponents.year! + 1) 112 | XCTAssertEqual(i.dateComponents.month!, date.dateComponents.month!) 113 | XCTAssertEqual(i.dateComponents.day!, date.dateComponents.day!) 114 | date = i 115 | } 116 | } 117 | 118 | func testEveryWeekday() { 119 | let p = Plan.every(.friday, .monday).at("11:11:00").first(5) 120 | for i in p.dates { 121 | XCTAssertTrue(i.dateComponents.weekday == 6 || i.dateComponents.weekday == 2) 122 | XCTAssertEqual(i.dateComponents.hour, 11) 123 | } 124 | } 125 | 126 | func testEveryMonthday() { 127 | let p = Plan.every(.april(1), .october(1)).at(11, 11).first(5) 128 | for i in p.dates { 129 | XCTAssertTrue(i.dateComponents.month == 4 || i.dateComponents.month == 10) 130 | XCTAssertEqual(i.dateComponents.day, 1) 131 | XCTAssertEqual(i.dateComponents.hour, 11) 132 | } 133 | } 134 | 135 | func testOffset() { 136 | let p1 = Plan.after(1.second).first(100) 137 | let p2 = p1.offset(by: 1.second).first(100) 138 | 139 | for (d1, d2) in zip(p1.dates, p2.dates) { 140 | XCTAssertTrue(d2.interval(since: d1).isAlmostEqual(to: 1.second, leeway: leeway)) 141 | } 142 | } 143 | 144 | static var allTests = [ 145 | ("testOfIntervals", testOfIntervals), 146 | ("testOfDates", testOfDates), 147 | ("testDates", testDates), 148 | ("testDistant", testDistant), 149 | ("testNever", testNever), 150 | ("testConcat", testConcat), 151 | ("testMerge", testMerge), 152 | ("testFirst", testFirst), 153 | ("testUntil", testUntil), 154 | ("testNow", testNow), 155 | ("testAt", testAt), 156 | ("testAfterAndRepeating", testAfterAndRepeating), 157 | ("testEveryPeriod", testEveryPeriod), 158 | ("testEveryWeekday", testEveryWeekday), 159 | ("testEveryMonthday", testEveryMonthday), 160 | ("testOffset", testOffset) 161 | ] 162 | } 163 | -------------------------------------------------------------------------------- /Tests/ScheduleTests/TaskCenterTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Schedule 3 | 4 | final class TaskCenterTests: XCTestCase { 5 | 6 | @discardableResult 7 | func makeTask() -> Task { 8 | return Plan.never.do { } 9 | } 10 | 11 | var center: TaskCenter { 12 | return TaskCenter.default 13 | } 14 | 15 | func testDefault() { 16 | let task = makeTask() 17 | XCTAssertTrue(center.allTasks.contains(task)) 18 | center.removeAll() 19 | } 20 | 21 | func testAdd() { 22 | let task = makeTask() 23 | XCTAssertEqual(center.allTasks.count, 1) 24 | 25 | let c = TaskCenter() 26 | c.add(task) 27 | 28 | XCTAssertEqual(center.allTasks.count, 0) 29 | XCTAssertEqual(c.allTasks.count, 1) 30 | 31 | center.add(task) 32 | XCTAssertEqual(center.allTasks.count, 1) 33 | XCTAssertEqual(c.allTasks.count, 0) 34 | 35 | center.removeAll() 36 | } 37 | 38 | func testRemove() { 39 | let task = makeTask() 40 | let tag = UUID().uuidString 41 | center.addTag(tag, to: task) 42 | 43 | task.removeFromTaskCenter() 44 | 45 | XCTAssertFalse(center.allTasks.contains(task)) 46 | XCTAssertFalse(center.allTags.contains(tag)) 47 | 48 | center.removeAll() 49 | } 50 | 51 | func testTag() { 52 | let task = makeTask() 53 | let tag = UUID().uuidString 54 | 55 | center.addTag(tag, to: task) 56 | XCTAssertTrue(center.tasks(forTag: tag).contains(task)) 57 | XCTAssertTrue(center.tags(forTask: task).contains(tag)) 58 | 59 | center.removeTag(tag, from: task) 60 | XCTAssertFalse(center.tasks(forTag: tag).contains(task)) 61 | XCTAssertFalse(center.tags(forTask: task).contains(tag)) 62 | 63 | center.removeAll() 64 | } 65 | 66 | func testAll() { 67 | let task = makeTask() 68 | let tag1 = UUID().uuidString 69 | let tag2 = UUID().uuidString 70 | 71 | center.addTags([tag1, tag2], to: task) 72 | 73 | XCTAssertEqual(center.allTags.sorted(), [tag1, tag2].sorted()) 74 | XCTAssertEqual(center.allTasks, [task]) 75 | 76 | center.removeAll() 77 | } 78 | 79 | func testOperation() { 80 | let task = makeTask() 81 | let tag = UUID().uuidString 82 | 83 | center.addTag(tag, to: task) 84 | 85 | center.suspend(byTag: tag) 86 | XCTAssertEqual(task.suspensionCount, 1) 87 | 88 | center.resume(byTag: tag) 89 | XCTAssertEqual(task.suspensionCount, 0) 90 | 91 | center.cancel(byTag: tag) 92 | XCTAssertTrue(task.isCancelled) 93 | 94 | center.removeAll() 95 | } 96 | 97 | func testWeak() { 98 | let block = { 99 | let task = self.makeTask() 100 | XCTAssertEqual(self.center.allTasks.count, 1) 101 | _ = task 102 | } 103 | block() 104 | 105 | XCTAssertEqual(center.allTasks.count, 0) 106 | } 107 | 108 | static var allTests = [ 109 | ("testDefault", testDefault), 110 | ("testAdd", testAdd), 111 | ("testRemove", testRemove), 112 | ("testTag", testTag), 113 | ("testAll", testAll), 114 | ("testOperation", testOperation), 115 | ("testWeak", testWeak) 116 | ] 117 | } 118 | -------------------------------------------------------------------------------- /Tests/ScheduleTests/TaskTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Schedule 3 | 4 | final class TaskTests: XCTestCase { 5 | 6 | let leeway = 0.01.second 7 | 8 | func testMetrics() { 9 | let e = expectation(description: "testMetrics") 10 | let date = Date() 11 | 12 | let task = Plan.after(0.01.second, repeating: 0.01.second).do(queue: .global()) { 13 | e.fulfill() 14 | } 15 | XCTAssertTrue(task.creationDate.interval(since: date).isAlmostEqual(to: 0.second, leeway: leeway)) 16 | 17 | waitForExpectations(timeout: 0.1) 18 | 19 | XCTAssertNotNil(task.firstExecutionDate) 20 | XCTAssertTrue(task.firstExecutionDate!.interval(since: date).isAlmostEqual(to: 0.01.second, leeway: leeway)) 21 | 22 | XCTAssertNotNil(task.lastExecutionDate) 23 | XCTAssertTrue(task.lastExecutionDate!.interval(since: date).isAlmostEqual(to: 0.01.second, leeway: leeway)) 24 | 25 | XCTAssertEqual(task.executionDates!.count, 1) 26 | } 27 | 28 | func testAfter() { 29 | let e = expectation(description: "testSchedule") 30 | let date = Date() 31 | let task = Plan.after(0.01.second).do(queue: .global()) { 32 | XCTAssertTrue(Date().interval(since: date).isAlmostEqual(to: 0.01.second, leeway: self.leeway)) 33 | e.fulfill() 34 | } 35 | waitForExpectations(timeout: 0.1) 36 | 37 | _ = task 38 | } 39 | 40 | func testRepeat() { 41 | let e = expectation(description: "testRepeat") 42 | var count = 0 43 | let task = Plan.every(0.01.second).first(3).do(queue: .global()) { 44 | count += 1 45 | if count == 3 { e.fulfill() } 46 | } 47 | waitForExpectations(timeout: 0.1) 48 | 49 | _ = task 50 | } 51 | 52 | func testTaskCenter() { 53 | let task = Plan.never.do { } 54 | XCTAssertTrue(task.taskCenter === TaskCenter.default) 55 | 56 | task.removeFromTaskCenter() 57 | XCTAssertNil(task.taskCenter) 58 | 59 | let center = TaskCenter() 60 | task.addToTaskCenter(center) 61 | XCTAssertTrue(task.taskCenter === center) 62 | } 63 | 64 | func testDispatchQueue() { 65 | let e = expectation(description: "testQueue") 66 | let q = DispatchQueue(label: UUID().uuidString) 67 | 68 | let task = Plan.after(0.01.second).do(queue: q) { 69 | XCTAssertTrue(DispatchQueue.is(q)) 70 | e.fulfill() 71 | } 72 | waitForExpectations(timeout: 0.1) 73 | 74 | _ = task 75 | } 76 | 77 | func testThread() { 78 | let e = expectation(description: "testThread") 79 | DispatchQueue.global().async { 80 | let thread = Thread.current 81 | let task = Plan.after(0.01.second).do { task in 82 | XCTAssertTrue(thread === Thread.current) 83 | e.fulfill() 84 | task.cancel() 85 | } 86 | _ = task 87 | RunLoop.current.run() 88 | } 89 | waitForExpectations(timeout: 0.1) 90 | } 91 | 92 | func testExecuteNow() { 93 | let e = expectation(description: "testExecuteNow") 94 | let task = Plan.never.do { 95 | e.fulfill() 96 | } 97 | task.executeNow() 98 | waitForExpectations(timeout: 0.1) 99 | } 100 | 101 | func testReschedule() { 102 | let e = expectation(description: "testReschedule") 103 | var i = 0 104 | let task = Plan.after(0.01.second).do(queue: .global()) { (task) in 105 | i += 1 106 | if task.executionCount == 3 && task.estimatedNextExecutionDate == nil { 107 | e.fulfill() 108 | } 109 | if task.executionCount > 3 { 110 | XCTFail("should never come here") 111 | } 112 | } 113 | DispatchQueue.global().async(after: 0.02.second) { 114 | task.reschedule(Plan.every(0.01.second).first(2)) 115 | } 116 | waitForExpectations(timeout: 0.1) 117 | } 118 | 119 | func testSuspendResume() { 120 | let task = Plan.never.do { } 121 | XCTAssertEqual(task.suspensionCount, 0) 122 | task.suspend() 123 | task.suspend() 124 | task.suspend() 125 | XCTAssertEqual(task.suspensionCount, 3) 126 | task.resume() 127 | XCTAssertEqual(task.suspensionCount, 2) 128 | } 129 | 130 | func testCancel() { 131 | let task = Plan.never.do { } 132 | XCTAssertFalse(task.isCancelled) 133 | task.cancel() 134 | XCTAssertTrue(task.isCancelled) 135 | } 136 | 137 | func testAddAndRemoveActions() { 138 | let e = expectation(description: "testAddAndRemoveActions") 139 | let task = Plan.after(0.1.second).do { } 140 | let date = Date() 141 | let key = task.addAction { _ in 142 | XCTAssertTrue(Date().timeIntervalSince(date).isAlmostEqual(to: 0.1, leeway: 0.1)) 143 | e.fulfill() 144 | } 145 | XCTAssertEqual(task.actionCount, 2) 146 | waitForExpectations(timeout: 0.5) 147 | 148 | task.removeAction(byKey: key) 149 | XCTAssertEqual(task.actionCount, 1) 150 | 151 | task.cancel() 152 | 153 | task.removeAllActions() 154 | XCTAssertEqual(task.actionCount, 0) 155 | } 156 | 157 | static var allTests = [ 158 | ("testAfter", testAfter), 159 | ("testRepeat", testRepeat), 160 | ("testTaskCenter", testTaskCenter), 161 | ("testDispatchQueue", testDispatchQueue), 162 | ("testThread", testThread), 163 | ("testExecuteNow", testExecuteNow), 164 | ("testReschedule", testReschedule), 165 | ("testSuspendResume", testSuspendResume), 166 | ("testCancel", testCancel), 167 | ("testAddAndRemoveActions", testAddAndRemoveActions) 168 | ] 169 | } 170 | -------------------------------------------------------------------------------- /Tests/ScheduleTests/TimeTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Schedule 3 | 4 | final class TimeTests: XCTestCase { 5 | 6 | func testTime() { 7 | let t1 = Time("11:12:13.456") 8 | XCTAssertNotNil(t1) 9 | XCTAssertEqual(t1?.hour, 11) 10 | XCTAssertEqual(t1?.minute, 12) 11 | XCTAssertEqual(t1?.second, 13) 12 | if let i = t1?.nanosecond.nanoseconds { 13 | XCTAssertTrue(i.isAlmostEqual(to: 0.456.second, leeway: 0.001.seconds)) 14 | } 15 | 16 | let t2 = Time("11 pm") 17 | XCTAssertNotNil(t2) 18 | XCTAssertEqual(t2?.hour, 23) 19 | 20 | let t3 = Time("12 am") 21 | XCTAssertNotNil(t3) 22 | XCTAssertEqual(t3?.hour, 0) 23 | 24 | let t4 = Time("schedule") 25 | XCTAssertNil(t4) 26 | } 27 | 28 | func testIntervalSinceStartOfDay() { 29 | XCTAssertEqual(Time(hour: 1)!.intervalSinceStartOfDay, 1.hour) 30 | } 31 | 32 | func testAsDateComponents() { 33 | let time = Time(hour: 11, minute: 12, second: 13, nanosecond: 456) 34 | let components = time?.asDateComponents() 35 | XCTAssertEqual(components?.hour, 11) 36 | XCTAssertEqual(components?.minute, 12) 37 | XCTAssertEqual(components?.second, 13) 38 | XCTAssertEqual(components?.nanosecond, 456) 39 | } 40 | 41 | func testDescription() { 42 | let time = Time("11:12:13.456") 43 | XCTAssertEqual(time!.debugDescription, "Time: 11:12:13.456") 44 | } 45 | 46 | static var allTests = [ 47 | ("testTime", testTime), 48 | ("testIntervalSinceStartOfDay", testIntervalSinceStartOfDay), 49 | ("testAsDateComponents", testAsDateComponents), 50 | ("testDescription", testDescription) 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /Tests/ScheduleTests/WeekdayTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Schedule 3 | 4 | final class WeekdayTests: XCTestCase { 5 | 6 | func testIs() { 7 | let d = Date(year: 2019, month: 1, day: 1) 8 | XCTAssertTrue(d.is(.tuesday, in: TimeZone.shanghai)) 9 | } 10 | 11 | func testAsDateComponents() { 12 | XCTAssertEqual(Weekday.monday.asDateComponents().weekday!, 2) 13 | } 14 | 15 | func testDescription() { 16 | let wd = Weekday.tuesday 17 | XCTAssertEqual(wd.debugDescription, "Weekday: Tuesday") 18 | } 19 | 20 | static var allTests = [ 21 | ("testIs", testIs), 22 | ("testAsDateComponents", testAsDateComponents), 23 | ("testDescription", testDescription) 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /Tests/ScheduleTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if os(Linux) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(AtomicTests.allTests), 7 | testCase(BagTests.allTests), 8 | testCase(ExtensionsTests.allTests), 9 | testCase(IntervalTests.allTests), 10 | testCase(MonthdayTests.allTests), 11 | testCase(PeriodTests.allTests), 12 | testCase(PlanTests.allTests), 13 | testCase(TaskCenterTests.allTests), 14 | testCase(TaskTests.allTests), 15 | testCase(TimeTests.allTests), 16 | testCase(WeekdayTests.allTests) 17 | ] 18 | } 19 | #endif 20 | -------------------------------------------------------------------------------- /assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoxiu/Schedule/e388fcab1d870f32c0359a414fda81b8e81b98c9/assets/demo.png --------------------------------------------------------------------------------