├── .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
--------------------------------------------------------------------------------