diff --git a/.swift-version b/.swift-version deleted file mode 100644 index bf77d54..0000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -4.2 diff --git a/.swiftlint.yml b/.swiftlint.yml index 002b4b7..c1dfb8c 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,10 +1,10 @@ included: - Sources + - Tests disabled_rules: - cyclomatic_complexity - file_length - function_body_length - identifier_name - -line_length: 200 + - type_name \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 6c29709..c7d482f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,23 +3,25 @@ branches: - master language: objective-c -osx_image: xcode10 +osx_image: xcode10.2 + +env: + global: + - PROJECT="Schedule.xcodeproj" + - SCHEME="Schedule-Package" matrix: include: - os: osx env: - - SCHEME="Schedule-iOS" - - SDK="iphonesimulator12.0" - - DESTINATION="OS=12.0,name=iPhone X" + - SDK="iphonesimulator12.2" + - DESTINATION="platform=iOS Simulator,name=iPhone 8,OS=12.2" - os: osx env: - - SCHEME="Schedule-macOS" - - SDK="macosx10.13" + - SDK="macosx10.14" - DESTINATION="arch=x86_64" - os: osx env: - - SCHEME="Schedule-tvOS" - SDK="appletvsimulator12.0" - DESTINATION="OS=12.0,name=Apple TV 4K" - os: linux @@ -28,7 +30,7 @@ matrix: before_install: - if [[ $TRAVIS_OS_NAME == 'osx' ]]; then - gem install xcpretty --no-rdoc --no-ri --no-document --quiet; + gem install xcpretty; fi - if [[ $TRAVIS_OS_NAME == 'linux' ]]; then eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"; @@ -36,10 +38,9 @@ before_install: script: - if [[ $TRAVIS_OS_NAME == 'osx' ]]; then - xcodebuild clean build test -project Schedule.xcodeproj -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -enableCodeCoverage YES | xcpretty; + xcodebuild clean build test -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -enableCodeCoverage YES | xcpretty; fi - if [[ $TRAVIS_OS_NAME == 'linux' ]]; then - swift build; swift test; fi diff --git a/Package.swift b/Package.swift index 4825b79..6b9dd4d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,16 +1,23 @@ -// swift-tools-version:4.2 +// swift-tools-version:5.0 import PackageDescription let package = Package( name: "Schedule", + platforms: [ + .macOS(.v10_12), + .iOS(.v10), + .tvOS(.v10), + .watchOS(.v3) + ], products: [ - .library(name: "Schedule", targets: ["Schedule"]), - .executable(name: "ScheduleDemo", targets: ["ScheduleDemo"]) + .library(name: "Schedule", targets: ["Schedule"]) ], targets: [ .target(name: "Schedule"), - .testTarget(name: "ScheduleTests", dependencies: ["Schedule"]), - .target(name: "ScheduleDemo", dependencies: ["Schedule"]) + .testTarget(name: "ScheduleTests", dependencies: ["Schedule"]) + ], + swiftLanguageVersions: [ + .v5 ] ) diff --git a/README.md b/README.md index 7358e9c..05137b6 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,31 @@ # Schedule([简体中文](README.zh_cn.md))

- -[![Build Status](https://travis-ci.org/jianstm/Schedule.svg?branch=master)](https://travis-ci.org/jianstm/Schedule) -[![codecov](https://codecov.io/gh/jianstm/Schedule/branch/master/graph/badge.svg)](https://codecov.io/gh/jianstm/Schedule) - + + + - + +

-Schedule is a lightweight timed tasks scheduler for Swift. It allows you run timed tasks using an incredibly human-friendly syntax. +Schedule is a timing tasks scheduler written in Swift. It allows you run timing tasks with elegant and intuitive syntax.

- +

## Features -- [x] Variety of Scheduling Rules -- [x] Suspend, Resume, Cancel -- [x] Reschedule -- [x] Tag-based Task Management -- [x] Child-action Add/Remove -- [x] Natural Language Parse -- [x] Atomic Operation -- [x] Full Control Over Life Cycle -- [x] 95%+ Test Coverage -- [x] Complete Documentation(All Public Types & Methods) -- [x] Linux Support(Tested on Ubuntu 16.04) +- [x] Elegant and intuitive API +- [x] Rich preset rules +- [x] Powerful management mechanism +- [x] Detailed execution history +- [x] Thread safe +- [x] Complete documentation +- [x] ~100%+ test coverage ### Why You Should Use Schedule @@ -37,29 +33,26 @@ Schedule is a lightweight timed tasks scheduler for Swift. It allows you run tim | --- | :---: | :---: | :---: | | ⏰ Interval-based Schedule | ✓ | ✓ | ✓ | | 📆 Date-based Schedule | ✓ | | ✓ | -| 🌈 Mixing Rules Schedule | | | ✓ | +| 🌈 Combined Plan Schedule | | | ✓ | +| 🗣️ Natural Language Parse | | | ✓ | +| 🏷 Batch Task Management | | | ✓ | +| 📝 Execution Record | | | ✓ | +| 🎡 Plan Reset | | ✓ | ✓ | | 🚦 Suspend, Resume, Cancel | | ✓ | ✓ | -| 🎡 Reschedule | | ✓ | ✓ | -| 🏷 Tag-based Task Management | | | ✓ | -| 🍰 Child-action Add/Remove | | | ✓ | -| 📝 Natural Language Parse | | | ✓ | -| 🚔 Atomic Operation | | | ✓ | -| 🕕 Lifecycly Bind | | | ✓ | -| 🚀 Realtime Timeline Inspect | | | ✓ | -| 🎯 Lifetime Specify | | | ✓ | +| 🍰 Child-action | | | ✓ | ## Usage ### Overview -Scheduling a task has never been so simple and intuitive, all you have to do is: +Scheduling a task has never been so elegant and intuitive, all you have to do is: ```swift // 1. define your plan: let plan = Plan.after(3.seconds) // 2. do your task: -plan.do { +let task = plan.do { print("3 seconds passed!") } ``` @@ -68,58 +61,59 @@ plan.do { #### Interval-based Schedule -Schedule uses a self-defined type `Interval` to configure timed tasks, so you don't have to worry about extensions of built-in type polluting your namespace. The smooth constructors make the configuration like a comfortable conversation: +The running mechanism of Schedule is based on `Plan`, and `Plan` is actually a sequence of `Interval`. + +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. ```swift -Plan.every(1.second).do { } +let t1 = Plan.every(1.second).do { } -Plan.after(1.hour, repeating: 1.minute).do { } +let t2 = Plan.after(1.hour, repeating: 1.minute).do { } -Plan.of(1.second, 2.minutes, 3.hours).do { } +let t3 = Plan.of(1.second, 2.minutes, 3.hours).do { } ``` #### Date-based Schedule -Configuring date-based timing tasks is the same, Schedule defines all the commonly used date time types, trying to make your writing experience intuitive and smooth:: +Configuring date-based `Plan` is the same, with the expressive Swift syntax, Schedule makes your code look like a fluent conversation. ```swift -Plan.at(when).do { } +let t1 = Plan.at(date).do { } -Plan.every(.monday, .tuesday).at("9:00:00").do { } +let t2 = Plan.every(.monday, .tuesday).at("9:00:00").do { } -Plan.every(.september(30)).at(10, 30).do { } +let t3 = Plan.every(.september(30)).at(10, 30).do { } -Plan.every("one month and ten days").do { } +let t4 = Plan.every("one month and ten days").do { } -Plan.of(date0, date1, date2).do { } +let t5 = Plan.of(date0, date1, date2).do { } ``` #### Natural Language Parse -In addition, Schedule also supports basic natural language parsing, which greatly improves the readability of your code: +In addition, Schedule also supports simple natural language parsing. ```swift -Plan.every("one hour and ten minutes").do { } +let t1 = Plan.every("one hour and ten minutes").do { } -Plan.every("1 hour, 5 minutes and 10 seconds").do { } +let t2 = Plan.every("1 hour, 5 minutes and 10 seconds").do { } -Plan.every(.friday).at("9:00 pm").do { } +let t3 = Plan.every(.firday).at("9:00 pm").do { } -// Extensions Period.registerQuantifier("many", for: 100 * 1000) -Plan.every("many days").do { } +let t4 = Plan.every("many days").do { } ``` -#### Mixing Rules Schedule +#### Combined Plan Schedule -Schedule provides several collection operators, this means you can use them to customize your awesome rules: +Schedule provides several basic collection operators, which means you can use them to customize your own powerful plans. ```swift /// Concat let p0 = Plan.at(birthdate) let p1 = Plan.every(1.year) let birthday = p0.concat.p1 -birthday.do { +let t1 = birthday.do { print("Happy birthday") } @@ -127,7 +121,7 @@ birthday.do { let p3 = Plan.every(.january(1)).at("8:00") let p4 = Plan.every(.october(1)).at("9:00 AM") let holiday = p3.merge(p4) -holiday.do { +let t2 = holiday.do { print("Happy holiday") } @@ -140,64 +134,89 @@ let p7 = P.every(.monday).at(11, 12) let p8 = p7.until(date) ``` -### Creation +### Management -#### Parasitism +#### DispatchQueue -Schedule provides a parasitic mechanism, that allows you to handle one of the most common scenarios in a more elegant way: +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. ```swift -Plan.every(1.second).do(host: self) { - // do something, and cancel the task when host is deallocated. - // this's very useful when you want to bind a task's lifetime to a controller. +Plan.every(1.second).do(queue: .global()) { + print("On a globle queue") } ``` -#### RunLoop -The task will be executed on the current thread by default, and its implementation is based on RunLoop. So you need to ensure that the current thread has a RunLoop available. If the task is created on a child thread, you may need to run `RunLoop.current.run()`. +#### RunLoop -By default, Task will be added to `.common` mode, you can specify another mode when creating a task: +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. ```swift -Plan.every(1.second).do(mode: .default) { +let task = Plan.every(1.second).do(mode: .default) { print("on default mode...") } ``` -#### DispatchQueue +#### Timeline -You can use `queue` to specify which DispatchQueue the task will be dispatched to. In this case, the execution of the task is no longer dependent on RunLoop, you can use it safely on a child thread: +You can observe the execution record of the task in real time using the following properties. ```swift -Plan.every(1.second).do(queue: .global()) { - print("On a globle queue") -} +task.creationDate + +task.executionHistory + +task.firstExecutionDate +task.lastExecutionDate + +task.estimatedNextExecutionDate ``` -### Management +#### TaskCenter & Tag + +Tasks are automatically added to `TaskCenter.default` by default,you can organize them using tags and task center. + +```swift +let plan = Plan.every(1.day) +let task0 = plan.do(queue: myTaskQueue) { } +let task1 = plan.do(queue: myTaskQueue) { } + +TaskCenter.default.addTags(["database", "log"], to: task1) +TaskCenter.default.removeTag("log", from: task1) -In schedule, every newly created task is automatically held by an internal global variable and will not be released until you cancel it actively. So you don't have to add variables to your controller and write nonsense like `weak var timer: Timer`, `self.timer = timer`: +TaskCenter.default.suspend(byTag: "log") +TaskCenter.default.resume(byTag: "log") +TaskCenter.default.cancel(byTag: "log") + +TaskCenter.default.clear() + +let myCenter = TaskCenter() +myCenter.add(task0) +``` + + +### Suspend,Resume, Cancel + +You can `suspend`, `resume`, `cancel` a task. ```swift let task = Plan.every(1.minute).do { } -// will increase task's suspensions +// will increase task's suspensionCount task.suspend() -// will decrease task's suspensions, +// will decrease task's suspensionCount, // but don't worry about excessive resumptions, I will handle these for you~ task.resume() -// cancel task, this will remove task from the internal holder, -// in other words, will reduce task's reference count, -// if there are no other holders, task will be released. +// will clear task's suspensionCount +// a canceled task can't do anything, event if it is set to a new plan. task.cancel() ``` #### Action -You can add more actions to a task and delete them at any time you want: +You can add more actions to a task and remove them at any time you want: ```swift let dailyTask = Plan.every(1.day) @@ -213,55 +232,6 @@ let key = dailyTask.addAction { dailyTask.removeAction(byKey: key) ``` -#### Tag - -You can organize tasks with tags, and use queue to specify to where the task should be dispatched: - -```swift -let s = Plan.every(1.day) -let task0 = s.do(queue: myTaskQueue) { } -let task1 = s.do(queue: myTaskQueue) { } - -task0.addTag("database") -task1.addTags("database", "log") -task1.removeTag("log") - -Task.suspend(byTag: "log") -Task.resume(byTag: "log") -Task.cancel(byTag: "log") -``` - -#### Timeline - -You can inspect the timeline of a task in real time: - -```swift -let timeline = task.timeline -print(timeline.initialization) -print(timeline.firstExecution) -print(timeline.lastExecution) -print(timeline.estimatedNextExecution) -``` - -#### Lifetime - -And specify the lifetime of task: - -```swift -// will be cancelled after 10 hours. -task.setLifetime(10.hours) - -// will add 1 hour to tasks lifetime -task.addLifetime(1.hour) - -task.restOfLifetime == 11.hours -``` - -## Support - -- iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+ -- Linux(Tested on Ubuntu 16.04) - ## Installation ### CocoaPods @@ -271,44 +241,44 @@ task.restOfLifetime == 11.hours use_frameworks! target 'YOUR_TARGET_NAME' do - pod 'Schedule', '~> 1.0' + pod 'Schedule', '~> 2.0' end ``` ### Carthage -```ruby -github "jianstm/Schedule" ~> 1.0 +``` +github "jianstm/Schedule" ~> 2.0 ``` ### Swift Package Manager ```swift dependencies: [ - .package(url: "https://github.com/jianstm/Schedule", .upToNextMajor(from: "1.0.0")) + .package( + url: "https://github.com/jianstm/Schedule", .upToNextMajor(from: "2.0.0") + ) ] ``` -## Acknowledgement - -Inspired by Dan Bader's [schedule](https://github.com/dbader/schedule)! Syntax design is heavily influenced by Ruby! - ## Contributing -Like **Schedule**? Thank you so much! At the same time, I need your help: +Like **Schedule**? Thanks!!! + +At the same time, I need your help~ ### Finding Bugs -Schedule is just getting started, it is difficult to say how far the project is from bug free. If you could help the Schedule find or fix bugs that haven't been discovered yet, I would appreciate it! +Schedule is just getting started. If you could help the Schedule find or fix potential bugs, I would be grateful! ### New Features -Any awesome ideas? Feel free to open an issue or submit your pull request directly! +Have some awesome ideas? Feel free to open an issue or submit your pull request directly! ### Documentation improvements. -Improvements to README and documentation are welcome at all times, whether typos or my lame English. For users, the documentation is sometimes much more important than the specific code implementation. +Improvements to README and documentation are welcome at all times, whether typos or my lame English, 🤣. -### Share +## Acknowledgement -The more users the project has, the more robust the project will become, so, star! fork! and tell your friends! +Inspired by Dan Bader's [schedule](https://github.com/dbader/schedule)! \ No newline at end of file diff --git a/README.zh_cn.md b/README.zh_cn.md index 17b63f6..14e4e2d 100644 --- a/README.zh_cn.md +++ b/README.zh_cn.md @@ -1,124 +1,119 @@ # Schedule

- -[![Build Status](https://travis-ci.org/jianstm/Schedule.svg?branch=master)](https://travis-ci.org/jianstm/Schedule) -[![codecov](https://codecov.io/gh/jianstm/Schedule/branch/master/graph/badge.svg)](https://codecov.io/gh/jianstm/Schedule) - + + + - + +

-Schedule 是一个轻量级的调度框架,它能让你用难以置信的友好语法执行定时任务。 +Schedule 是一个用 Swift 编写的定时任务调度器,它能让你用优雅、直观的语法执行定时任务。

- +

## 功能 -- [x] 多种调度规则 -- [x] 暂停、继续、取消 -- [x] 重置调度规则 -- [x] 基于 tag 的任务管理 -- [x] 添加、移除子动作 -- [x] 自然语言解析 -- [x] 原子操作 -- [x] 对生命周期的完全控制 -- [x] 95%+ 测试覆盖 -- [x] 完善的文档(所有 public 类型和方法) -- [x] 支持 Linux(通过 Ubuntu 16.04 测试) +- [x] 优雅,直观的 API +- [x] 丰富的预置规则 +- [x] 强大的管理机制 +- [x] 细致的执行记录 +- [x] 线程安全 +- [x] 完整的文档 +- [x] ~100% 的测试覆盖 -### 为什么你该用 Schedule +### 为什么你该使用 Schedule,而不是…… | 功能 | Timer | DispatchSourceTimer | Schedule | | --- | :---: | :---: | :---: | | ⏰ 基于时间间隔调度 | ✓ | ✓ | ✓ | | 📆 基于日期调度 | ✓ | | ✓ | -| 🌈 自定义规则调度 | | | ✓ | +| 🌈 组合计划调度 | | | ✓ | +| 🗣️ 自然语言解析 | | | ✓ | +| 🏷 批任务管理 | | | ✓ | +| 📝 执行记录 | | | ✓ | +| 🎡 规则重置 | | ✓ | ✓ | | 🚦 暂停、继续、取消 | | ✓ | ✓ | -| 🎡 重置规则 | | ✓ | ✓ | -| 🏷 基于 tag 的任务管理 | | | ✓ | -| 🍰 添加、移除子动作 | | | ✓ | -| 📝 自然语言解析 | | | ✓ | -| 🚔 原子操作 | | | ✓ | -| 🕕 生命周期绑定 | | | ✓ | -| 🚀 实时观察时间线 | | | ✓ | -| 🏌 寿命设置 | | | ✓ | +| 🍰 子动作 | | | ✓ | ## 用法 ### 一瞥 -调度一个定时任务从未如此简单直观,你要做的只有: +调度一个定时任务从未如此优雅、直观,你只需要: ```swift // 1. 定义你的计划: let plan = Plan.after(3.seconds) // 2. 执行你的任务: -plan.do { +let task = plan.do { print("3 seconds passed!") } ``` -### 规则 +### 计划 #### 基于时间间隔调度 -Schedule 使用自定义的 `Interval` 类型来配置定时任务,你不必担心对内置类型的扩展会污染你的命名空间。流畅的构造方法让配置像一场舒服的对话: +Schedule 的机制基于 `Plan`,而 `Plan` 的本质是一系列 `Interval`。 + +Schedule 通过扩展 `Int` 和 `Double` 让 `Plan` 的定义更加优雅、直观。同时,因为 `Interval` 是 Schedule 的内置类型,所以你不用担心这会对你的命名空间产生污染。 ```swift -Plan.every(1.second).do { } +let t1 = Plan.every(1.second).do { } -Plan.after(1.hour, repeating: 1.minute).do { } +let t2 = Plan.after(1.hour, repeating: 1.minute).do { } -Plan.of(1.second, 2.minutes, 3.hours).do { } +let t3 = Plan.of(1.second, 2.minutes, 3.hours).do { } ``` #### 基于日期调度 -配置基于日期的调度同样如此,Schedule 定义了所有常用的日期类型,尽力让你的书写直观、流畅: +定制基于日期的 `Plan` 同样如此,配合富有表现力的 Swift 语法,Schedule 让你的代码看起来就像一场流畅的对话。 ```swift -Plan.at(when).do { } +let t1 = Plan.at(date).do { } -Plan.every(.monday, .tuesday).at("9:00:00").do { } +let t2 = Plan.every(.monday, .tuesday).at("9:00:00").do { } -Plan.every(.september(30)).at(10, 30).do { } +let t3 = Plan.every(.september(30)).at(10, 30).do { } -Plan.every("one month and ten days").do { } +let t4 = Plan.every("one month and ten days").do { } -Plan.of(date0, date1, date2).do { } +let t5 = Plan.of(date0, date1, date2).do { } ``` #### 自然语言解析 -除此之外,Schedule 还支持基础的自然语言解析,这大大增强了你的代码的可读性: +除此之外,Schedule 还支持简单的自然语言解析。 ```swift -Plan.every("one hour and ten minutes").do { } +let t1 = Plan.every("one hour and ten minutes").do { } -Plan.every("1 hour, 5 minutes and 10 seconds").do { } +let t2 = Plan.every("1 hour, 5 minutes and 10 seconds").do { } -Plan.every(.firday).at("9:00 pm").do { } +let t3 = Plan.every(.firday).at("9:00 pm").do { } Period.registerQuantifier("many", for: 100 * 1000) -Plan.every("many days").do { } +let t4 = Plan.every("many days").do { } ``` -#### 自定义规则调度 +#### 组合计划调度 -Schedule 还提供了几个简单的集合操作符,这意味着你可以使用它们定制属于你的强大规则: +Schedule 提供了几个基本的集合操作符,这意味着,你可以使用它们自由组合,定制属于你的强大规则。 ```swift /// Concat let p0 = Plan.at(birthdate) let p1 = Plan.every(1.year) let birthday = p0.concat.p1 -birthday.do { +let t1 = birthday.do { print("Happy birthday") } @@ -126,7 +121,7 @@ birthday.do { let p3 = Plan.every(.january(1)).at("8:00") let p4 = Plan.every(.october(1)).at("9:00 AM") let holiday = p3.merge(p4) -holiday.do { +let t2 = holiday.do { print("Happy holiday") } @@ -139,42 +134,68 @@ let p7 = P.every(.monday).at(11, 12) let p8 = p7.until(date) ``` -### 创建 +### 管理 -#### 寄生 +#### DispatchQueue -Schedule 提供了一种寄生机制,它让你可以以一种更优雅的方式处理 task 的生命周期: +调用 `plan.do` 来调度定时任务时,你可以使用 `queue` 来指定当时间到时,task 会被派发到哪个 `DispatchQueue` 上。这个操作不像 `Timer` 那样依赖 `RunLoop`,所以你可以在任意线程上使用它。 ```swift -Plan.every(1.second).do(host: self) { - // task 会在 host 被 deallocated 后自动被 cancel - // 这在你想要把一个 task 的生命周期绑定到控制器上时非常有用 +let task = Plan.every(1.second).do(queue: .global()) { + print("On a globle queue") } ``` #### RunLoop -Task 默认会在当前线程上执行,它的实现依赖于 RunLoop,所以你需要保证当前线程有一个可用的 RunLoop。如果 task 的创建在子线程上,你可能需要执行 `RunLoop.current.run()`。默认情况下, task 会被添加到 `.common` mode 上,你可以在创建 task 时指定其它 mode: +如果没有指定 `queue`,Schedule 会使用 `RunLoop` 来调度 task,这时,task 会在当前线程上执行。**要注意**,和同样基于 `RunLoop` 的 `Timer` 一样,你需要保证当前线程有一个**可用**的 `RunLoop`。默认情况下, task 会被添加到 `.common` mode 上,你可以在创建 task 时指定其它 mode。 ```swift -Plan.every(1.second).do(mode: .default) { +let task = Plan.every(1.second).do(mode: .default) { print("on default mode...") } ``` -#### DispatchQueue +#### Timeline -你也可以使用 queue 来指定 task 会被派发到哪个 DispatchQueue 上,这时,task 的执行不再依赖于 RunLoop,意味着你可以放心地子线程上使用: +你可以使用以下属性实时地观察 task 的执行记录。 ```swift -Plan.every(1.second).do(queue: .global()) { - print("On a globle queue") -} +task.creationDate + +task.executionHistory + +task.firstExecutionDate +task.lastExecutionDate + +task.estimatedNextExecutionDate ``` -### 管理 +#### TaskCenter 和 Tag + +task 默认会被自动添加到 `TaskCenter.default` 上,你可以使用 tag 配合 taskCenter 来组织 tasks: -在 Schedule 里,每一个新创建的 task 都会被一个内部的全局变量自动持有,除非你显式地 cancel 它们,否则它们不会被提前释放。也就是说你不用再在控制器里写那些诸如 `weak var timer: Timer`, `self.timer = timer` 之类的啰唆代码了: +```swift +let plan = Plan.every(1.day) +let task0 = plan.do(queue: myTaskQueue) { } +let task1 = plan.do(queue: myTaskQueue) { } + +TaskCenter.default.addTags(["database", "log"], to: task1) +TaskCenter.default.removeTag("log", from: task1) + +TaskCenter.default.suspend(byTag: "log") +TaskCenter.default.resume(byTag: "log") +TaskCenter.default.cancel(byTag: "log") + +TaskCenter.default.removeAll() + +let myCenter = TaskCenter() +myCenter.add(task0) +``` + +### Suspend、Resume、Cancel + +你可以 suspend,resume,cancel 一个 task。 ```swift let task = Plan.every(1.minute).do { } @@ -182,19 +203,22 @@ let task = Plan.every(1.minute).do { } // 会增加 task 的暂停计数 task.suspend() -// 会减少 task 的暂停计数,不过不用担心过度减少, -// 我会帮你处理好这些~ +task.suspensions == 1 + +// 会减少 task 的暂停计数 +// 不过不用担心过度减少,我会帮你处理好这些~ task.resume() -// 取消任务,这会把任务从内部持有者那儿移除 -// 也就是说,会减少 task 的引用计数 -// 如果没有其它持有者的话,这个任务就会被释放 +task.suspensions == 0 + +// 会清零 task 的暂停计数 +// 被 cancel 的 task 即使重新设置其它调度规则也不会有任何作用了 task.cancel() ``` #### 子动作 -你可以添加更多的 action 到一个 task 上去,并在任意时刻移除它们: +你可以添加更多的 action 到 task,并在任意时刻移除它们。 ```swift let dailyTask = Plan.every(1.day) @@ -210,55 +234,6 @@ let key = dailyTask.addAction { dailyTask.removeAction(byKey: key) ``` -#### 标签 - -你可以用 tag 来组织 tasks,用 queue 指定这个 task 派发到哪里: - -```swift -let s = Plan.every(1.day) -let task0 = s.do(queue: myTaskQueue) { } -let task1 = s.do(queue: myTaskQueue) { } - -task0.addTag("database") -task1.addTags("database", "log") -task1.removeTag("log") - -Task.suspend(byTag: "log") -Task.resume(byTag: "log") -Task.cancel(byTag: "log") -``` - -#### 时间线 - -你可以实时地观察 task 的当前时间线: - -```swift -let timeline = task.timeline -print(timeline.initialization) -print(timeline.firstExecution) -print(timeline.lastExecution) -print(timeline.estimatedNextExecution) -``` - -#### 寿命 - -也可以精确地设置 task 的寿命: - -```swift -// 会再 10 小时后取消该 task -task.setLifetime(10.hours) - -// 会给该 task 的寿命增加 1 小时 -task.addLifetime(1.hour) - -task.restOfLifetime == 11.hours -``` - -## 支持 - -- iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+ -- Linux(Tested on Ubuntu 16.04) - ## 安装 ### CocoaPods @@ -268,44 +243,45 @@ task.restOfLifetime == 11.hours use_frameworks! target 'YOUR_TARGET_NAME' do - pod 'Schedule', '~> 1.0' + pod 'Schedule', '~> 2.0' end ``` ### Carthage -```ruby -github "jianstm/Schedule" ~> 1.0 +``` +# Cartfile +github "jianstm/Schedule" ~> 2.0 ``` ### Swift Package Manager ```swift dependencies: [ - .package(url: "https://github.com/jianstm/Schedule", .upToNextMajor("1.0.0")) + .package( + url: "https://github.com/jianstm/Schedule", .upToNextMajor(from: "2.0.0") + ) ] ``` -## 致谢 - -项目灵感来自于 Dan Bader 的 [schedule](https://github.com/dbader/schedule)!语法设计深受 Ruby 影响! - ## 贡献 -喜欢 **Schedule** 吗?谢谢!与此同时我需要你的帮助: +喜欢 **Schedule** 吗?谢谢!!! + +与此同时如果你想参与进来的话,你可以: ### 找 Bugs -Schedule 还是一个非常年轻的项目,很难说项目离 bug free 还有多远。如果你能帮 Schedule 找到或者解决还没被发现的 bug 的话,我将感激不尽! +Schedule 还是一个非常年轻的项目,如果你能帮 Schedule 找到甚至解决潜在的 bugs 的话,那就太感谢啦! ### 新功能 -对项目有什么新的想法吗?尽管在 issue 里分享出来,或者你也可以直接提交你的 Pull Request! +有一些有趣的想法?尽管在 issue 里分享出来,或者直接提交你的 Pull Request! ### 改善文档 -对 README 或者文档注释的改善建议在任何时候都非常欢迎,无论是错别字还是纠正我的蹩脚英文。对使用者来说,有时文档要比具体的代码实现要重要得多。 +任何时候都欢迎对 README 或者文档注释的修改建议,无论是错别字还是纠正我的蹩脚英文,🤣。 -### 分享 +## 致谢 -无疑,用的人越多,项目就会变得越健壮,所以,star!fork!然后告诉你的朋友们吧! \ No newline at end of file +项目灵感来自 Dan Bader 的 [schedule](https://github.com/dbader/schedule)! \ No newline at end of file diff --git a/Schedule.podspec b/Schedule.podspec index 91ec172..25f76d3 100644 --- a/Schedule.podspec +++ b/Schedule.podspec @@ -1,15 +1,15 @@ Pod::Spec.new do |s| s.name = "Schedule" - s.version = "2.0.0-beta.1" + s.version = "2.0.0" s.license = { :type => "MIT" } s.homepage = "https://github.com/jianstm/Schedule" s.author = { "Quentin Jin" => "jianstm@gmail.com" } - s.summary = "Lightweight timing task scheduler" + s.summary = "Schedule timing task in Swift using a fluent API" s.source = { :git => "https://github.com/jianstm/Schedule.git", :tag => "#{s.version}" } s.source_files = "Sources/Schedule/*.swift" - s.swift_version = "4.2" + s.swift_version = "5.0" s.ios.deployment_target = "10.0" s.osx.deployment_target = "10.12" diff --git a/Schedule.xcodeproj/Schedule_Info.plist b/Schedule.xcodeproj/Schedule_Info.plist index 3833fd1..57ada9f 100644 --- a/Schedule.xcodeproj/Schedule_Info.plist +++ b/Schedule.xcodeproj/Schedule_Info.plist @@ -1,26 +1,25 @@ - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 2.0.0-beta.1 - CFBundleSignature - ???? - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + diff --git a/Schedule.xcodeproj/project.pbxproj b/Schedule.xcodeproj/project.pbxproj index d25aff4..2374337 100644 --- a/Schedule.xcodeproj/project.pbxproj +++ b/Schedule.xcodeproj/project.pbxproj @@ -9,11 +9,11 @@ /* Begin PBXAggregateTarget section */ "Schedule::SchedulePackageTests::ProductTarget" /* SchedulePackageTests */ = { isa = PBXAggregateTarget; - buildConfigurationList = OBJ_83 /* Build configuration list for PBXAggregateTarget "SchedulePackageTests" */; + buildConfigurationList = OBJ_72 /* Build configuration list for PBXAggregateTarget "SchedulePackageTests" */; buildPhases = ( ); dependencies = ( - OBJ_86 /* PBXTargetDependency */, + OBJ_75 /* PBXTargetDependency */, ); name = SchedulePackageTests; productName = SchedulePackageTests; @@ -21,54 +21,44 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ - OBJ_100 /* TaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_37 /* TaskTests.swift */; }; - OBJ_101 /* XCTestManifests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_38 /* XCTestManifests.swift */; }; - OBJ_103 /* Schedule.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "Schedule::Schedule::Product" /* Schedule.framework */; }; - OBJ_50 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Atomic.swift */; }; - OBJ_51 /* Cabinet.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* Cabinet.swift */; }; - OBJ_52 /* DeinitObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* DeinitObserver.swift */; }; - OBJ_53 /* Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* Deprecated.swift */; }; - OBJ_54 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* Extensions.swift */; }; - OBJ_55 /* Interval.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_14 /* Interval.swift */; }; - OBJ_56 /* Monthday.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* Monthday.swift */; }; - OBJ_57 /* Period.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_16 /* Period.swift */; }; - OBJ_58 /* Plan.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* Plan.swift */; }; - OBJ_59 /* RunLoopTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_18 /* RunLoopTask.swift */; }; - OBJ_60 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_19 /* Task.swift */; }; - OBJ_61 /* TaskCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_20 /* TaskCenter.swift */; }; - OBJ_62 /* Time.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_21 /* Time.swift */; }; - OBJ_63 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_22 /* Timeline.swift */; }; - OBJ_64 /* Weekday.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_23 /* Weekday.swift */; }; - OBJ_71 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_25 /* Log.swift */; }; - OBJ_72 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_26 /* main.swift */; }; - OBJ_74 /* Schedule.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "Schedule::Schedule::Product" /* Schedule.framework */; }; - OBJ_81 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; }; - OBJ_92 /* AtomicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_29 /* AtomicTests.swift */; }; - OBJ_93 /* CabinetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_30 /* CabinetTests.swift */; }; - OBJ_94 /* DateTimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_31 /* DateTimeTests.swift */; }; - OBJ_95 /* DeinitObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_32 /* DeinitObserverTests.swift */; }; - OBJ_96 /* ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_33 /* ExtensionsTests.swift */; }; - OBJ_97 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_34 /* Helpers.swift */; }; - OBJ_98 /* PlanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_35 /* PlanTests.swift */; }; - OBJ_99 /* TaskCenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_36 /* TaskCenterTests.swift */; }; + 629FAA6F2255CC2700ED5D67 /* IntervalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 629FAA6E2255CC2700ED5D67 /* IntervalTests.swift */; }; + 629FAA712255D14800ED5D67 /* WeekdayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 629FAA702255D14800ED5D67 /* WeekdayTests.swift */; }; + 629FAA732255D1CA00ED5D67 /* PeriodTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 629FAA722255D1CA00ED5D67 /* PeriodTests.swift */; }; + 629FAA752255D6B100ED5D67 /* MonthdayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 629FAA742255D6B100ED5D67 /* MonthdayTests.swift */; }; + OBJ_49 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Atomic.swift */; }; + OBJ_50 /* Bag.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* Bag.swift */; }; + OBJ_53 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* Extensions.swift */; }; + OBJ_54 /* Interval.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_14 /* Interval.swift */; }; + OBJ_55 /* Monthday.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* Monthday.swift */; }; + OBJ_56 /* Period.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_16 /* Period.swift */; }; + OBJ_57 /* Plan.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* Plan.swift */; }; + OBJ_58 /* RunLoopTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_18 /* RunLoopTask.swift */; }; + OBJ_59 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_19 /* Task.swift */; }; + OBJ_60 /* TaskCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_20 /* TaskCenter.swift */; }; + OBJ_61 /* Time.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_21 /* Time.swift */; }; + OBJ_63 /* Weekday.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_23 /* Weekday.swift */; }; + OBJ_70 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; }; + OBJ_81 /* AtomicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_26 /* AtomicTests.swift */; }; + OBJ_82 /* BagTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_27 /* BagTests.swift */; }; + OBJ_83 /* TimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_28 /* TimeTests.swift */; }; + OBJ_85 /* ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_30 /* ExtensionsTests.swift */; }; + OBJ_86 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_31 /* Helpers.swift */; }; + OBJ_87 /* PlanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_32 /* PlanTests.swift */; }; + OBJ_88 /* TaskCenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_33 /* TaskCenterTests.swift */; }; + OBJ_89 /* TaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_34 /* TaskTests.swift */; }; + OBJ_90 /* XCTestManifests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_35 /* XCTestManifests.swift */; }; + OBJ_92 /* Schedule.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "Schedule::Schedule::Product" /* Schedule.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 66C8798D2237FA2F00A95D60 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = OBJ_1 /* Project object */; - proxyType = 1; - remoteGlobalIDString = "Schedule::Schedule"; - remoteInfo = Schedule; - }; - 66C8798E2237FA2F00A95D60 /* PBXContainerItemProxy */ = { + 623D582F2254B44800565925 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = OBJ_1 /* Project object */; proxyType = 1; remoteGlobalIDString = "Schedule::Schedule"; remoteInfo = Schedule; }; - 66C8798F2237FA3000A95D60 /* PBXContainerItemProxy */ = { + 623D58302254B44900565925 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = OBJ_1 /* Project object */; proxyType = 1; @@ -78,9 +68,11 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - OBJ_10 /* Cabinet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cabinet.swift; sourceTree = ""; }; - OBJ_11 /* DeinitObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeinitObserver.swift; sourceTree = ""; }; - OBJ_12 /* Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deprecated.swift; sourceTree = ""; }; + 629FAA6E2255CC2700ED5D67 /* IntervalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalTests.swift; sourceTree = ""; }; + 629FAA702255D14800ED5D67 /* WeekdayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekdayTests.swift; sourceTree = ""; }; + 629FAA722255D1CA00ED5D67 /* PeriodTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeriodTests.swift; sourceTree = ""; }; + 629FAA742255D6B100ED5D67 /* MonthdayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthdayTests.swift; sourceTree = ""; }; + OBJ_10 /* Bag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bag.swift; sourceTree = ""; }; OBJ_13 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; OBJ_14 /* Interval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interval.swift; sourceTree = ""; }; OBJ_15 /* Monthday.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Monthday.swift; sourceTree = ""; }; @@ -90,118 +82,78 @@ OBJ_19 /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; }; OBJ_20 /* TaskCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskCenter.swift; sourceTree = ""; }; OBJ_21 /* Time.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Time.swift; sourceTree = ""; }; - OBJ_22 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = ""; }; OBJ_23 /* Weekday.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weekday.swift; sourceTree = ""; }; - OBJ_25 /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; - OBJ_26 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; - OBJ_29 /* AtomicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicTests.swift; sourceTree = ""; }; - OBJ_30 /* CabinetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CabinetTests.swift; sourceTree = ""; }; - OBJ_31 /* DateTimeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeTests.swift; sourceTree = ""; }; - OBJ_32 /* DeinitObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeinitObserverTests.swift; sourceTree = ""; }; - OBJ_33 /* ExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionsTests.swift; sourceTree = ""; }; - OBJ_34 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; - OBJ_35 /* PlanTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanTests.swift; sourceTree = ""; }; - OBJ_36 /* TaskCenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskCenterTests.swift; sourceTree = ""; }; - OBJ_37 /* TaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskTests.swift; sourceTree = ""; }; - OBJ_38 /* XCTestManifests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestManifests.swift; sourceTree = ""; }; - OBJ_40 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = SOURCE_ROOT; }; + OBJ_26 /* AtomicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicTests.swift; sourceTree = ""; }; + OBJ_27 /* BagTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BagTests.swift; sourceTree = ""; }; + OBJ_28 /* TimeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeTests.swift; sourceTree = ""; }; + OBJ_30 /* ExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionsTests.swift; sourceTree = ""; }; + OBJ_31 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; + OBJ_32 /* PlanTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanTests.swift; sourceTree = ""; }; + OBJ_33 /* TaskCenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskCenterTests.swift; sourceTree = ""; }; + OBJ_34 /* TaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskTests.swift; sourceTree = ""; }; + OBJ_35 /* XCTestManifests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestManifests.swift; sourceTree = ""; }; + OBJ_39 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = SOURCE_ROOT; }; + OBJ_40 /* Schedule.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = Schedule.podspec; sourceTree = ""; }; + OBJ_41 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + OBJ_42 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + OBJ_43 /* README.zh_cn.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.zh_cn.md; sourceTree = ""; }; OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; OBJ_9 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; "Schedule::Schedule::Product" /* Schedule.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Schedule.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - "Schedule::ScheduleDemo::Product" /* ScheduleDemo */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; path = ScheduleDemo; sourceTree = BUILT_PRODUCTS_DIR; }; - "Schedule::ScheduleTests::Product" /* ScheduleTests.xctest */ = {isa = PBXFileReference; lastKnownFileType = file; path = ScheduleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + "Schedule::ScheduleTests::Product" /* ScheduleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; path = ScheduleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - OBJ_102 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 0; - files = ( - OBJ_103 /* Schedule.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - OBJ_65 /* Frameworks */ = { + OBJ_64 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 0; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - OBJ_73 /* Frameworks */ = { + OBJ_91 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 0; files = ( - OBJ_74 /* Schedule.framework in Frameworks */, + OBJ_92 /* Schedule.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 66C87992223A90D300A95D60 /* Utils */ = { - isa = PBXGroup; - children = ( - OBJ_13 /* Extensions.swift */, - OBJ_11 /* DeinitObserver.swift */, - OBJ_9 /* Atomic.swift */, - OBJ_10 /* Cabinet.swift */, - ); - name = Utils; - sourceTree = ""; - }; - 66C87993223A90EE00A95D60 /* DateTime */ = { - isa = PBXGroup; - children = ( - OBJ_14 /* Interval.swift */, - OBJ_15 /* Monthday.swift */, - OBJ_16 /* Period.swift */, - OBJ_21 /* Time.swift */, - OBJ_23 /* Weekday.swift */, - ); - name = DateTime; - sourceTree = ""; - }; - OBJ_24 /* ScheduleDemo */ = { - isa = PBXGroup; - children = ( - OBJ_25 /* Log.swift */, - OBJ_26 /* main.swift */, - ); - name = ScheduleDemo; - path = Sources/ScheduleDemo; - sourceTree = SOURCE_ROOT; - }; - OBJ_27 /* Tests */ = { + OBJ_24 /* Tests */ = { isa = PBXGroup; children = ( - OBJ_28 /* ScheduleTests */, + OBJ_25 /* ScheduleTests */, ); name = Tests; sourceTree = SOURCE_ROOT; }; - OBJ_28 /* ScheduleTests */ = { + OBJ_25 /* ScheduleTests */ = { isa = PBXGroup; children = ( - OBJ_29 /* AtomicTests.swift */, - OBJ_30 /* CabinetTests.swift */, - OBJ_31 /* DateTimeTests.swift */, - OBJ_32 /* DeinitObserverTests.swift */, - OBJ_33 /* ExtensionsTests.swift */, - OBJ_34 /* Helpers.swift */, - OBJ_35 /* PlanTests.swift */, - OBJ_36 /* TaskCenterTests.swift */, - OBJ_37 /* TaskTests.swift */, - OBJ_38 /* XCTestManifests.swift */, + OBJ_26 /* AtomicTests.swift */, + OBJ_27 /* BagTests.swift */, + OBJ_30 /* ExtensionsTests.swift */, + OBJ_31 /* Helpers.swift */, + 629FAA6E2255CC2700ED5D67 /* IntervalTests.swift */, + 629FAA742255D6B100ED5D67 /* MonthdayTests.swift */, + 629FAA722255D1CA00ED5D67 /* PeriodTests.swift */, + OBJ_32 /* PlanTests.swift */, + OBJ_33 /* TaskCenterTests.swift */, + OBJ_34 /* TaskTests.swift */, + OBJ_28 /* TimeTests.swift */, + 629FAA702255D14800ED5D67 /* WeekdayTests.swift */, + OBJ_35 /* XCTestManifests.swift */, ); name = ScheduleTests; path = Tests/ScheduleTests; sourceTree = SOURCE_ROOT; }; - OBJ_41 /* Products */ = { + OBJ_36 /* Products */ = { isa = PBXGroup; children = ( - "Schedule::ScheduleDemo::Product" /* ScheduleDemo */, "Schedule::Schedule::Product" /* Schedule.framework */, "Schedule::ScheduleTests::Product" /* ScheduleTests.xctest */, ); @@ -213,9 +165,13 @@ children = ( OBJ_6 /* Package.swift */, OBJ_7 /* Sources */, - OBJ_27 /* Tests */, - OBJ_40 /* assets */, - OBJ_41 /* Products */, + OBJ_24 /* Tests */, + OBJ_36 /* Products */, + OBJ_39 /* assets */, + OBJ_40 /* Schedule.podspec */, + OBJ_41 /* LICENSE */, + OBJ_42 /* README.md */, + OBJ_43 /* README.zh_cn.md */, ); sourceTree = ""; }; @@ -223,7 +179,6 @@ isa = PBXGroup; children = ( OBJ_8 /* Schedule */, - OBJ_24 /* ScheduleDemo */, ); name = Sources; sourceTree = SOURCE_ROOT; @@ -231,14 +186,18 @@ OBJ_8 /* Schedule */ = { isa = PBXGroup; children = ( - 66C87993223A90EE00A95D60 /* DateTime */, - 66C87992223A90D300A95D60 /* Utils */, - OBJ_12 /* Deprecated.swift */, + OBJ_9 /* Atomic.swift */, + OBJ_10 /* Bag.swift */, + OBJ_13 /* Extensions.swift */, + OBJ_14 /* Interval.swift */, + OBJ_15 /* Monthday.swift */, + OBJ_16 /* Period.swift */, OBJ_17 /* Plan.swift */, OBJ_18 /* RunLoopTask.swift */, OBJ_19 /* Task.swift */, OBJ_20 /* TaskCenter.swift */, - OBJ_22 /* Timeline.swift */, + OBJ_21 /* Time.swift */, + OBJ_23 /* Weekday.swift */, ); name = Schedule; path = Sources/Schedule; @@ -249,10 +208,10 @@ /* Begin PBXNativeTarget section */ "Schedule::Schedule" /* Schedule */ = { isa = PBXNativeTarget; - buildConfigurationList = OBJ_46 /* Build configuration list for PBXNativeTarget "Schedule" */; + buildConfigurationList = OBJ_45 /* Build configuration list for PBXNativeTarget "Schedule" */; buildPhases = ( - OBJ_49 /* Sources */, - OBJ_65 /* Frameworks */, + OBJ_48 /* Sources */, + OBJ_64 /* Frameworks */, ); buildRules = ( ); @@ -263,34 +222,17 @@ productReference = "Schedule::Schedule::Product" /* Schedule.framework */; productType = "com.apple.product-type.framework"; }; - "Schedule::ScheduleDemo" /* ScheduleDemo */ = { - isa = PBXNativeTarget; - buildConfigurationList = OBJ_67 /* Build configuration list for PBXNativeTarget "ScheduleDemo" */; - buildPhases = ( - OBJ_70 /* Sources */, - OBJ_73 /* Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - OBJ_75 /* PBXTargetDependency */, - ); - name = ScheduleDemo; - productName = ScheduleDemo; - productReference = "Schedule::ScheduleDemo::Product" /* ScheduleDemo */; - productType = "com.apple.product-type.tool"; - }; "Schedule::ScheduleTests" /* ScheduleTests */ = { isa = PBXNativeTarget; - buildConfigurationList = OBJ_88 /* Build configuration list for PBXNativeTarget "ScheduleTests" */; + buildConfigurationList = OBJ_77 /* Build configuration list for PBXNativeTarget "ScheduleTests" */; buildPhases = ( - OBJ_91 /* Sources */, - OBJ_102 /* Frameworks */, + OBJ_80 /* Sources */, + OBJ_91 /* Frameworks */, ); buildRules = ( ); dependencies = ( - OBJ_104 /* PBXTargetDependency */, + OBJ_93 /* PBXTargetDependency */, ); name = ScheduleTests; productName = ScheduleTests; @@ -299,9 +241,9 @@ }; "Schedule::SwiftPMPackageDescription" /* SchedulePackageDescription */ = { isa = PBXNativeTarget; - buildConfigurationList = OBJ_77 /* Build configuration list for PBXNativeTarget "SchedulePackageDescription" */; + buildConfigurationList = OBJ_66 /* Build configuration list for PBXNativeTarget "SchedulePackageDescription" */; buildPhases = ( - OBJ_80 /* Sources */, + OBJ_69 /* Sources */, ); buildRules = ( ); @@ -317,6 +259,7 @@ OBJ_1 /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftMigration = 9999; LastUpgradeCheck = 9999; }; buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "Schedule" */; @@ -324,15 +267,15 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = OBJ_5; - productRefGroup = OBJ_41 /* Products */; + productRefGroup = OBJ_36 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( "Schedule::Schedule" /* Schedule */, - "Schedule::ScheduleDemo" /* ScheduleDemo */, "Schedule::SwiftPMPackageDescription" /* SchedulePackageDescription */, "Schedule::SchedulePackageTests::ProductTarget" /* SchedulePackageTests */, "Schedule::ScheduleTests" /* ScheduleTests */, @@ -341,34 +284,30 @@ /* End PBXProject section */ /* Begin PBXSourcesBuildPhase section */ - OBJ_49 /* Sources */ = { + OBJ_48 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 0; files = ( - OBJ_50 /* Atomic.swift in Sources */, - OBJ_51 /* Cabinet.swift in Sources */, - OBJ_52 /* DeinitObserver.swift in Sources */, - OBJ_53 /* Deprecated.swift in Sources */, - OBJ_54 /* Extensions.swift in Sources */, - OBJ_55 /* Interval.swift in Sources */, - OBJ_56 /* Monthday.swift in Sources */, - OBJ_57 /* Period.swift in Sources */, - OBJ_58 /* Plan.swift in Sources */, - OBJ_59 /* RunLoopTask.swift in Sources */, - OBJ_60 /* Task.swift in Sources */, - OBJ_61 /* TaskCenter.swift in Sources */, - OBJ_62 /* Time.swift in Sources */, - OBJ_63 /* Timeline.swift in Sources */, - OBJ_64 /* Weekday.swift in Sources */, + OBJ_49 /* Atomic.swift in Sources */, + OBJ_50 /* Bag.swift in Sources */, + OBJ_53 /* Extensions.swift in Sources */, + OBJ_54 /* Interval.swift in Sources */, + OBJ_55 /* Monthday.swift in Sources */, + OBJ_56 /* Period.swift in Sources */, + OBJ_57 /* Plan.swift in Sources */, + OBJ_58 /* RunLoopTask.swift in Sources */, + OBJ_59 /* Task.swift in Sources */, + OBJ_60 /* TaskCenter.swift in Sources */, + OBJ_61 /* Time.swift in Sources */, + OBJ_63 /* Weekday.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - OBJ_70 /* Sources */ = { + OBJ_69 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 0; files = ( - OBJ_71 /* Log.swift in Sources */, - OBJ_72 /* main.swift in Sources */, + OBJ_70 /* Package.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -376,44 +315,34 @@ isa = PBXSourcesBuildPhase; buildActionMask = 0; files = ( - OBJ_81 /* Package.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - OBJ_91 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 0; - files = ( - OBJ_92 /* AtomicTests.swift in Sources */, - OBJ_93 /* CabinetTests.swift in Sources */, - OBJ_94 /* DateTimeTests.swift in Sources */, - OBJ_95 /* DeinitObserverTests.swift in Sources */, - OBJ_96 /* ExtensionsTests.swift in Sources */, - OBJ_97 /* Helpers.swift in Sources */, - OBJ_98 /* PlanTests.swift in Sources */, - OBJ_99 /* TaskCenterTests.swift in Sources */, - OBJ_100 /* TaskTests.swift in Sources */, - OBJ_101 /* XCTestManifests.swift in Sources */, + OBJ_81 /* AtomicTests.swift in Sources */, + OBJ_82 /* BagTests.swift in Sources */, + OBJ_83 /* TimeTests.swift in Sources */, + OBJ_85 /* ExtensionsTests.swift in Sources */, + OBJ_86 /* Helpers.swift in Sources */, + 629FAA712255D14800ED5D67 /* WeekdayTests.swift in Sources */, + 629FAA6F2255CC2700ED5D67 /* IntervalTests.swift in Sources */, + OBJ_87 /* PlanTests.swift in Sources */, + 629FAA732255D1CA00ED5D67 /* PeriodTests.swift in Sources */, + OBJ_88 /* TaskCenterTests.swift in Sources */, + 629FAA752255D6B100ED5D67 /* MonthdayTests.swift in Sources */, + OBJ_89 /* TaskTests.swift in Sources */, + OBJ_90 /* XCTestManifests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - OBJ_104 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = "Schedule::Schedule" /* Schedule */; - targetProxy = 66C8798E2237FA2F00A95D60 /* PBXContainerItemProxy */; - }; OBJ_75 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = "Schedule::Schedule" /* Schedule */; - targetProxy = 66C8798D2237FA2F00A95D60 /* PBXContainerItemProxy */; + target = "Schedule::ScheduleTests" /* ScheduleTests */; + targetProxy = 623D58302254B44900565925 /* PBXContainerItemProxy */; }; - OBJ_86 /* PBXTargetDependency */ = { + OBJ_93 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = "Schedule::ScheduleTests" /* ScheduleTests */; - targetProxy = 66C8798F2237FA3000A95D60 /* PBXContainerItemProxy */; + target = "Schedule::Schedule" /* Schedule */; + targetProxy = 623D582F2254B44800565925 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -429,8 +358,9 @@ ENABLE_NS_ASSERTIONS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", "$(inherited)", + "SWIFT_PACKAGE=1", + "DEBUG=1", ); MACOSX_DEPLOYMENT_TARGET = 10.10; ONLY_ACTIVE_ARCH = YES; @@ -438,7 +368,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "SWIFT_PACKAGE DEBUG"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE DEBUG"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; USE_HEADERMAP = NO; }; @@ -453,18 +383,22 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "SWIFT_PACKAGE=1", + ); MACOSX_DEPLOYMENT_TARGET = 10.10; OTHER_SWIFT_FLAGS = "-DXcode"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; USE_HEADERMAP = NO; }; name = Release; }; - OBJ_47 /* Debug */ = { + OBJ_46 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ENABLE_TESTABILITY = YES; @@ -485,14 +419,14 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGET_NAME = Schedule; TVOS_DEPLOYMENT_TARGET = 10.0; WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Debug; }; - OBJ_48 /* Release */ = { + OBJ_47 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ENABLE_TESTABILITY = YES; @@ -513,88 +447,44 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGET_NAME = Schedule; TVOS_DEPLOYMENT_TARGET = 10.0; WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Release; }; - OBJ_68 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PLATFORM_DIR)/Developer/Library/Frameworks", - ); - HEADER_SEARCH_PATHS = "$(inherited)"; - INFOPLIST_FILE = Schedule.xcodeproj/ScheduleDemo_Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx @executable_path"; - MACOSX_DEPLOYMENT_TARGET = 10.12; - OTHER_CFLAGS = "$(inherited)"; - OTHER_LDFLAGS = "$(inherited)"; - OTHER_SWIFT_FLAGS = "$(inherited)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; - SWIFT_FORCE_DYNAMIC_LINK_STDLIB = YES; - SWIFT_FORCE_STATIC_LINK_STDLIB = NO; - SWIFT_VERSION = 4.2; - TARGET_NAME = ScheduleDemo; - }; - name = Debug; - }; - OBJ_69 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PLATFORM_DIR)/Developer/Library/Frameworks", - ); - HEADER_SEARCH_PATHS = "$(inherited)"; - INFOPLIST_FILE = Schedule.xcodeproj/ScheduleDemo_Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx @executable_path"; - MACOSX_DEPLOYMENT_TARGET = 10.12; - OTHER_CFLAGS = "$(inherited)"; - OTHER_LDFLAGS = "$(inherited)"; - OTHER_SWIFT_FLAGS = "$(inherited)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; - SWIFT_FORCE_DYNAMIC_LINK_STDLIB = YES; - SWIFT_FORCE_STATIC_LINK_STDLIB = NO; - SWIFT_VERSION = 4.2; - TARGET_NAME = ScheduleDemo; - }; - name = Release; - }; - OBJ_78 /* Debug */ = { + OBJ_67 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { LD = /usr/bin/true; - OTHER_SWIFT_FLAGS = "-swift-version 4.2 -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"; - SWIFT_VERSION = 4.2; + 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"; + SWIFT_VERSION = 5.0; }; name = Debug; }; - OBJ_79 /* Release */ = { + OBJ_68 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { LD = /usr/bin/true; - OTHER_SWIFT_FLAGS = "-swift-version 4.2 -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"; - SWIFT_VERSION = 4.2; + 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"; + SWIFT_VERSION = 5.0; }; name = Release; }; - OBJ_84 /* Debug */ = { + OBJ_73 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Debug; }; - OBJ_85 /* Release */ = { + OBJ_74 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Release; }; - OBJ_89 /* Debug */ = { + OBJ_78 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; @@ -605,17 +495,21 @@ ); HEADER_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = Schedule.xcodeproj/ScheduleTests_Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.12; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGET_NAME = ScheduleTests; + TVOS_DEPLOYMENT_TARGET = 10.0; + WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Debug; }; - OBJ_90 /* Release */ = { + OBJ_79 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; @@ -626,13 +520,17 @@ ); HEADER_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = Schedule.xcodeproj/ScheduleTests_Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.12; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGET_NAME = ScheduleTests; + TVOS_DEPLOYMENT_TARGET = 10.0; + WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Release; }; @@ -648,47 +546,38 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - OBJ_46 /* Build configuration list for PBXNativeTarget "Schedule" */ = { + OBJ_45 /* Build configuration list for PBXNativeTarget "Schedule" */ = { isa = XCConfigurationList; buildConfigurations = ( - OBJ_47 /* Debug */, - OBJ_48 /* Release */, + OBJ_46 /* Debug */, + OBJ_47 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - OBJ_67 /* Build configuration list for PBXNativeTarget "ScheduleDemo" */ = { + OBJ_66 /* Build configuration list for PBXNativeTarget "SchedulePackageDescription" */ = { isa = XCConfigurationList; buildConfigurations = ( - OBJ_68 /* Debug */, - OBJ_69 /* Release */, + OBJ_67 /* Debug */, + OBJ_68 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - OBJ_77 /* Build configuration list for PBXNativeTarget "SchedulePackageDescription" */ = { + OBJ_72 /* Build configuration list for PBXAggregateTarget "SchedulePackageTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - OBJ_78 /* Debug */, - OBJ_79 /* Release */, + OBJ_73 /* Debug */, + OBJ_74 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - OBJ_83 /* Build configuration list for PBXAggregateTarget "SchedulePackageTests" */ = { + OBJ_77 /* Build configuration list for PBXNativeTarget "ScheduleTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - OBJ_84 /* Debug */, - OBJ_85 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - OBJ_88 /* Build configuration list for PBXNativeTarget "ScheduleTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - OBJ_89 /* Debug */, - OBJ_90 /* Release */, + OBJ_78 /* Debug */, + OBJ_79 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/Schedule.xcodeproj/xcshareddata/xcschemes/Schedule-Package.xcscheme b/Schedule.xcodeproj/xcshareddata/xcschemes/Schedule-Package.xcscheme index 09f0fee..b21fd30 100644 --- a/Schedule.xcodeproj/xcshareddata/xcschemes/Schedule-Package.xcscheme +++ b/Schedule.xcodeproj/xcshareddata/xcschemes/Schedule-Package.xcscheme @@ -20,30 +20,18 @@ ReferencedContainer = "container:Schedule.xcodeproj"> - - - - + skipped = "NO" + parallelizable = "YES"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/Schedule/Atomic.swift b/Sources/Schedule/Atomic.swift index eb9997c..7565199 100644 --- a/Sources/Schedule/Atomic.swift +++ b/Sources/Schedule/Atomic.swift @@ -1,30 +1,38 @@ import Foundation -/// A value box that can read and write the underlying value atomically. +/// An atomic box that can read and write the underlying value atomically. final class Atomic { - private var v: T + private var val: T private let lock = NSLock() + /// Create an atomic box with the given initial value. + @inline(__always) init(_ value: T) { - self.v = value + self.val = value } - /// Creates a snapshot of the value nonatomically. + /// Reads the current value atomically. @inline(__always) - func snapshot() -> T { - return v + func read(_ body: (T) -> U) -> U { + return lock.withLock { body(val) } } - /// Reads the value atomically. + /// Reads the current value atomically. @inline(__always) - func read(_ body: (T) -> U) -> U { - return lock.withLock { body(v) } + func readVoid(_ body: (T) -> Void) { + lock.withLockVoid { body(val) } } - /// Writes the value atomically. + /// Writes the current value atomically. @inline(__always) func write(_ body: (inout T) -> U) -> U { - return lock.withLock { body(&v) } + return lock.withLock { body(&val) } + } + + /// Writes the current value atomically. + @inline(__always) + func writeVoid(_ body: (inout T) -> Void) { + lock.withLockVoid { body(&val) } } } diff --git a/Sources/Schedule/Bag.swift b/Sources/Schedule/Bag.swift new file mode 100644 index 0000000..d82a648 --- /dev/null +++ b/Sources/Schedule/Bag.swift @@ -0,0 +1,96 @@ +import Foundation + +/// A unique key for removing an element from a bag. +struct BagKey: Equatable { + + fileprivate let i: UInt64 + + fileprivate init(underlying: UInt64) { + self.i = underlying + } +} + +/// A generator that can generate a sequence of unique `BagKey`. +/// +/// let k1 = gen.next() +/// let k2 = gen.next() +/// ... +struct BagKeyGenerator: Sequence, IteratorProtocol { + + typealias Element = BagKey + + private var k = BagKey(underlying: 0) + + /// Advances to the next key and returns it, or nil if no next key exists. + mutating func next() -> BagKey? { + if k.i == UInt64.max { return nil } + defer { k = BagKey(underlying: k.i + 1) } + return k + } +} + +/// An ordered sequence. +/// +/// let k1 = bag.append(e1) +/// let k2 = bag.append(e2) +/// +/// for e in bag { +/// // -> e1 +/// // -> e2 +/// } +/// +/// bag.removeValue(for: k1) +struct Bag { + + private typealias Entry = (key: BagKey, val: Element) + + private var keyGen = BagKeyGenerator() + private var entries: [Entry] = [] + + /// Appends a new element at the end of this bag. + @discardableResult + mutating func append(_ new: Element) -> BagKey { + let key = keyGen.next()! + + let entry = (key: key, val: new) + entries.append(entry) + + return key + } + + /// Returns the element associated with a given key. + func value(for key: BagKey) -> Element? { + return entries.first(where: { $0.key == key })?.val + } + + /// Removes the given key and its associated element from this bag. + @discardableResult + mutating func removeValue(for key: BagKey) -> Element? { + if let i = entries.firstIndex(where: { $0.key == key }) { + return entries.remove(at: i).val + } + return nil + } + + /// Removes all elements from this bag. + mutating func removeAll() { + entries.removeAll() + } + + /// The number of elements in this bag. + var count: Int { + return entries.count + } +} + +extension Bag: Sequence { + + /// Returns an iterator over the elements of this bag. + @inline(__always) + func makeIterator() -> AnyIterator { + var iterator = entries.makeIterator() + return AnyIterator { + return iterator.next()?.val + } + } +} diff --git a/Sources/Schedule/Cabinet.swift b/Sources/Schedule/Cabinet.swift deleted file mode 100644 index 99ba436..0000000 --- a/Sources/Schedule/Cabinet.swift +++ /dev/null @@ -1,70 +0,0 @@ -import Foundation - -struct CabinetKey: Equatable { - - private let i: UInt64 - - init(underlying: UInt64) { - self.i = underlying - } - - func increased() -> CabinetKey { - return CabinetKey(underlying: i &+ 1) - } - - static func == (lhs: CabinetKey, rhs: CabinetKey) -> Bool { - return lhs.i == rhs.i - } -} - -struct Cabinet { - - private typealias Entry = (key: CabinetKey, element: Element) - - private var key = CabinetKey(underlying: 0) - private var entries: [Entry] = [] - - @discardableResult - mutating func append(_ new: Element) -> CabinetKey { - defer { key = key.increased() } - - let entry = (key: key, element: new) - entries.append(entry) - - return key - } - - func get(_ key: CabinetKey) -> Element? { - if let entry = entries.first(where: { $0.key == key }) { - return entry.element - } - return nil - } - - @discardableResult - mutating func delete(_ key: CabinetKey) -> Element? { - if let i = entries.firstIndex(where: { $0.key == key }) { - return entries.remove(at: i).element - } - - return nil - } - - mutating func clear() { - entries.removeAll() - } - - var count: Int { - return entries.count - } -} - -extension Cabinet: Sequence { - - func makeIterator() -> AnyIterator { - var iterator = entries.makeIterator() - return AnyIterator { - return iterator.next()?.element - } - } -} diff --git a/Sources/Schedule/DeinitObserver.swift b/Sources/Schedule/DeinitObserver.swift deleted file mode 100644 index 50e684c..0000000 --- a/Sources/Schedule/DeinitObserver.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation - -#if canImport(ObjectiveC) - -private var deinitObserverKey: Void = () - -class DeinitObserver { - - private(set) weak var object: AnyObject? - - private var action: (() -> Void)? - - private init(_ action: @escaping () -> Void) { - self.action = action - } - - @discardableResult - static func observe( - _ object: AnyObject, - onDeinit action: @escaping () -> Void - ) -> DeinitObserver { - let observer = DeinitObserver(action) - observer.object = object - - objc_setAssociatedObject(object, &deinitObserverKey, observer, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - - return observer - } - - func invalidate() { - action = nil - if let o = object { - objc_setAssociatedObject(o, &deinitObserverKey, nil, .OBJC_ASSOCIATION_ASSIGN) - } - } - - deinit { - action?() - } -} - -#endif diff --git a/Sources/Schedule/Deprecated.swift b/Sources/Schedule/Deprecated.swift deleted file mode 100644 index abcbf03..0000000 --- a/Sources/Schedule/Deprecated.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// Deprecated.swift -// Schedule -// -// Created by Quentin Jin on 2019/3/4. -// Copyright © 2019 Schedule. All rights reserved. -// - -import Foundation - -extension Monthday { - - /// A Boolean value indicating whether today is the monthday. - @available(*, deprecated, message: "Use date.is(_:Monthday)") - public var isToday: Bool { - let components = toDateComponents() - - let m = Calendar.gregorian.component(.month, from: Date()) - let d = Calendar.gregorian.component(.day, from: Date()) - - return m == components.month && d == components.day - } -} - -extension Weekday { - - /// A Boolean value indicating whether today is the weekday. - @available(*, deprecated, message: "Use date.is(_:Weekday)") - public var isToday: Bool { - return Calendar.gregorian.component(.weekday, from: Date()) == rawValue - } -} - -extension Time { - - /// The interval between this time and zero o'clock. - @available(*, deprecated, renamed: "intervalSinceStartOfDay") - public var intervalSinceZeroClock: Interval { - return hour.hours + minute.minutes + second.seconds + nanosecond.nanoseconds - } -} - -extension Plan { - - /// Creates a plan from an interval sequence. - /// The task will be executed after each interval in the sequence. - @available(*, deprecated, message: "Use Plan.of") - public static func from(_ sequence: S) -> Plan where S: Sequence, S.Element == Interval { - return Plan.of(sequence) - } - - /// Creates a plan from a date sequence. - /// The task will be executed at each date in the sequence. - /// - Note: Returns `Plan.never` if given no parameters. - @available(*, deprecated, message: "Use Plan.of") - public static func from(_ sequence: S) -> Plan where S: Sequence, S.Element == Date { - return Plan.make(sequence.makeIterator) - } -} diff --git a/Sources/Schedule/Extensions.swift b/Sources/Schedule/Extensions.swift index 825cf3a..b99b1a8 100644 --- a/Sources/Schedule/Extensions.swift +++ b/Sources/Schedule/Extensions.swift @@ -2,6 +2,7 @@ import Foundation extension Double { + /// Returns a value of this number clamped to `Int.min...Int.max`. func clampedToInt() -> Int { if self >= Double(Int.max) { return Int.max } if self <= Double(Int.min) { return Int.min } @@ -11,26 +12,31 @@ extension Double { extension Int { + /// Returns the sum of the two given values, in case of any overflow, + /// the result will be clamped to int. func clampedAdding(_ other: Int) -> Int { return (Double(self) + Double(other)).clampedToInt() } +} - func clampedSubtracting(_ other: Int) -> Int { - return clampedAdding(-other) - } +extension Locale { + + static let posix = Locale(identifier: "en_US_POSIX") } extension Calendar { + /// The gregorian calendar with `en_US_POSIX` locale. static let gregorian: Calendar = { var cal = Calendar(identifier: .gregorian) - cal.locale = Locale(identifier: "en_US_POSIX") + cal.locale = Locale.posix return cal }() } extension Date { + /// Zero o'clock in the morning. var startOfToday: Date { return Calendar.gregorian.startOfDay(for: self) } @@ -38,10 +44,17 @@ extension Date { extension NSLocking { + /// Executes a closure returning a value while acquiring the lock. @inline(__always) func withLock(_ body: () throws -> T) rethrows -> T { - lock() - defer { unlock() } + lock(); defer { unlock() } return try body() } + + /// Executes a closure while acquiring the lock. + @inline(__always) + func withLockVoid(_ body: () throws -> Void) rethrows { + lock(); defer { unlock() } + try body() + } } diff --git a/Sources/Schedule/Interval.swift b/Sources/Schedule/Interval.swift index 3ab230c..d7e230b 100644 --- a/Sources/Schedule/Interval.swift +++ b/Sources/Schedule/Interval.swift @@ -1,6 +1,6 @@ import Foundation -/// `Interval` represents a length of time. +/// Type used to represent a time-based amount of time, such as '34.5 seconds'. public struct Interval { /// The length of this interval in nanoseconds. @@ -12,50 +12,27 @@ public struct Interval { } } -// MARK: - Describing +extension Interval: Hashable { } + extension Interval { - /// A boolean value indicating whether this interval is less than zero. - /// - /// An interval can be negative: + /// A Boolean value indicating whether this interval is less than zero. /// - /// - The interval from 6:00 to 7:00 is `1.hour`, - /// but the interval from 7:00 to 6:00 is `-1.hour`. - /// In this case, `-1.hour` means **one hour ago**. /// - /// - The interval comparing `3.hour` to `1.hour` is `2.hour`, - /// but the interval comparing `1.hour` to `3.hour` is `-2.hour`. - /// In this case, `-2.hour` means **two hours shorter** + /// An Interval represents a directed distance between two points + /// on the time-line and can therefore be positive, zero or negative. public var isNegative: Bool { return nanoseconds < 0 } - /// See `isNegative`. - public var isPositive: Bool { - return nanoseconds > 0 - } - - /// The absolute value of the length of this interval in nanoseconds. - public var magnitude: Double { - return nanoseconds.magnitude - } - - /// The additive inverse of this interval. - public var opposite: Interval { - return (-nanoseconds).nanoseconds - } -} - -extension Interval: Hashable { - - /// The hashValue of this interval. - public var hashValue: Int { - return nanoseconds.hashValue + /// A copy of this duration with a positive length. + public var abs: Interval { + return Interval(nanoseconds: Swift.abs(nanoseconds)) } - /// Returns a boolean value indicating whether two intervals are equal. - public static func == (lhs: Interval, rhs: Interval) -> Bool { - return lhs.nanoseconds == rhs.nanoseconds + /// A copy of this interval with the length negated. + public var negated: Interval { + return Interval(nanoseconds: -nanoseconds) } } @@ -63,9 +40,9 @@ extension Interval: CustomStringConvertible { /// A textual representation of this interval. /// - /// Interval: 1000 nanoseconds + /// "Interval: 1000 nanoseconds" public var description: String { - return "Interval: \(nanoseconds.clampedToInt()) nanoseconds" + return "Interval: \(nanoseconds.clampedToInt()) nanosecond(s)" } } @@ -73,24 +50,29 @@ extension Interval: CustomDebugStringConvertible { /// A textual representation of this interval for debugging. /// - /// Interval: 1000 nanoseconds + /// "Interval: 1000 nanoseconds" public var debugDescription: String { return description } } // MARK: - Comparing + extension Interval: Comparable { - /// Compares two intervals. + /// Compares two intervals and returns a comparison result value + /// that indicates the sort order of two intervals. /// /// A positive interval is always ordered ascending to a negative interval. public func compare(_ other: Interval) -> ComparisonResult { - let now = Date() - return now.adding(self).compare(now.adding(other)) + let d = nanoseconds - other.nanoseconds + + if d < 0 { return .orderedAscending } + if d > 0 { return .orderedDescending } + return .orderedSame } - /// Returns a boolean value indicating whether the first interval is + /// Returns a Boolean value indicating whether the first interval is /// less than the second interval. /// /// A negative interval is always less than a positive interval. @@ -98,47 +80,23 @@ extension Interval: Comparable { return lhs.compare(rhs) == .orderedAscending } - /// Returns a boolean value indicating whether this interval is longer - /// than the given value. + /// Returns a Boolean value indicating whether this interval is longer + /// than the given interval. public func isLonger(than other: Interval) -> Bool { - return magnitude > other.magnitude + return abs > other.abs } - /// Returns a boolean value indicating whether this interval is shorter - /// than the given value. + /// Returns a Boolean value indicating whether this interval is shorter + /// than the given interval. public func isShorter(than other: Interval) -> Bool { - return magnitude < other.magnitude - } - - /// Returns the longest interval of the given values. - /// - Note: Returns initialized with `init(nanoseconds: 0)` if given no parameters. - public static func longest(_ intervals: Interval...) -> Interval { - return longest(intervals)! - } - - /// Returns the longest interval of the given values. - /// - Note: Returns initialized with `init(nanoseconds: 0)` if given an empty array. - public static func longest(_ intervals: [Interval]) -> Interval? { - guard !intervals.isEmpty else { return nil } - return intervals.sorted(by: { $0.magnitude > $1.magnitude })[0] - } - - /// Returns the shortest interval of the given values. - /// - Note: Returns initialized with `init(nanoseconds: 0)` if given no parameters. - public static func shortest(_ intervals: Interval...) -> Interval { - return shortest(intervals)! - } - - /// Returns the shortest interval of the given values. - /// - Note: Returns initialized with `init(nanoseconds: 0)` if given an empty array. - public static func shortest(_ intervals: [Interval]) -> Interval? { - guard !intervals.isEmpty else { return nil } - return intervals.sorted(by: { $0.magnitude < $1.magnitude })[0] + return abs < other.abs } } // MARK: - Adding & Subtracting + extension Interval { + /// Returns a new interval by multipling this interval by the given number. /// /// 1.hour * 2 == 2.hours @@ -152,13 +110,6 @@ extension Interval { public func adding(_ other: Interval) -> Interval { return Interval(nanoseconds: nanoseconds + other.nanoseconds) } - - /// Returns a new interval by subtracting the given interval from this interval. - /// - /// 2.hours - 1.hour == 1.hour - public func subtracting(_ other: Interval) -> Interval { - return Interval(nanoseconds: nanoseconds - other.nanoseconds) - } } // MARK: - Operators @@ -182,27 +133,23 @@ extension Interval { /// /// 2.hours - 1.hour == 1.hour public static func - (lhs: Interval, rhs: Interval) -> Interval { - return lhs.subtracting(rhs) + return lhs.adding(rhs.negated) } - /// Adds two intervals and stores the result in the first interval. + /// Adds two intervals and stores the result in the left interval. public static func += (lhs: inout Interval, rhs: Interval) { lhs = lhs.adding(rhs) } /// Returns the additive inverse of the specified interval. public prefix static func - (interval: Interval) -> Interval { - return interval.opposite + return interval.negated } } // MARK: - Sugars -extension Interval { - /// Creates an interval from the given number of seconds. - public init(seconds: Double) { - self.init(nanoseconds: seconds * pow(10, 9)) - } +extension Interval { /// The length of this interval in nanoseconds. public func asNanoseconds() -> Double { @@ -253,6 +200,7 @@ public protocol IntervalConvertible { extension Int: IntervalConvertible { + /// Creates an interval from this amount of nanoseconds. public var nanoseconds: Interval { return Interval(nanoseconds: Double(self)) } @@ -260,6 +208,7 @@ extension Int: IntervalConvertible { extension Double: IntervalConvertible { + /// Creates an interval from this amount of nanoseconds. public var nanoseconds: Interval { return Interval(nanoseconds: self) } @@ -267,80 +216,96 @@ extension Double: IntervalConvertible { extension IntervalConvertible { + // Alias for `nanoseconds`. public var nanosecond: Interval { return nanoseconds } + // Alias for `microseconds`. public var microsecond: Interval { return microseconds } + /// Creates an interval from this amount of microseconds. public var microseconds: Interval { return nanoseconds * pow(10, 3) } + /// Alias for `milliseconds`. public var millisecond: Interval { return milliseconds } + /// Creates an interval from this amount of milliseconds. public var milliseconds: Interval { - return nanoseconds * pow(10, 6) + return microseconds * pow(10, 3) } + /// Alias for `second`. public var second: Interval { return seconds } + /// Creates an interval from this amount of seconds. public var seconds: Interval { - return nanoseconds * pow(10, 9) + return milliseconds * pow(10, 3) } + /// Alias for `minute`. public var minute: Interval { return minutes } + /// Creates an interval from this amount of minutes. public var minutes: Interval { return seconds * 60 } + /// Alias for `hours`. public var hour: Interval { return hours } + /// Creates an interval from this amount of hours. public var hours: Interval { return minutes * 60 } + /// Alias for `days`. public var day: Interval { return days } + /// Creates an interval from this amount of days. public var days: Interval { return hours * 24 } + /// Alias for `weeks`. public var week: Interval { return weeks } + /// Creates an interval from this amount of weeks. public var weeks: Interval { return days * 7 } } // MARK: - Date + extension Date { /// The interval between this date and the current date and time. /// - /// If this date is earlier than now, this interval will be negative. + /// If this date is earlier than now, the interval will be negative. public var intervalSinceNow: Interval { return timeIntervalSinceNow.seconds } /// Returns the interval between this date and the given date. /// - /// If this date is earlier than the given date, this interval will be negative. + /// If this date is earlier than the given date, the interval will be negative. public func interval(since date: Date) -> Interval { return timeIntervalSince(date).seconds } @@ -350,17 +315,19 @@ extension Date { return addingTimeInterval(interval.asSeconds()) } - /// Returns a date with an interval added to it. + /// Returns a new date by adding an interval to the date. public static func + (lhs: Date, rhs: Interval) -> Date { return lhs.adding(rhs) } } // MARK: - DispatchSourceTimer + extension DispatchSourceTimer { + /// Schedule this timer after the given interval. func schedule(after timeout: Interval) { - guard !timeout.isNegative else { return } + if timeout.isNegative { return } let ns = timeout.nanoseconds.clampedToInt() schedule(wallDeadline: .now() + DispatchTimeInterval.nanoseconds(ns)) } diff --git a/Sources/Schedule/Monthday.swift b/Sources/Schedule/Monthday.swift index 5a991f9..5bc29d5 100644 --- a/Sources/Schedule/Monthday.swift +++ b/Sources/Schedule/Monthday.swift @@ -1,6 +1,6 @@ import Foundation -/// `Monthday` represents a day of a month. +/// `Monthday` represents the combination of a month and day-of-month. public enum Monthday { case january(Int) @@ -27,9 +27,9 @@ public enum Monthday { case december(Int) - /// Returns a dateComponenets of the monthday, using gregorian calender and + /// Returns a dateComponenets of this monthday, using gregorian calender and /// current time zone. - public func toDateComponents() -> DateComponents { + public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents { var month, day: Int switch self { case .january(let n): month = 1; day = n @@ -47,18 +47,17 @@ public enum Monthday { } return DateComponents( calendar: Calendar.gregorian, - timeZone: TimeZone.current, + timeZone: timeZone, month: month, - day: day - ) + day: day) } } extension Date { - /// Returns a boolean value indicating whether today is the monthday. - public func `is`(_ monthday: Monthday) -> Bool { - let components = monthday.toDateComponents() + /// Returns a Boolean value indicating whether this date is the monthday in current time zone.. + public func `is`(_ monthday: Monthday, in timeZone: TimeZone = .current) -> Bool { + let components = monthday.asDateComponents(timeZone) let m = Calendar.gregorian.component(.month, from: self) let d = Calendar.gregorian.component(.day, from: self) @@ -72,7 +71,7 @@ extension Monthday: CustomStringConvertible { /// /// "Monthday: May 1st" public var description: String { - let components = toDateComponents() + let components = asDateComponents() let m = components.month! let d = components.day! @@ -80,6 +79,7 @@ extension Monthday: CustomStringConvertible { let ms = Calendar.gregorian.monthSymbols[m - 1] let fmt = NumberFormatter() + fmt.locale = Locale.posix fmt.numberStyle = .ordinal let ds = fmt.string(from: NSNumber(value: d))! diff --git a/Sources/Schedule/Period.swift b/Sources/Schedule/Period.swift index 876aca0..12aa4ef 100644 --- a/Sources/Schedule/Period.swift +++ b/Sources/Schedule/Period.swift @@ -1,6 +1,7 @@ import Foundation -/// `Period` represents a period of time defined in terms of fields. +/// Type used to represent a date-based amount of time in the ISO-8601 calendar system, +/// such as '2 years, 3 months and 4 days'. /// /// It's a little different from `Interval`: /// @@ -28,6 +29,7 @@ public struct Period { public private(set) var nanoseconds: Int + /// Initializes a period value, optional sepcifying values for its fields. public init(years: Int = 0, months: Int = 0, days: Int = 0, hours: Int = 0, minutes: Int = 0, seconds: Int = 0, nanoseconds: Int = 0) { @@ -50,104 +52,124 @@ public struct Period { /// Period.registerQuantifier("fifty", for: 15) /// let period = Period("fifty minutes") public static func registerQuantifier(_ word: String, for number: Int) { - quantifiers.write { $0[word] = number } + quantifiers.writeVoid { $0[word] = number } } - /// Creates a period from a natural expression. + /// Initializes a period from a natural expression. /// - /// Period("one second") => Period(seconds: 1) - /// Period("two hours and ten minutes") => Period(hours: 2, minutes: 10) - /// Period("1 year, 2 months and 3 days") => Period(years: 1, months: 2, days: 3) + /// Period("one second") -> Period(seconds: 1) + /// Period("two hours and ten minutes") -> Period(hours: 2, minutes: 10) + /// Period("1 year, 2 months and 3 days") -> Period(years: 1, months: 2, days: 3) public init?(_ string: String) { - var period = string + var str = string for (word, number) in Period.quantifiers.read({ $0 }) { - period = period.replacingOccurrences(of: word, with: "\(number)") + str = str.replacingOccurrences(of: word, with: "\(number)") } - guard let regex = try? NSRegularExpression(pattern: "( and |, )") else { - return nil - } - period = regex.stringByReplacingMatches(in: period, range: NSRange(location: 0, length: period.count), withTemplate: "$") - var result = 0.year - for pair in period.split(separator: "$").map({ $0.split(separator: " ") }) { - guard pair.count == 2 else { return nil } - guard let number = Int(pair[0]) else { return nil } - var unit = String(pair[1]) + // swiftlint:disable force_try + let regexp = try! NSRegularExpression(pattern: "( and |, )") + + let mark: Character = "秋" + str = regexp.stringByReplacingMatches( + in: str, + range: NSRange(str.startIndex..., in: str), + withTemplate: String(mark) + ) + + var period = 0.year + for pair in str.split(separator: mark).map({ $0.split(separator: " ") }) { + guard + pair.count == 2, + let number = Int(pair[0]) + else { + return nil + } + + var unit = pair[1] if unit.last == "s" { unit.removeLast() } switch unit { - case "year": result = result + number.years - case "month": result = result + number.months - case "day": result = result + number.days - case "week": result = result + (number * 7).days - case "hour": result = result + number.hours - case "minute": result = result + number.minutes - case "second": result = result + number.second - case "nanosecond": result = result + number.nanosecond + case "year": period = period + number.years + case "month": period = period + number.months + case "day": period = period + number.days + case "week": period = period + (number * 7).days + case "hour": period = period + number.hours + case "minute": period = period + number.minutes + case "second": period = period + number.second + case "nanosecond": period = period + number.nanosecond default: break } } - self = result.tidied(to: .day) + self = period } - /// Returns a new period by adding a period to this period. + /// Returns a new period by adding the given period to this period. public func adding(_ other: Period) -> Period { - return Period(years: years.clampedAdding(other.years), - months: months.clampedAdding(other.months), - days: days.clampedAdding(other.days), - hours: hours.clampedAdding(other.hours), - minutes: minutes.clampedAdding(other.minutes), - seconds: seconds.clampedAdding(other.seconds), - nanoseconds: nanoseconds.clampedAdding(other.nanoseconds)) + return Period( + years: years.clampedAdding(other.years), + months: months.clampedAdding(other.months), + days: days.clampedAdding(other.days), + hours: hours.clampedAdding(other.hours), + minutes: minutes.clampedAdding(other.minutes), + seconds: seconds.clampedAdding(other.seconds), + nanoseconds: nanoseconds.clampedAdding(other.nanoseconds)) } - /// Returns a new period by adding an interval to this period. + /// Returns a new period by adding an interval to the period. + /// + /// The return value will be tidied to `day` aotumatically. public func adding(_ interval: Interval) -> Period { return Period( years: years, months: months, days: days, hours: hours, minutes: minutes, seconds: seconds, - nanoseconds: nanoseconds.clampedAdding(interval.nanoseconds.clampedToInt()) - ).tidied(to: .day) + nanoseconds: nanoseconds.clampedAdding(interval.nanoseconds.clampedToInt())) + .tidied(to: .day) } - /// Adds two periods and produces their sum. + /// Returns a new period by adding the right period to the left period. + /// + /// Period(days: 1) + Period(days: 1) -> Period(days: 2) public static func + (lhs: Period, rhs: Period) -> Period { return lhs.adding(rhs) } - /// Returns a period with an interval added to it. + /// Returns a new period by adding an interval to the period. + /// + /// The return value will be tidied to `day` aotumatically. public static func + (lhs: Period, rhs: Interval) -> Period { return lhs.adding(rhs) } - /// Type to be used as tidying parameter. - public enum Unit { + /// Represents the tidy level. + public enum TideLevel { case day, hour, minute, second, nanosecond } - /// Returns a tidied period. + /// Returns the tidied period. /// /// Period(hours: 25).tidied(to .day) => Period(days: 1, hours: 1) - public func tidied(to unit: Unit) -> Period { + public func tidied(to level: TideLevel) -> Period { var period = self - if case .nanosecond = unit { return period } + + if case .nanosecond = level { return period } + if period.nanoseconds.magnitude >= UInt(1.second.nanoseconds) { period.seconds += period.nanoseconds / Int(1.second.nanoseconds) period.nanoseconds %= Int(1.second.nanoseconds) } + if case .second = level { return period } - if case .second = unit { return period } if period.seconds.magnitude >= 60 { period.minutes += period.seconds / 60 period.seconds %= 60 } + if case .minute = level { return period } - if case .minute = unit { return period } if period.minutes.magnitude >= 60 { period.hours += period.minutes / 60 period.minutes %= 60 } + if case .hour = level { return period } - if case .hour = unit { return period } if period.hours.magnitude >= 24 { period.days += period.hours / 24 period.hours %= 24 @@ -155,12 +177,20 @@ public struct Period { return period } - /// Returns a dateComponenets of the period, using gregorian calender and + /// Returns a dateComponenets of this period, using gregorian calender and /// current time zone. - public func toDateComponents() -> DateComponents { - return DateComponents(year: years, month: months, day: days, - hour: hours, minute: minutes, second: seconds, - nanosecond: nanoseconds) + public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents { + return DateComponents( + calendar: Calendar.gregorian, + timeZone: timeZone, + year: years, + month: months, + day: days, + hour: hours, + minute: minutes, + second: seconds, + nanosecond: nanoseconds + ) } } @@ -168,10 +198,10 @@ extension Date { /// Returns a new date by adding a period to this date. public func adding(_ period: Period) -> Date { - return Calendar.gregorian.date(byAdding: period.toDateComponents(), to: self) ?? .distantFuture + return Calendar.gregorian.date(byAdding: period.asDateComponents(), to: self) ?? .distantFuture } - /// Returns a date with a period added to it. + /// Returns a new date by adding a period to this date. public static func + (lhs: Date, rhs: Period) -> Date { return lhs.adding(rhs) } @@ -179,18 +209,22 @@ extension Date { extension Int { + /// Creates a period from this amount of years. public var years: Period { return Period(years: self) } + /// Alias for `years`. public var year: Period { return years } + /// Creates a period from this amount of month. public var months: Period { return Period(months: self) } + /// Alias for `month`. public var month: Period { return months } @@ -200,7 +234,7 @@ extension Period: CustomStringConvertible { /// A textual representation of this period. /// - /// Period: 1 year(s) 2 month(s) 3 day(s) + /// "Period: 1 year(s) 2 month(s) 3 day(s)" public var description: String { let period = tidied(to: .day) var desc = "Period:" @@ -219,7 +253,7 @@ extension Period: CustomDebugStringConvertible { /// A textual representation of this period for debugging. /// - /// Period: 1 year(s) 2 month(s) 3 day(s) + /// "Period: 1 year(s) 2 month(s) 3 day(s)" public var debugDescription: String { return description } diff --git a/Sources/Schedule/Plan.swift b/Sources/Schedule/Plan.swift index f2c3482..987c4ce 100644 --- a/Sources/Schedule/Plan.swift +++ b/Sources/Schedule/Plan.swift @@ -1,50 +1,54 @@ import Foundation -/// `Plan` represents a plan that gives time at which a task should be +/// `Plan` represents a sequence of times at which a task should be /// executed. /// /// `Plan` is `Interval` based. -public struct Plan { +public struct Plan: Sequence { - private var iSeq: AnySequence + private var seq: AnySequence private init(_ sequence: S) where S: Sequence, S.Element == Interval { - iSeq = AnySequence(sequence) + seq = AnySequence(sequence) } - func makeIterator() -> AnyIterator { - return iSeq.makeIterator() + /// Returns an iterator over the interval of this sequence. + public func makeIterator() -> AnyIterator { + return seq.makeIterator() } /// Schedules a task with this plan. /// /// - Parameters: - /// - queue: The queue to which the task will be dispatched. - /// - onElapse: The action to do when time is out. + /// - queue: The dispatch queue to which the action should be dispatched. + /// - action: A block to be executed when time is up. /// - Returns: The task just created. - public func `do`(queue: DispatchQueue, - onElapse: @escaping (Task) -> Void) -> Task { - return Task(plan: self, queue: queue, onElapse: onElapse) + public func `do`( + queue: DispatchQueue, + action: @escaping (Task) -> Void + ) -> Task { + return Task(plan: self, queue: queue, action: action) } /// Schedules a task with this plan. /// /// - Parameters: - /// - queue: The queue to which the task will be dispatched. - /// - onElapse: The action to do when time is out. + /// - queue: The dispatch queue to which the action should be dispatched. + /// - action: A block to be executed when time is up. /// - Returns: The task just created. - public func `do`(queue: DispatchQueue, - onElapse: @escaping () -> Void) -> Task { - return self.do(queue: queue, onElapse: { (_) in onElapse() }) + public func `do`( + queue: DispatchQueue, + action: @escaping () -> Void + ) -> Task { + return self.do(queue: queue, action: { (_) in action() }) } } extension Plan { - /// Creates a plan from a `makeUnderlyingIterator()` method. + /// Creates a plan whose `makeIterator()` method forwards to makeUnderlyingIterator. /// - /// The task will be executed after each interval produced by the iterator - /// that `makeUnderlyingIterator` returns. + /// The task will be executed after each interval. /// /// For example: /// @@ -52,11 +56,11 @@ extension Plan { /// var i = 0 /// return AnyIterator { /// i += 1 - /// return i + /// return i // 1, 2, 3, ... /// } /// } /// plan.do { - /// print(Date()) + /// logTimestamp() /// } /// /// > "2001-01-01 00:00:00" @@ -64,20 +68,22 @@ extension Plan { /// > "2001-01-01 00:00:03" /// > "2001-01-01 00:00:06" /// ... - public static func make(_ makeUnderlyingIterator: @escaping () -> I) -> Plan where I: IteratorProtocol, I.Element == Interval { + public static func make( + _ makeUnderlyingIterator: @escaping () -> I + ) -> Plan where I: IteratorProtocol, I.Element == Interval { return Plan(AnySequence(makeUnderlyingIterator)) } /// Creates a plan from a list of intervals. + /// /// The task will be executed after each interval in the array. - /// - Note: Returns `Plan.never` if given no parameters. public static func of(_ intervals: Interval...) -> Plan { return Plan.of(intervals) } /// Creates a plan from a list of intervals. + /// /// The task will be executed after each interval in the array. - /// - Note: Returns `Plan.never` if given an empty array. public static func of(_ intervals: S) -> Plan where S: Sequence, S.Element == Interval { return Plan(intervals) } @@ -85,10 +91,9 @@ extension Plan { extension Plan { - /// Creates a plan from a `makeUnderlyingIterator()` method. + /// Creates a plan whose `makeIterator()` method forwards to makeUnderlyingIterator. /// - /// The task will be executed at each date - /// produced by the iterator that `makeUnderlyingIterator` returns. + /// The task will be executed at each date. /// /// For example: /// @@ -97,40 +102,44 @@ extension Plan { /// return Date().addingTimeInterval(3) /// } /// } - /// print("now:", Date()) + /// /// plan.do { - /// print("task", Date()) + /// logTimestamp() /// } /// - /// > "now: 2001-01-01 00:00:00" - /// > "task: 2001-01-01 00:00:03" + /// > "2001-01-01 00:00:00" + /// > "2001-01-01 00:00:03" + /// > "2001-01-01 00:00:06" + /// > "2001-01-01 00:00:09" /// ... /// - /// You are not supposed to return `Date()` in making interator. - /// If you want to execute a task immediately, - /// use `Plan.now` then `concat` another plan instead. - public static func make(_ makeUnderlyingIterator: @escaping () -> I) -> Plan where I: IteratorProtocol, I.Element == Date { + /// You should not return `Date()` in making iterator. + /// If you want to execute a task immediately, use `Plan.now`. + public static func make( + _ makeUnderlyingIterator: @escaping () -> I + ) -> Plan where I: IteratorProtocol, I.Element == Date { return Plan.make { () -> AnyIterator in var iterator = makeUnderlyingIterator() - var last: Date! + var prev: Date! return AnyIterator { - last = last ?? Date() + prev = prev ?? Date() guard let next = iterator.next() else { return nil } - defer { last = next } - return next.interval(since: last) + defer { prev = next } + return next.interval(since: prev) } } } /// Creates a plan from a list of dates. + /// /// The task will be executed at each date in the array. public static func of(_ dates: Date...) -> Plan { return Plan.of(dates) } /// Creates a plan from a list of dates. + /// /// The task will be executed at each date in the array. - /// - Note: Returns `Plan.never` if given no parameters. public static func of(_ sequence: S) -> Plan where S: Sequence, S.Element == Date { return Plan.make(sequence.makeIterator) } @@ -139,13 +148,13 @@ extension Plan { public var dates: AnySequence { return AnySequence { () -> AnyIterator in let iterator = self.makeIterator() - var last: Date! + var prev: Date! return AnyIterator { - last = last ?? Date() + prev = prev ?? Date() guard let interval = iterator.next() else { return nil } // swiftlint:disable shorthand_operator - last = last + interval - return last + prev = prev + interval + return prev } } } @@ -153,27 +162,27 @@ extension Plan { extension Plan { - /// A plan with a distant past date. + /// A plan of a distant past date. public static var distantPast: Plan { return Plan.of(Date.distantPast) } - /// A plan with a distant future date. + /// A plan of a distant future date. public static var distantFuture: Plan { return Plan.of(Date.distantFuture) } - /// A plan that is never going to happen. + /// A plan that will never happen. public static var never: Plan { return Plan.make { - AnyIterator { nil } + AnyIterator { nil } } } } extension Plan { - /// Returns a new plan by concatenating a plan to this plan. + /// Returns a new plan by concatenating the given plan to this plan. /// /// For example: /// @@ -194,40 +203,44 @@ extension Plan { } } - /// Returns a new plan by merging a plan to this plan. + /// Returns a new plan by merging the given plan to this plan. /// /// For example: /// /// let s0 = Plan.of(1.second, 3.seconds, 5.seconds) /// let s1 = Plan.of(2.seconds, 4.seconds, 6.seconds) - /// let s2 = s0.concat(s1) + /// let s2 = s0.merge(s1) /// > s2 /// > 1.second, 1.seconds, 2.seconds, 2.seconds, 3.seconds, 3.seconds public func merge(_ plan: Plan) -> Plan { return Plan.make { () -> AnyIterator in let i0 = self.dates.makeIterator() let i1 = plan.dates.makeIterator() - var buffer0: Date! - var buffer1: Date! + + var buf0: Date! + var buf1: Date! + return AnyIterator { - if buffer0 == nil { buffer0 = i0.next() } - if buffer1 == nil { buffer1 = i1.next() } + if buf0 == nil { buf0 = i0.next() } + if buf1 == nil { buf1 = i1.next() } var d: Date! - if let d0 = buffer0, let d1 = buffer1 { + if let d0 = buf0, let d1 = buf1 { d = Swift.min(d0, d1) } else { - d = buffer0 ?? buffer1 + d = buf0 ?? buf1 } - if d == buffer0 { buffer0 = nil } - if d == buffer1 { buffer1 = nil } + if d == nil { return d } + + if d == buf0 { buf0 = nil; return d } + if d == buf1 { buf1 = nil } return d } } } - /// Returns a new plan by only taking the first specific number of this plan. + /// Returns a new plan by taking the first specific number of intervals from this plan. /// /// For example: /// @@ -247,70 +260,69 @@ extension Plan { } } - /// Returns a new plan by only taking the part before the date. + /// Returns a new plan by taking the part before the given date. public func until(_ date: Date) -> Plan { return Plan.make { () -> AnyIterator in let iterator = self.dates.makeIterator() return AnyIterator { guard let next = iterator.next(), next < date else { - return nil + return nil } return next } } } -} - -extension Plan { /// Creates a plan that executes the task immediately. public static var now: Plan { return Plan.of(0.nanosecond) } - /// Creates a plan that executes the task after delay. + /// Creates a plan that executes the task after the given interval. public static func after(_ delay: Interval) -> Plan { return Plan.of(delay) } - /// Creates a plan that executes the task every interval. - public static func every(_ interval: Interval) -> Plan { - return Plan.make { - AnyIterator { interval } - } - } - - /// Creates a plan that executes the task after delay then repeat - /// every interval. + /// Creates a plan that executes the task after the given interval then repeat the execution. public static func after(_ delay: Interval, repeating interval: Interval) -> Plan { return Plan.after(delay).concat(Plan.every(interval)) } - /// Creates a plan that executes the task at the specific date. + /// Creates a plan that executes the task at the given date. public static func at(_ date: Date) -> Plan { return Plan.of(date) } - /// Creates a plan that executes the task every period. + /// Creates a plan that executes the task every given interval. + public static func every(_ interval: Interval) -> Plan { + return Plan.make { + AnyIterator { interval } + } + } + + /// Creates a plan that executes the task every given period. public static func every(_ period: Period) -> Plan { return Plan.make { () -> AnyIterator in let calendar = Calendar.gregorian - var last: Date! + var prev: Date! return AnyIterator { - last = last ?? Date() - guard let next = calendar.date(byAdding: period.toDateComponents(), - to: last) else { + prev = prev ?? Date() + guard + let next = calendar.date( + byAdding: period.asDateComponents(), + to: prev) + else { return nil } - defer { last = next } - return next.interval(since: last) + defer { prev = next } + return next.interval(since: prev) } } } /// Creates a plan that executes the task every period. /// - /// See Period's constructor + /// See Period's constructor: `init?(_ string: String)`. public static func every(_ period: String) -> Plan { guard let p = Period(period) else { return Plan.never @@ -322,16 +334,16 @@ extension Plan { extension Plan { /// `DateMiddleware` represents a middleware that wraps a plan - /// which was only specified date without time. + /// which was only specified with date without time. /// - /// You should call `at` method to get the plan with time specified. + /// You should call `at` method to specified time of the plan. public struct DateMiddleware { fileprivate let plan: Plan - /// Returns a plan at the specific time. + /// Creates a plan with time specified. public func at(_ time: Time) -> Plan { - guard !self.plan.isNever() else { return .never } + if plan.isNever() { return .never } var interval = time.intervalSinceStartOfDay return Plan.make { () -> AnyIterator in @@ -346,56 +358,54 @@ extension Plan { } } - /// Returns a plan at the specific time. + /// Creates a plan with time specified. /// - /// See Time's constructor + /// See Time's constructor: `init?(_ string: String)`. public func at(_ time: String) -> Plan { - guard - !self.plan.isNever(), - let time = Time(time) - else { + if plan.isNever() { return .never } + guard let time = Time(time) else { return .never } - return at(time) } - /// Returns a plan at the specific time. + /// Creates a plan with time specified. /// /// .at(1) => 01 /// .at(1, 2) => 01:02 /// .at(1, 2, 3) => 01:02:03 /// .at(1, 2, 3, 456) => 01:02:03.456 - /// - /// - Note: Returns `Plan.never` if given no parameters. public func at(_ time: Int...) -> Plan { return self.at(time) } - /// Returns a plan at the specific time. + /// Creates a plan with time specified. /// /// .at([1]) => 01 /// .at([1, 2]) => 01:02 /// .at([1, 2, 3]) => 01:02:03 /// .at([1, 2, 3, 456]) => 01:02:03.456 - /// - /// - Note: Returns `Plan.never` if given an empty array. public func at(_ time: [Int]) -> Plan { - guard !time.isEmpty, !self.plan.isNever() else { return .never } + if plan.isNever() || time.isEmpty { return .never } let hour = time[0] let minute = time.count > 1 ? time[1] : 0 let second = time.count > 2 ? time[2] : 0 let nanosecond = time.count > 3 ? time[3]: 0 - guard let time = Time(hour: hour, minute: minute, second: second, nanosecond: nanosecond) else { + guard let time = Time( + hour: hour, + minute: minute, + second: second, + nanosecond: nanosecond + ) else { return Plan.never } return at(time) } } - /// Creates a plan that executes the task every specific weekday. + /// Creates a date middleware that executes the task on every specific week day. public static func every(_ weekday: Weekday) -> DateMiddleware { let plan = Plan.make { () -> AnyIterator in let calendar = Calendar.gregorian @@ -406,7 +416,7 @@ extension Plan { } else if Date().is(weekday) { date = Date().startOfToday } else { - let components = weekday.toDateComponents() + let components = weekday.asDateComponents() date = calendar.nextDate(after: Date(), matching: components, matchingPolicy: .strict) } return date @@ -415,27 +425,23 @@ extension Plan { return DateMiddleware(plan: plan) } - /// Creates a plan that executes the task every specific weekdays. - /// - Note: Returns initialized with `Plan.never` if given no parameters. + /// Creates a date middleware that executes the task on every specific week day. public static func every(_ weekdays: Weekday...) -> DateMiddleware { return Plan.every(weekdays) } - /// Creates a plan that executes the task every specific weekdays. - /// - Note: Returns initialized with `Plan.never` if given an empty array. + /// Creates a date middleware that executes the task on every specific week day. public static func every(_ weekdays: [Weekday]) -> DateMiddleware { guard !weekdays.isEmpty else { return .init(plan: .never) } var plan = every(weekdays[0]).plan - if weekdays.count > 1 { - for i in 1.. DateMiddleware { let plan = Plan.make { () -> AnyIterator in let calendar = Calendar.gregorian @@ -445,8 +451,8 @@ extension Plan { date = calendar.date(byAdding: .year, value: 1, to: d) } else if Date().is(monthday) { date = Date().startOfToday - } else { - let components = monthday.toDateComponents() + } else { + let components = monthday.asDateComponents() date = calendar.nextDate(after: Date(), matching: components, matchingPolicy: .strict) } return date @@ -455,34 +461,33 @@ extension Plan { return DateMiddleware(plan: plan) } - /// Creates a plan that executes the task every specific days in the months. - /// - Note: Returns initialized with `Plan.never` if given no parameters. + /// Creates a date middleware that executes the task on every specific month day. public static func every(_ mondays: Monthday...) -> DateMiddleware { return Plan.every(mondays) } - /// Creates a plan that executes the task every specific days in the months. - /// - Note: Returns initialized with `Plan.never` if given an empty array. + /// Creates a date middleware that executes the task on every specific month day. public static func every(_ mondays: [Monthday]) -> DateMiddleware { guard !mondays.isEmpty else { return .init(plan: .never) } var plan = every(mondays[0]).plan - if mondays.count > 1 { - for i in 1.. Bool { - return self.iSeq.makeIterator().next() == nil + return seq.makeIterator().next() == nil } } extension Plan { + /// Creates a new plan that is offset by the specified interval in the /// closure body. /// @@ -491,23 +496,15 @@ extension Plan { /// /// If the returned interval offset is `nil`, then no offset is added /// to that next-run date. - public func offset(by intervalOffset: @escaping () -> Interval?) -> Plan { + public func offset(by interval: @autoclosure @escaping () -> Interval?) -> Plan { return Plan.make { () -> AnyIterator in let it = self.makeIterator() return AnyIterator { if let next = it.next() { - return next + (intervalOffset() ?? 0.second) + return next + (interval() ?? 0.second) } return nil } } } - - /// Creates a new plan that is offset by the specified interval. - /// - /// If the specified interval offset is `nil`, then no offset is - /// added to the plan (ie. it stays the same). - public func offset(by intervalOffset: Interval?) -> Plan { - return self.offset(by: { intervalOffset }) - } } diff --git a/Sources/Schedule/RunLoopTask.swift b/Sources/Schedule/RunLoopTask.swift index d9c407a..7b97b77 100644 --- a/Sources/Schedule/RunLoopTask.swift +++ b/Sources/Schedule/RunLoopTask.swift @@ -5,38 +5,42 @@ extension Plan { /// Schedules a task with this plan. /// /// When time is up, the task will be executed on current thread. It behaves - /// like a `Timer`, so you have to make sure the current thread has a - /// runloop available. + /// like a `Timer`, so you need to make sure that the current thread has a + /// available runloop. /// - /// Since this method relies on run loop, it is recommended to use + /// Since this method relies on run loop, it is remove recommended to use /// `do(queue: _, onElapse: _)`. /// /// - Parameters: - /// - mode: The mode in which to add the task. - /// - onElapse: The action to do when time is out. + /// - mode: The mode to which the action should be added. + /// - action: A block to be executed when time is up. /// - Returns: The task just created. - public func `do`(mode: RunLoop.Mode = .common, - onElapse: @escaping (Task) -> Void) -> Task { - return RunLoopTask(plan: self, mode: mode, onElapse: onElapse) + public func `do`( + mode: RunLoop.Mode = .common, + action: @escaping (Task) -> Void + ) -> Task { + return RunLoopTask(plan: self, mode: mode, action: action) } /// Schedules a task with this plan. /// /// When time is up, the task will be executed on current thread. It behaves - /// like a `Timer`, so you have to make sure the current thread has a - /// runloop available. + /// like a `Timer`, so you need to make sure that the current thread has a + /// available runloop. /// - /// Since this method relies on run loop, it is recommended to use + /// Since this method relies on run loop, it is remove recommended to use /// `do(queue: _, onElapse: _)`. /// /// - Parameters: - /// - mode: The mode in which to add the task. - /// - onElapse: The action to do when time is out. + /// - mode: The mode to which the action should be added. + /// - action: A block to be executed when time is up. /// - Returns: The task just created. - public func `do`(mode: RunLoop.Mode = .common, - onElapse: @escaping () -> Void) -> Task { - return self.do(mode: mode) { (_) in - onElapse() + public func `do`( + mode: RunLoop.Mode = .common, + action: @escaping () -> Void + ) -> Task { + return self.do(mode: mode) { _ in + action() } } } @@ -45,24 +49,26 @@ private final class RunLoopTask: Task { var timer: Timer! - init(plan: Plan, mode: RunLoop.Mode, onElapse: @escaping (Task) -> Void) { - - weak var this: Task? - - let distant = Date.distantFuture.timeIntervalSinceReferenceDate - timer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, distant, distant, 0, 0) { _ in - guard let task = this else { return } - onElapse(task) + init( + plan: Plan, + mode: RunLoop.Mode, + action: @escaping (Task) -> Void + ) { + super.init(plan: plan, queue: nil) { (task) in + guard let task = task as? RunLoopTask, let timer = task.timer else { return } + timer.fireDate = Date() } - RunLoop.current.add(timer, forMode: mode) - - super.init(plan: plan, queue: nil) { (task) in - guard let task = task as? RunLoopTask else { return } - task.timer.fireDate = Date() + timer = Timer( + fire: Date.distantFuture, + interval: .greatestFiniteMagnitude, + repeats: false + ) { [weak self] _ in + guard let self = self else { return } + action(self) } - this = self + RunLoop.current.add(timer, forMode: mode) } deinit { diff --git a/Sources/Schedule/Task.swift b/Sources/Schedule/Task.swift index 85a546b..d0dafd6 100644 --- a/Sources/Schedule/Task.swift +++ b/Sources/Schedule/Task.swift @@ -3,56 +3,145 @@ import Foundation /// `ActionKey` represents a token that can be used to remove the action. public struct ActionKey { - fileprivate let cabinetKey: CabinetKey + fileprivate let bagKey: BagKey } -extension CabinetKey { +extension BagKey { - func asActionKey() -> ActionKey { - return ActionKey(cabinetKey: self) + fileprivate func asActionKey() -> ActionKey { + return ActionKey(bagKey: self) } } -/// `Task` represents a timed task. +/// `Task` represents a timing task. open class Task { - public typealias Action = (Task) -> Void + // MARK: - Private properties - private let _mutex = NSRecursiveLock() + private let _lock = NSRecursiveLock() private var _iterator: AnyIterator - private var _timer: DispatchSourceTimer + private let _timer: DispatchSourceTimer + + private lazy var _actions = Bag() + + private lazy var _suspensionCount: Int = 0 + private lazy var _executionCount: Int = 0 + + private lazy var _executionDates: [Date]? = nil + private lazy var _estimatedNextExecutionDate: Date? = nil - private lazy var _onElapseActions = Cabinet() - private lazy var _onDeinitActions = Cabinet() + private weak var _taskCenter: TaskCenter? + private let _taskCenterLock = NSRecursiveLock() - private lazy var _suspensions: UInt64 = 0 - private lazy var _timeline = Timeline() + private var associateKey = 1 + + // MARK: - Public properties + + /// The unique id of this task. + public let id = UUID() + + public typealias Action = (Task) -> Void - private lazy var _countOfExecutions: Int = 0 + /// The date of creation. + public let creationDate = Date() - private lazy var _lifetime: Interval = Int.max.seconds - private lazy var _lifetimeTimer: DispatchSourceTimer = { - let timer = DispatchSource.makeTimerSource() - timer.setEventHandler { - self.cancel() + /// The date of first execution. + open var firstExecutionDate: Date? { + return _lock.withLock { _executionDates?.first } + } + + /// The date of last execution. + open var lastExecutionDate: Date? { + return _lock.withLock { _executionDates?.last } + } + + /// Histories of executions. + open var executionDates: [Date]? { + return _lock.withLock { _executionDates } + } + + /// The date of estimated next execution. + open var estimatedNextExecutionDate: Date? { + return _lock.withLock { _estimatedNextExecutionDate } + } + + /// The number of task executions. + public var executionCount: Int { + return _lock.withLock { + _executionCount + } + } + + /// The number of task suspensions. + public var suspensionCount: Int { + return _lock.withLock { + _suspensionCount + } + } + + /// The number of actions in this task. + public var actionCount: Int { + return _lock.withLock { + _actions.count + } + } + + /// A Boolean indicating whether the task was canceled. + public var isCancelled: Bool { + return _lock.withLock { + _timer.isCancelled } - timer.schedule(after: _lifetime) - timer.resume() - return timer - }() + } + + /// The task center to which this task currently belongs. + open var taskCenter: TaskCenter? { + return _lock.withLock { _taskCenter } + } + + // MARK: - Task center - open internal(set) weak var taskCenter: TaskCenter? - let taskCenterMutex = NSRecursiveLock() + /// Adds this task to the given task center. + func addToTaskCenter(_ center: TaskCenter) { + _taskCenterLock.lock() + defer { _taskCenterLock.unlock() } + + if _taskCenter === center { return } + + let c = _taskCenter + _taskCenter = center + + c?.remove(self) + } - init(plan: Plan, - queue: DispatchQueue?, - onElapse: @escaping (Task) -> Void) { + /// Removes this task from the given task center. + func removeFromTaskCenter(_ center: TaskCenter) { + _taskCenterLock.lock() + defer { _taskCenterLock.unlock() } + if _taskCenter !== center { return } + + _taskCenter = nil + center.remove(self) + } + + // MARK: - Init + + /// Initializes a timing task. + /// + /// - Parameters: + /// - plan: The plan. + /// - queue: The dispatch queue to which the action should be dispatched. + /// - action: A block to be executed when time is up. + init( + plan: Plan, + queue: DispatchQueue?, + action: @escaping (Task) -> Void + ) { _iterator = plan.makeIterator() _timer = DispatchSource.makeTimerSource(queue: queue) - _onElapseActions.append(onElapse) + _actions.append(action) _timer.setEventHandler { [weak self] in guard let self = self else { return } @@ -61,254 +150,133 @@ open class Task { if let interval = _iterator.next(), !interval.isNegative { _timer.schedule(after: interval) - _timeline.estimatedNextExecution = Date().adding(interval) + _estimatedNextExecutionDate = Date().adding(interval) } _timer.resume() + TaskCenter.default.add(self) } deinit { - for action in _onDeinitActions { - action(self) - } - - while _suspensions > 0 { + print("deinit") + while _suspensionCount > 0 { _timer.resume() - _suspensions -= 1 + _suspensionCount -= 1 } - cancel() + taskCenter?.remove(self) + } + + private func elapse() { + scheduleNextExecution() + executeNow() } - private func scheduleNext() { - _mutex.withLock { + private func scheduleNextExecution() { + _lock.withLockVoid { let now = Date() - var estimated = _timeline.estimatedNextExecution ?? now + var estimated = _estimatedNextExecutionDate ?? now repeat { guard let interval = _iterator.next(), !interval.isNegative else { - _timeline.estimatedNextExecution = nil + _estimatedNextExecutionDate = nil return } estimated = estimated.adding(interval) } while (estimated < now) - _timeline.estimatedNextExecution = estimated - _timer.schedule(after: _timeline.estimatedNextExecution!.interval(since: now)) + _estimatedNextExecutionDate = estimated + _timer.schedule(after: _estimatedNextExecutionDate!.interval(since: now)) } } - /// Execute this task now, without disrupting its plan. - public func execute() { - let actions = _mutex.withLock { () -> Cabinet in + /// Execute this task now, without interrupting its plan. + open func executeNow() { + let actions = _lock.withLock { () -> Bag in let now = Date() - if _timeline.firstExecution == nil { - _timeline.firstExecution = now + if _executionDates == nil { + _executionDates = [now] + } else { + _executionDates?.append(now) } - _timeline.lastExecution = now - _countOfExecutions += 1 - return _onElapseActions + _executionCount += 1 + return _actions } actions.forEach { $0(self) } } - private func elapse() { - scheduleNext() - execute() - } - - #if canImport(ObjectiveC) - open func host(on target: AnyObject) { - DeinitObserver.observe(target) { [weak self] in - self?.cancel() - } - } - #endif - - /// The number of times the task has been executed. - public var countOfExecutions: Int { - return _mutex.withLock { - _countOfExecutions - } - } - - /// A Boolean indicating whether the task was canceled. - public var isCancelled: Bool { - return _mutex.withLock { - _timer.isCancelled - } - } - - // MARK: - Manage + // MARK: - Features /// Reschedules this task with the new plan. public func reschedule(_ new: Plan) { - _mutex.withLock { - _iterator = new.makeIterator() - } - scheduleNext() - } + _lock.withLockVoid { + if _timer.isCancelled { return } - /// Suspensions of this task. - public var suspensions: UInt64 { - return _mutex.withLock { - _suspensions + _iterator = new.makeIterator() } + scheduleNextExecution() } /// Suspends this task. public func suspend() { - _mutex.withLock { - if _suspensions < UInt64.max { + _lock.withLockVoid { + if _timer.isCancelled { return } + + if _suspensionCount < UInt64.max { _timer.suspend() - _suspensions += 1 + _suspensionCount += 1 } } } /// Resumes this task. public func resume() { - _mutex.withLock { - if _suspensions > 0 { + _lock.withLockVoid { + if _timer.isCancelled { return } + + if _suspensionCount > 0 { _timer.resume() - _suspensions -= 1 + _suspensionCount -= 1 } } } /// Cancels this task. public func cancel() { - _mutex.withLock { + _lock.withLockVoid { _timer.cancel() - } - TaskCenter.default.remove(self) - } - - @discardableResult - open func onDeinit(_ body: @escaping Action) -> ActionKey { - return _mutex.withLock { - return _onDeinitActions.append(body).asActionKey() - } - } - - // MARK: - Lifecycle - - /// The snapshot timeline of this task. - public var timeline: Timeline { - return _mutex.withLock { - _timeline - } - } - - /// The lifetime of this task. - public var lifetime: Interval { - return _mutex.withLock { - _lifetime - } - } - - /// The rest of lifetime. - public var restOfLifetime: Interval { - return _mutex.withLock { - _lifetime - Date().interval(since: _timeline.initialization) - } - } - - /// Set a new lifetime for this task. - /// - /// If this task has already ended its lifetime, setting will fail, - /// if new lifetime is shorter than its age, setting will fail, too. - /// - /// - Returns: `true` if set successfully, `false` if not. - @discardableResult - public func setLifetime(_ interval: Interval) -> Bool { - guard restOfLifetime.isPositive else { return false } - - _mutex.lock() - let age = Date().interval(since: _timeline.initialization) - guard age.isShorter(than: interval) else { - _mutex.unlock() - return false - } - - _lifetime = interval - _lifetimeTimer.schedule(after: interval - age) - _mutex.unlock() - return true - } - - /// Add an interval to this task's lifetime. - /// - /// If this task has already ended its lifetime, adding will fail, - /// if new lifetime is shorter than its age, adding will fail, too. - /// - /// - Returns: `true` if set successfully, `false` if not. - @discardableResult - public func addLifetime(_ interval: Interval) -> Bool { - var rest = restOfLifetime - guard rest.isPositive else { return false } - rest += interval - guard rest.isPositive else { return false } - _mutex.withLock { - _lifetime += interval - _lifetimeTimer.schedule(after: rest) - } - return true - } - - /// Subtract an interval to this task's lifetime. - /// - /// If this task has already ended its lifetime, subtracting will fail, - /// if new lifetime is shorter than its age, subtracting will fail, too. - /// - /// - Returns: `true` if set successfully, `false` if not. - @discardableResult - public func subtractLifetime(_ interval: Interval) -> Bool { - return addLifetime(interval.opposite) - } - - // MARK: - Action - - /// The number of actions in this task. - public var countOfActions: Int { - return _mutex.withLock { - _onElapseActions.count + _suspensionCount = 0 } } /// Adds action to this task. @discardableResult public func addAction(_ action: @escaping (Task) -> Void) -> ActionKey { - return _mutex.withLock { - return _onElapseActions.append(action).asActionKey() + return _lock.withLock { + return _actions.append(action).asActionKey() } } /// Removes action by key from this task. public func removeAction(byKey key: ActionKey) { - _mutex.withLock { - _ = _onElapseActions.delete(key.cabinetKey) + _lock.withLockVoid { + _ = _actions.removeValue(for: key.bagKey) } } /// Removes all actions from this task. public func removeAllActions() { - _mutex.withLock { - _onElapseActions.clear() + _lock.withLockVoid { + _actions.removeAll() } } - - // MARK: - Tag - open func add(to: TaskCenter) { - _mutex.lock() - } } extension Task: Hashable { - /// The task's hash value. - public var hashValue: Int { - return ObjectIdentifier(self).hashValue + /// Hashes the essential components of this value by feeding them into the given hasher. + public func hash(into hasher: inout Hasher) { + hasher.combine(id) } /// Returns a boolean value indicating whether two tasks are equal. @@ -316,24 +284,3 @@ extension Task: Hashable { return lhs === rhs } } - -extension Task: CustomStringConvertible { - - /// A textual representation of this task. - public var description: String { - return "Task: { " + - "\"isCancelled\": \(_timer.isCancelled), " + - "\"countOfActions\": \(_onElapseActions.count), " + - "\"countOfExecutions\": \(_countOfExecutions), " + - "\"timeline\": \(_timeline)" + - " }" - } -} - -extension Task: CustomDebugStringConvertible { - - /// A textual representation of this task for debugging. - public var debugDescription: String { - return description - } -} diff --git a/Sources/Schedule/TaskCenter.swift b/Sources/Schedule/TaskCenter.swift index e164112..d847f59 100644 --- a/Sources/Schedule/TaskCenter.swift +++ b/Sources/Schedule/TaskCenter.swift @@ -8,157 +8,168 @@ extension TaskCenter { weak var task: Task? - // To find slot - let hashValue: Int + // Used to find slot in dictionary/set + let hash: Int init(_ task: Task) { self.task = task - self.hashValue = task.hashValue + self.hash = task.hashValue } - // To find task + func hash(into hasher: inout Hasher) { + hasher.combine(hash) + } + + // Used to find task in a slot in dictionary/set static func == (lhs: TaskBox, rhs: TaskBox) -> Bool { return lhs.task == rhs.task } } } +/// A task center that enables batch operation. open class TaskCenter { - private let mutex = NSRecursiveLock() + private let lock = NSLock() - private var taskMap: [String: Set] = [:] - private var tagMap: [TaskBox: Set] = [:] + private var tags: [String: Set] = [:] + private var tasks: [TaskBox: Set] = [:] + /// Default task center. open class var `default`: TaskCenter { return _default } + /// Adds the given task to this center. open func add(_ task: Task) { - task.taskCenterMutex.lock() - defer { - task.taskCenterMutex.unlock() - } + task.addToTaskCenter(self) - if let center = task.taskCenter { - if center === self { return } - center.remove(task) - } - task.taskCenter = self - - mutex.withLock { + lock.withLockVoid { let box = TaskBox(task) - tagMap[box] = [] - } - - task.onDeinit { [weak self] (t) in - guard let self = self else { return } - self.remove(t) + self.tasks[box] = [] } } + /// Removes the given task from this center. open func remove(_ task: Task) { - task.taskCenterMutex.lock() - defer { - task.taskCenterMutex.unlock() - } - - guard task.taskCenter === self else { - return - } - task.taskCenter = nil + task.removeFromTaskCenter(self) - mutex.withLock { + lock.withLockVoid { let box = TaskBox(task) - if let tags = self.tagMap[box] { + if let tags = self.tasks[box] { for tag in tags { - self.taskMap[tag]?.remove(box) + self.tags[tag]?.remove(box) + if self.tags[tag]?.count == 0 { + self.tags[tag] = nil + } } - self.tagMap[box] = nil + self.tasks[box] = nil } } } + /// Adds a tag to the task. + /// + /// If the task is not in this center, do nothing. open func addTag(_ tag: String, to task: Task) { addTags([tag], to: task) } + /// Adds tags to the task. + /// + /// If the task is not in this center, do nothing. open func addTags(_ tags: [String], to task: Task) { guard task.taskCenter === self else { return } - mutex.withLock { + lock.withLockVoid { let box = TaskBox(task) - if tagMap[box] == nil { - tagMap[box] = [] - } for tag in tags { - tagMap[box]?.insert(tag) - if taskMap[tag] == nil { - taskMap[tag] = [] + tasks[box]?.insert(tag) + if self.tags[tag] == nil { + self.tags[tag] = [] } - taskMap[tag]?.insert(box) + self.tags[tag]?.insert(box) } } } + /// Removes a tag from the task. + /// + /// If the task is not in this center, do nothing. open func removeTag(_ tag: String, from task: Task) { removeTags([tag], from: task) } + /// Removes tags from the task. + /// + /// If the task is not in this center, do nothing. open func removeTags(_ tags: [String], from task: Task) { guard task.taskCenter === self else { return } - mutex.withLock { + lock.withLockVoid { let box = TaskBox(task) for tag in tags { - tagMap[box]?.remove(tag) - taskMap[tag]?.remove(box) + self.tasks[box]?.remove(tag) + self.tags[tag]?.remove(box) + if self.tags[tag]?.count == 0 { + self.tags[tag] = nil + } } } } - open func tagsForTask(_ task: Task) -> [String] { + /// Returns all tags for the task. + /// + /// If the task is not in this center, return an empty array. + open func tags(forTask task: Task) -> [String] { guard task.taskCenter === self else { return [] } - return mutex.withLock { - Array(tagMap[TaskBox(task)] ?? []) + return lock.withLock { + Array(tasks[TaskBox(task)] ?? []) } } - open func tasksForTag(_ tag: String) -> [Task] { - return mutex.withLock { - taskMap[tag]?.compactMap { $0.task } ?? [] + /// Returns all tasks for the tag. + open func tasks(forTag tag: String) -> [Task] { + return lock.withLock { + tags[tag]?.compactMap { $0.task } ?? [] } } + /// Returns all tasks in this center. open var allTasks: [Task] { - return mutex.withLock { - tagMap.compactMap { $0.key.task } + return lock.withLock { + tasks.compactMap { $0.key.task } } } + /// Returns all tags in this center. open var allTags: [String] { - return mutex.withLock { - taskMap.map { $0.key } + return lock.withLock { + tags.map { $0.key } } } - open func clear() { - mutex.withLock { - tagMap = [:] - taskMap = [:] + /// Removes all tasks from this center. + open func removeAll() { + lock.withLockVoid { + tasks = [:] + tags = [:] } } - open func suspendByTag(_ tag: String) { - tasksForTag(tag).forEach { $0.suspend() } + /// Suspends all tasks by tag. + open func suspend(byTag tag: String) { + tasks(forTag: tag).forEach { $0.suspend() } } - open func resumeByTag(_ tag: String) { - tasksForTag(tag).forEach { $0.resume() } + /// Resumes all tasks by tag. + open func resume(byTag tag: String) { + tasks(forTag: tag).forEach { $0.resume() } } - open func cancelByTag(_ tag: String) { - tasksForTag(tag).forEach { $0.cancel() } + /// Cancels all tasks by tag. + open func cancel(byTag tag: String) { + tasks(forTag: tag).forEach { $0.cancel() } } } diff --git a/Sources/Schedule/Time.swift b/Sources/Schedule/Time.swift index 6cb8398..0f80cac 100644 --- a/Sources/Schedule/Time.swift +++ b/Sources/Schedule/Time.swift @@ -15,9 +15,7 @@ public struct Time { /// Nanosecond of second. public let nanosecond: Int - /// Creates a time with `hour`, `minute`, `second` and `nanosecond` fields. - /// - /// If any parameter is illegal, return nil. + /// Initializes a time with `hour`, `minute`, `second` and `nanosecond`. /// /// Time(hour: 11, minute: 11) => "11:11:00.000" /// Time(hour: 25) => nil @@ -36,36 +34,40 @@ public struct Time { self.nanosecond = nanosecond } - /// Creates a time with a string. - /// - /// If the parameter is illegal, return nil. + /// Initializes a time with a string. /// - /// Time("11") == Time(hour: 11) - /// Time("11:12") == Time(hour: 11, minute: 12) - /// Time("11:12:13") == Time(hour: 11, minute: 12, second: 13) - /// Time("11:12:13.123") == Time(hour: 11, minute: 12, second: 13, nanosecond: 123000000) + /// Time("11") -> Time(hour: 11) + /// Time("11:12") -> Time(hour: 11, minute: 12) + /// Time("11:12:13") -> Time(hour: 11, minute: 12, second: 13) + /// Time("11:12:13.123") -> Time(hour: 11, minute: 12, second: 13, nanosecond: 123000000) /// /// Time("-1.0") == nil /// - /// Any of the previous examples can have a period suffix("am", "AM", "pm", "PM"), - /// separated by a space. - /// /// Time("11 pm") == Time(hour: 23) /// Time("11:12:13 PM") == Time(hour: 23, minute: 12, second: 13) public init?(_ string: String) { let pattern = "^(\\d{1,2})(:(\\d{1,2})(:(\\d{1,2})(.(\\d{1,3}))?)?)?( (am|AM|pm|PM))?$" - guard let regexp = try? NSRegularExpression(pattern: pattern, options: []) else { return nil } - guard let result = regexp.matches(in: string, options: [], range: NSRange(location: 0, length: string.count)).first else { return nil } + // swiftlint:disable force_try + let regexp = try! NSRegularExpression(pattern: pattern, options: []) + let nsString = NSString(string: string) + guard let matches = regexp.matches( + in: string, + options: [], + range: NSRange(location: 0, length: nsString.length)).first + else { + return nil + } var hasAM = false var hasPM = false var values: [Int] = [] + values.reserveCapacity(matches.numberOfRanges) - for i in 0.. DateComponents { + public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents { return DateComponents(calendar: Calendar.gregorian, - timeZone: TimeZone.current, + timeZone: timeZone, hour: hour, minute: minute, second: second, diff --git a/Sources/Schedule/Timeline.swift b/Sources/Schedule/Timeline.swift deleted file mode 100644 index 9cce011..0000000 --- a/Sources/Schedule/Timeline.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation - -/// `Timeline` records a task's lifecycle. -public struct Timeline { - - /// The time of initialization. - public let initialization = Date() - - /// The time of first execution. - public internal(set) var firstExecution: Date? - - /// The time of last execution. - public internal(set) var lastExecution: Date? - - /// The time of estimated next execution. - public internal(set) var estimatedNextExecution: Date? - - init() { } -} - -extension Timeline: CustomStringConvertible { - - /// A textual representation of this timeline. - public var description: String { - let f = DateFormatter() - f.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" - - let desc = { (d: Date?) -> String in - guard let d = d else { return "nil" } - return f.string(from: d) - } - - return "Timeline: { " + - "\"initialization\": \(desc(initialization))" + - "\"firstExecution\": \(desc(firstExecution)), " + - "\"lastExecution\": \(desc(lastExecution)), " + - "\"estimatedNextExecution\": \(desc(estimatedNextExecution))" + - " }" - } -} - -extension Timeline: CustomDebugStringConvertible { - - /// A textual representation of this timeline for debugging. - public var debugDescription: String { - return description - } -} diff --git a/Sources/Schedule/Weekday.swift b/Sources/Schedule/Weekday.swift index 81059a3..a0e5d82 100644 --- a/Sources/Schedule/Weekday.swift +++ b/Sources/Schedule/Weekday.swift @@ -7,20 +7,21 @@ public enum Weekday: Int { /// Returns dateComponenets of the weekday, using gregorian calender and /// current time zone. - public func toDateComponents() -> DateComponents { + public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents { return DateComponents( calendar: Calendar.gregorian, - timeZone: TimeZone.current, - weekday: rawValue - ) + timeZone: timeZone, + weekday: rawValue) } } extension Date { - /// Returns a boolean value indicating whether this day is the weekday. - public func `is`(_ weekday: Weekday) -> Bool { - return Calendar.gregorian.component(.weekday, from: self) == weekday.rawValue + /// Returns a Boolean value indicating whether this date is the weekday in current time zone. + public func `is`(_ weekday: Weekday, in timeZone: TimeZone = .current) -> Bool { + var cal = Calendar.gregorian + cal.timeZone = timeZone + return cal.component(.weekday, from: self) == weekday.rawValue } } @@ -28,7 +29,7 @@ extension Weekday: CustomStringConvertible { /// A textual representation of this weekday. /// - /// Weekday: Friday + /// "Weekday: Friday" public var description: String { return "Weekday: \(Calendar.gregorian.weekdaySymbols[rawValue - 1])" } @@ -38,7 +39,7 @@ extension Weekday: CustomDebugStringConvertible { /// A textual representation of this weekday for debugging. /// - /// Weekday: Friday + /// "Weekday: Friday" public var debugDescription: String { return description } diff --git a/Sources/ScheduleDemo/Log.swift b/Sources/ScheduleDemo/Log.swift deleted file mode 100644 index 811048f..0000000 --- a/Sources/ScheduleDemo/Log.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Log.swift -// Schedule -// -// Created by Quentin Jin on 2019/3/12. -// Copyright © 2019 Schedule. All rights reserved. -// - -import Foundation - -let fmt = ISO8601DateFormatter() - -func Log(_ t: Any) { - - let now = fmt.string(from: Date()) - let thread = Thread.isMainThread ? "main" : "background" - - print("\(now) [\(thread)] -> \(t)") -} diff --git a/Sources/ScheduleDemo/main.swift b/Sources/ScheduleDemo/main.swift deleted file mode 100644 index 5674f00..0000000 --- a/Sources/ScheduleDemo/main.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// main.swift -// Schedule -// -// Created by Quentin Jin on 2019/3/12. -// Copyright © 2019 Schedule. All rights reserved. -// - -import Foundation -import Schedule - -Log("Wake up") - -let t1 = Plan.after(1.second).do { - Log("1 second passed!") -} - -let t2 = Plan.after(1.minute, repeating: 0.5.seconds).do { - Log("Ping!") -} - -let t3 = Plan.every("one minute and ten seconds").do { - Log("One minute and ten seconds have elapsed!") -} - -let t4 = Plan.every(.monday, .tuesday, .wednesday, .thursday, .friday).at(6, 50).do { - Log("Get up!") -} - -let t5 = Plan.every(.june(14)).at("08:59:59 am").do { - Log("Happy birthday!") -} - -RunLoop.current.run() diff --git a/Tests/ScheduleTests/AtomicTests.swift b/Tests/ScheduleTests/AtomicTests.swift index 8b29502..d7f300e 100644 --- a/Tests/ScheduleTests/AtomicTests.swift +++ b/Tests/ScheduleTests/AtomicTests.swift @@ -3,29 +3,42 @@ import XCTest final class AtomicTests: XCTestCase { - func testSnapshot() { + func testRead() { let i = Atomic(1) - XCTAssertEqual(i.snapshot(), 1) + let val = i.read { $0 } + XCTAssertEqual(val, 1) } - func testRead() { + func testReadVoid() { let i = Atomic(1) - i.read { - XCTAssertEqual($0, 1) - } + var val = 0 + i.readVoid { val = $0 } + XCTAssertEqual(val, 1) } func testWrite() { let i = Atomic(1) - i.write { + let val = i.write { v -> Int in + v += 1 + return v + } + XCTAssertEqual(i.read { $0 }, val) + } + + func testWriteVoid() { + let i = Atomic(1) + var val = 0 + i.writeVoid { $0 += 1 + val = $0 } - XCTAssertEqual(i.snapshot(), 2) + XCTAssertEqual(i.read { $0 }, val) } static var allTests = [ - ("testSnapshot", testSnapshot), ("testRead", testRead), - ("testWrite", testWrite) + ("testReadVoid", testReadVoid), + ("testWrite", testWrite), + ("testWriteVoid", testWriteVoid) ] } diff --git a/Tests/ScheduleTests/BagTests.swift b/Tests/ScheduleTests/BagTests.swift new file mode 100644 index 0000000..97be43e --- /dev/null +++ b/Tests/ScheduleTests/BagTests.swift @@ -0,0 +1,108 @@ +import XCTest +@testable import Schedule + +final class BagTests: XCTestCase { + + typealias Fn = () -> Int + + func testBagKey() { + var g = BagKeyGenerator() + let k1 = g.next() + let k2 = g.next() + XCTAssertNotNil(k1) + XCTAssertNotNil(k2) + XCTAssertNotEqual(k1, k2) + } + + func testAppend() { + var bag = Bag() + bag.append { 1 } + bag.append { 2 } + + XCTAssertEqual(bag.count, 2) + } + + func testValueForKey() { + var bag = Bag() + let k1 = bag.append { 1 } + let k2 = bag.append { 2 } + + let fn1 = bag.value(for: k1) + XCTAssertNotNil(fn1) + + let fn2 = bag.value(for: k2) + XCTAssertNotNil(fn2) + + guard let _fn1 = fn1, let _fn2 = fn2 else { return } + + XCTAssertEqual(_fn1(), 1) + XCTAssertEqual(_fn2(), 2) + } + + func testRemoveValueForKey() { + var bag = Bag() + + let k1 = bag.append { 1 } + let k2 = bag.append { 2 } + + let fn1 = bag.removeValue(for: k1) + XCTAssertNotNil(fn1) + XCTAssertNil(bag.value(for: k1)) + + let fn2 = bag.removeValue(for: k2) + XCTAssertNotNil(fn2) + XCTAssertNil(bag.removeValue(for: k2)) + + guard let _fn1 = fn1, let _fn2 = fn2 else { return } + + XCTAssertEqual(_fn1(), 1) + XCTAssertEqual(_fn2(), 2) + } + + func testCount() { + var bag = Bag() + + let k1 = bag.append { 1 } + let k2 = bag.append { 2 } + + XCTAssertEqual(bag.count, 2) + + bag.removeValue(for: k1) + bag.removeValue(for: k2) + + XCTAssertEqual(bag.count, 0) + } + + func testRemoveAll() { + var bag = Bag() + + bag.append { 1 } + bag.append { 2 } + + bag.removeAll() + XCTAssertEqual(bag.count, 0) + } + + func testSequence() { + var bag = Bag() + bag.append { 0 } + bag.append { 1 } + bag.append { 2 } + + var i = 0 + for fn in bag { + XCTAssertEqual(fn(), i) + i += 1 + } + } + + static var allTests = [ + ("testBagKey", testBagKey), + ("testAppend", testAppend), + ("testValueForKey", testValueForKey), + ("testRemoveValueForKey", testRemoveValueForKey), + ("testCount", testCount), + ("testRemoveAll", testRemoveAll), + ("testSequence", testSequence) + ] +} diff --git a/Tests/ScheduleTests/CabinetTests.swift b/Tests/ScheduleTests/CabinetTests.swift deleted file mode 100644 index d064d8a..0000000 --- a/Tests/ScheduleTests/CabinetTests.swift +++ /dev/null @@ -1,92 +0,0 @@ -import XCTest -@testable import Schedule - -final class CabinetTests: XCTestCase { - - typealias Fn = () -> Int - - func testCabinetKey() { - let key = CabinetKey(underlying: 0) - XCTAssertEqual(key.increased(), CabinetKey(underlying: 1)) - } - - func testAppend() { - var cabinet = Cabinet() - let k1 = cabinet.append { 1 } - let k2 = cabinet.append { 2 } - - XCTAssertEqual(k1.increased(), k2) - XCTAssertEqual(cabinet.count, 2) - } - - func testGet() { - var cabinet = Cabinet() - let k1 = cabinet.append { 1 } - let k2 = cabinet.append { 2 } - - guard - let fn1 = cabinet.get(k1), - let fn2 = cabinet.get(k2) - else { - XCTFail() - return - } - XCTAssertEqual(fn1(), 1) - XCTAssertEqual(fn2(), 2) - - XCTAssertNil(cabinet.get(k2.increased())) - } - - func testDelete() { - var cabinet = Cabinet() - - let k1 = cabinet.append { 1 } - let k2 = cabinet.append { 2 } - - XCTAssertEqual(cabinet.count, 2) - - let fn1 = cabinet.delete(k1) - XCTAssertNotNil(fn1) - - let fn2 = cabinet.delete(k2) - XCTAssertNotNil(fn2) - - XCTAssertEqual(cabinet.count, 0) - - XCTAssertNil(cabinet.delete(k2.increased())) - } - - func testClear() { - var cabinet = Cabinet() - - cabinet.append { 1 } - cabinet.append { 2 } - - XCTAssertEqual(cabinet.count, 2) - - cabinet.clear() - XCTAssertEqual(cabinet.count, 0) - } - - func testSequence() { - var cabinet = Cabinet() - cabinet.append { 0 } - cabinet.append { 1 } - cabinet.append { 2 } - - var i = 0 - for fn in cabinet { - XCTAssertEqual(fn(), i) - i += 1 - } - } - - static var allTests = [ - ("testCabinetKey", testCabinetKey), - ("testAppend", testAppend), - ("testGet", testGet), - ("testDelete", testDelete), - ("testClear", testClear), - ("testSequence", testSequence) - ] -} diff --git a/Tests/ScheduleTests/DateTimeTests.swift b/Tests/ScheduleTests/DateTimeTests.swift deleted file mode 100644 index 44f4c3a..0000000 --- a/Tests/ScheduleTests/DateTimeTests.swift +++ /dev/null @@ -1,153 +0,0 @@ -import XCTest -@testable import Schedule - -final class DateTimeTests: XCTestCase { - - func testInterval() { - - XCTAssertTrue((-1).second.isNegative) - XCTAssertTrue(1.second.isPositive) - XCTAssertEqual(1.1.second.magnitude, 1.1.second.nanoseconds) - XCTAssertEqual(1.second.opposite, (-1).second) - - XCTAssertEqual(7.day.hashValue, 1.week.hashValue) - XCTAssertEqual(7.day, 1.week) - - XCTAssertEqual((-2).seconds.compare(1.second), .orderedAscending) - XCTAssertTrue(1.1.second > 1.0.second) - XCTAssertTrue(3.days < 1.week) - XCTAssertTrue(4.day >= 4.days) - XCTAssertTrue(-2.seconds < 1.seconds) - - XCTAssertTrue(1.1.second.isLonger(than: 1.0.second)) - XCTAssertTrue(3.days.isShorter(than: 1.week)) - XCTAssertEqual(Interval.longest(1.hour, 1.day, 1.week), 1.week) - XCTAssertEqual(Interval.longest([]), nil) - XCTAssertEqual(Interval.shortest(1.hour, 59.minutes, 2999.seconds), 2999.seconds) - XCTAssertEqual(Interval.shortest([]), nil) - - XCTAssertEqual(1.second * 60, 1.minute) - XCTAssertEqual(59.minutes + 60.seconds, 1.hour) - XCTAssertEqual(1.week - 24.hours, 6.days) - var i0 = 1.day - i0 += 1.day - XCTAssertEqual(i0, 2.days) - XCTAssertEqual(-(1.second), (-1).second) - - let i1 = Interval(seconds: 24 * 60 * 60) - XCTAssertEqual(1.nanosecond * i1.asNanoseconds(), 1.day) - XCTAssertEqual(2.microsecond * i1.asMicroseconds(), 2.days) - XCTAssertEqual(3.millisecond * i1.asMilliseconds(), 3.days) - XCTAssertEqual(4.second * i1.asSeconds(), 4.days) - XCTAssertEqual(5.1.minute * i1.asMinutes(), 5.1.days) - XCTAssertEqual(6.2.hour * i1.asHours(), 6.2.days) - XCTAssertEqual(7.3.day * i1.asDays(), 7.3.days) - XCTAssertEqual(1.week * i1.asWeeks(), 1.days) - - let date0 = Date() - let date1 = date0.addingTimeInterval(100) - XCTAssertEqual(date0.interval(since: date1), date0.timeIntervalSince(date1).seconds) - XCTAssertEqual(date0.adding(1.seconds), date0.addingTimeInterval(1)) - XCTAssertEqual(date0 + 1.seconds, date0.addingTimeInterval(1)) - } - - func testMonthday() { - let d = Date(year: 2019, month: 1, day: 1) - XCTAssertTrue(d.is(.january(1))) - - XCTAssertEqual(Monthday.january(1).toDateComponents().month, 1) - XCTAssertEqual(Monthday.february(1).toDateComponents().month, 2) - XCTAssertEqual(Monthday.march(1).toDateComponents().month, 3) - XCTAssertEqual(Monthday.april(1).toDateComponents().month, 4) - XCTAssertEqual(Monthday.may(1).toDateComponents().month, 5) - XCTAssertEqual(Monthday.june(1).toDateComponents().month, 6) - XCTAssertEqual(Monthday.july(1).toDateComponents().month, 7) - XCTAssertEqual(Monthday.august(1).toDateComponents().month, 8) - XCTAssertEqual(Monthday.september(1).toDateComponents().month, 9) - XCTAssertEqual(Monthday.october(1).toDateComponents().month, 10) - XCTAssertEqual(Monthday.november(1).toDateComponents().month, 11) - XCTAssertEqual(Monthday.december(1).toDateComponents().month, 12) - } - - func testPeriod() { - let p0 = (1.year + 2.years + 1.month + 2.months + 3.days).tidied(to: .day) - XCTAssertEqual(p0.years, 3) - XCTAssertEqual(p0.months, 3) - XCTAssertEqual(p0.days, 3) - - let p1 = Period("one second")?.tidied(to: .second) - XCTAssertNotNil(p1) - XCTAssertEqual(p1!.seconds, 1) - let p2 = Period("two hours and ten minutes")?.tidied(to: .day) - XCTAssertNotNil(p2) - XCTAssertEqual(p2!.hours, 2) - XCTAssertEqual(p2!.minutes, 10) - let p3 = Period("1 year, 2 months and 3 days")?.tidied(to: .day) - XCTAssertNotNil(p3) - XCTAssertEqual(p3!.years, 1) - XCTAssertEqual(p3!.months, 2) - XCTAssertEqual(p3!.days, 3) - - Period.registerQuantifier("many", for: 100 * 1000) - let p4 = Period("many days") - XCTAssertEqual(p4!.days, 100 * 1000) - - let date = Date(year: 1989, month: 6, day: 4) + 1.year - let year = date.dateComponents.year - XCTAssertEqual(year, 1990) - - let p5 = Period(hours: 25).tidied(to: .day) - XCTAssertEqual(p5.days, 1) - } - - func testTime() { - let t0 = Time(hour: -1, minute: -2, second: -3, nanosecond: -4) - XCTAssertNil(t0) - - let t1 = Time("11:12:13.456") - XCTAssertNotNil(t1) - XCTAssertEqual(t1?.hour, 11) - XCTAssertEqual(t1?.minute, 12) - XCTAssertEqual(t1?.second, 13) - if let i = t1?.nanosecond.nanoseconds { - XCTAssertTrue(i.isAlmostEqual(to: (0.456.second.nanoseconds).nanoseconds, leeway: 0.001.seconds)) - } - - let components = t1?.toDateComponents() - XCTAssertEqual(components?.hour, 11) - XCTAssertEqual(components?.minute, 12) - XCTAssertEqual(components?.second, 13) - if let i = components?.nanosecond?.nanoseconds { - XCTAssertTrue(i.isAlmostEqual(to: (0.456.second.nanoseconds).nanoseconds, leeway: 0.001.seconds)) - } - - let t2 = Time("11 pm") - XCTAssertNotNil(t2) - XCTAssertEqual(t2?.hour, 23) - - let t3 = Time("12 am") - XCTAssertNotNil(t3) - XCTAssertEqual(t3?.hour, 0) - - let t4 = Time("schedule") - XCTAssertNil(t4) - - XCTAssertEqual(Time(hour: 1)!.intervalSinceStartOfDay, 1.hour) - } - - func testWeekday() { - // Be careful the time zone problem. - let d = Date(year: 2019, month: 1, day: 1) - XCTAssertTrue(d.is(.tuesday)) - - XCTAssertEqual(Weekday.monday.toDateComponents().weekday!, 2) - } - - static var allTests = [ - ("testInterval", testInterval), - ("testMonthday", testMonthday), - ("testPeriod", testPeriod), - ("testTime", testTime), - ("testWeekday", testWeekday) - ] -} diff --git a/Tests/ScheduleTests/DeinitObserverTests.swift b/Tests/ScheduleTests/DeinitObserverTests.swift deleted file mode 100644 index bfde058..0000000 --- a/Tests/ScheduleTests/DeinitObserverTests.swift +++ /dev/null @@ -1,35 +0,0 @@ -import XCTest -@testable import Schedule - -#if canImport(ObjectiveC) - -final class DeinitObserverTests: XCTestCase { - - func testObserver() { - var i = 0 - var fn = { - let obj = NSObject() - DeinitObserver.observe(obj) { - i += 1 - } - } - fn() - XCTAssertEqual(i, 1) - - fn = { - let obj = NSObject() - let observer = DeinitObserver.observe(obj) { - i += 1 - } - observer.invalidate() - } - fn() - XCTAssertEqual(i, 1) - } - - static var allTests = [ - ("testObserver", testObserver) - ] -} - -#endif diff --git a/Tests/ScheduleTests/ExtensionsTests.swift b/Tests/ScheduleTests/ExtensionsTests.swift index 6ec4ef5..507ae92 100644 --- a/Tests/ScheduleTests/ExtensionsTests.swift +++ b/Tests/ScheduleTests/ExtensionsTests.swift @@ -16,21 +16,16 @@ final class ExtensionsTests: XCTestCase { XCTAssertEqual(i.clampedAdding(1), Int.max) } - func testClampedSubtracting() { - let i = Int.min - XCTAssertEqual(i.clampedSubtracting(1), Int.min) - } - func testStartOfToday() { let components = Date().startOfToday.dateComponents - guard - let h = components.hour, - let m = components.minute, - let s = components.second - else { - XCTFail() - return - } + + let h = components.hour + let m = components.minute + let s = components.second + XCTAssertNotNil(h) + XCTAssertNotNil(m) + XCTAssertNotNil(s) + XCTAssertEqual(h, 0) XCTAssertEqual(m, 0) XCTAssertEqual(s, 0) @@ -39,7 +34,6 @@ final class ExtensionsTests: XCTestCase { static var allTests = [ ("testClampedToInt", testClampedToInt), ("testClampedAdding", testClampedAdding), - ("testClampedSubtracting", testClampedSubtracting), ("testStartOfToday", testStartOfToday) ] } diff --git a/Tests/ScheduleTests/Helpers.swift b/Tests/ScheduleTests/Helpers.swift index 9c103fe..4ca663a 100644 --- a/Tests/ScheduleTests/Helpers.swift +++ b/Tests/ScheduleTests/Helpers.swift @@ -11,7 +11,7 @@ extension Date { year: Int, month: Int, day: Int, hour: Int = 0, minute: Int = 0, second: Int = 0, nanosecond: Int = 0 - ) { + ) { let components = DateComponents( calendar: Calendar.gregorian, timeZone: TimeZone.current, @@ -26,7 +26,7 @@ extension Date { extension Interval { func isAlmostEqual(to interval: Interval, leeway: Interval) -> Bool { - return (interval - self).magnitude <= leeway.magnitude + return (interval - self).abs <= leeway.abs } } @@ -76,3 +76,8 @@ extension DispatchQueue { return DispatchQueue.getSpecific(key: key) != nil } } + +extension TimeZone { + + static let shanghai = TimeZone(identifier: "Asia/Shanghai")! +} diff --git a/Tests/ScheduleTests/IntervalTests.swift b/Tests/ScheduleTests/IntervalTests.swift new file mode 100644 index 0000000..1bff1ed --- /dev/null +++ b/Tests/ScheduleTests/IntervalTests.swift @@ -0,0 +1,111 @@ +// +// IntervalTests.swift +// ScheduleTests +// +// Created by Quentin MED on 2019/4/4. +// + +import XCTest +@testable import Schedule + +final class IntervalTests: XCTestCase { + + private let leeway = 0.01.second + + func testEquatable() { + XCTAssertEqual(1.second, 1.second) + XCTAssertEqual(1.week, 7.days) + } + + func testIsNegative() { + XCTAssertFalse(1.second.isNegative) + XCTAssertTrue((-1).second.isNegative) + } + + func testAbs() { + XCTAssertEqual(1.second, (-1).second.abs) + } + + func testNegated() { + XCTAssertEqual(1.second.negated, (-1).second) + XCTAssertEqual(1.second.negated.negated, 1.second) + } + + func testCompare() { + XCTAssertEqual((-1).second.compare(1.second), ComparisonResult.orderedAscending) + XCTAssertEqual(8.days.compare(1.week), ComparisonResult.orderedDescending) + XCTAssertEqual(1.day.compare(24.hours), ComparisonResult.orderedSame) + + XCTAssertTrue(23.hours < 1.day) + XCTAssertTrue(25.hours > 1.day) + } + + func testLongerShorter() { + XCTAssertTrue((-25).hour.isLonger(than: 1.day)) + XCTAssertTrue(1.week.isShorter(than: 8.days)) + } + + func testMultiplying() { + XCTAssertEqual(7.days * 2, 2.week) + } + + func testAdding() { + XCTAssertEqual(6.days + 1.day, 1.week) + + XCTAssertEqual(1.1.weeks, 1.week + 0.1.weeks) + } + + func testOperators() { + XCTAssertEqual(1.week - 6.days, 1.day) + + var i = 6.days + i += 1.day + XCTAssertEqual(i, 1.week) + + XCTAssertEqual(-(7.days), (-1).week) + } + + func testAs() { + XCTAssertEqual(1.millisecond.asNanoseconds(), 1.microsecond.asNanoseconds() * pow(10, 3)) + + XCTAssertEqual(1.second.asNanoseconds(), pow(10, 9)) + XCTAssertEqual(1.second.asMicroseconds(), pow(10, 6)) + XCTAssertEqual(1.second.asMilliseconds(), pow(10, 3)) + + XCTAssertEqual(1.minute.asSeconds(), 60) + XCTAssertEqual(1.hour.asMinutes(), 60) + XCTAssertEqual(1.day.asHours(), 24) + XCTAssertEqual(1.week.asDays(), 7) + XCTAssertEqual(7.days.asWeeks(), 1) + } + + func testDate() { + let date0 = Date() + let date1 = date0.addingTimeInterval(100) + + XCTAssertTrue(date1.intervalSinceNow.isAlmostEqual(to: 100.seconds, leeway: leeway)) + + XCTAssertEqual(date0.interval(since: date1), date0.timeIntervalSince(date1).seconds) + + XCTAssertEqual(date0.adding(1.seconds), date0.addingTimeInterval(1)) + XCTAssertEqual(date0 + 1.seconds, date0.addingTimeInterval(1)) + } + + func testDescription() { + XCTAssertEqual(1.nanosecond.debugDescription, "Interval: 1 nanosecond(s)") + } + + static var allTests = [ + ("testEquatable", testEquatable), + ("testIsNegative", testIsNegative), + ("testAbs", testAbs), + ("testNegated", testNegated), + ("testCompare", testCompare), + ("testLongerShorter", testLongerShorter), + ("testMultiplying", testMultiplying), + ("testAdding", testAdding), + ("testOperators", testOperators), + ("testAs", testAs), + ("testDate", testDate) + ] +} diff --git a/Tests/ScheduleTests/MonthdayTests.swift b/Tests/ScheduleTests/MonthdayTests.swift new file mode 100644 index 0000000..dbf4a41 --- /dev/null +++ b/Tests/ScheduleTests/MonthdayTests.swift @@ -0,0 +1,27 @@ +import XCTest +@testable import Schedule + +final class MonthdayTests: XCTestCase { + + func testIs() { + let d = Date(year: 2019, month: 1, day: 1) + XCTAssertTrue(d.is(.january(1), in: TimeZone.shanghai)) + } + + func testAsDateComponents() { + let comps = Monthday.april(1).asDateComponents() + XCTAssertEqual(comps.month, 4) + XCTAssertEqual(comps.day, 1) + } + + func testDescription() { + let md = Monthday.april(1) + XCTAssertEqual(md.debugDescription, "Monthday: April 1st") + } + + static var allTests = [ + ("testIs", testIs), + ("testAsDateComponents", testAsDateComponents), + ("testDescription", testDescription) + ] +} diff --git a/Tests/ScheduleTests/PeriodTests.swift b/Tests/ScheduleTests/PeriodTests.swift new file mode 100644 index 0000000..ca5bb73 --- /dev/null +++ b/Tests/ScheduleTests/PeriodTests.swift @@ -0,0 +1,79 @@ +import XCTest +@testable import Schedule + +final class PeriodTests: XCTestCase { + + func testPeriod() { + let period = (1.year + 2.years + 1.month + 2.months + 3.days) + XCTAssertEqual(period.years, 3) + XCTAssertEqual(period.months, 3) + XCTAssertEqual(period.days, 3) + } + + func testInitWithString() { + let p1 = Period("one second") + XCTAssertNotNil(p1) + XCTAssertEqual(p1!.seconds, 1) + + let p2 = Period("two hours and ten minutes") + XCTAssertNotNil(p2) + XCTAssertEqual(p2!.hours, 2) + XCTAssertEqual(p2!.minutes, 10) + + let p3 = Period("1 year, 2 months and 3 days") + XCTAssertNotNil(p3) + XCTAssertEqual(p3!.years, 1) + XCTAssertEqual(p3!.months, 2) + XCTAssertEqual(p3!.days, 3) + + Period.registerQuantifier("many", for: 100 * 1000) + let p4 = Period("many days") + XCTAssertEqual(p4!.days, 100 * 1000) + + let p5 = Period("hi, 😈") + XCTAssertNil(p5) + } + + func testAdd() { + XCTAssertEqual(1.month.adding(1.month).months, 2) + XCTAssertEqual(Period(days: 1).adding(1.day).days, 2) + } + + func testTidy() { + let period = 1.month.adding(25.hour).tidied(to: .day) + XCTAssertEqual(period.days, 1) + } + + func testAsDateComponents() { + let period = Period(years: 1, months: 2, days: 3, hours: 4, minutes: 5, seconds: 6, nanoseconds: 7) + let comps = period.asDateComponents() + XCTAssertEqual(comps.year, 1) + XCTAssertEqual(comps.month, 2) + XCTAssertEqual(comps.day, 3) + XCTAssertEqual(comps.hour, 4) + XCTAssertEqual(comps.minute, 5) + XCTAssertEqual(comps.second, 6) + XCTAssertEqual(comps.nanosecond, 7) + } + + func testDate() { + let d = Date(year: 1989, month: 6, day: 4) + 1.year + let year = d.dateComponents.year + XCTAssertEqual(year, 1990) + } + + func testDescription() { + let period = Period(years: 1, nanoseconds: 1) + XCTAssertEqual(period.debugDescription, "Period: 1 year(s) 1 nanosecond(s)") + } + + static var allTests = [ + ("testPeriod", testPeriod), + ("testInitWithString", testInitWithString), + ("testAdd", testAdd), + ("testTidy", testTidy), + ("testAsDateComponents", testAsDateComponents), + ("testDate", testDate), + ("testDescription", testDescription) + ] +} diff --git a/Tests/ScheduleTests/PlanTests.swift b/Tests/ScheduleTests/PlanTests.swift index 000c847..d71f7bc 100644 --- a/Tests/ScheduleTests/PlanTests.swift +++ b/Tests/ScheduleTests/PlanTests.swift @@ -3,36 +3,46 @@ import XCTest final class PlanTests: XCTestCase { - let leeway = 0.01.seconds + private let leeway = 0.01.seconds - func testMake() { - let intervals = [1.second, 2.hours, 3.days, 4.weeks] - let s0 = Plan.of(intervals[0], intervals[1], intervals[2], intervals[3]) - XCTAssertTrue(s0.makeIterator().isAlmostEqual(to: intervals, leeway: leeway)) - let s1 = Plan.from(intervals) - XCTAssertTrue(s1.makeIterator().isAlmostEqual(to: intervals, leeway: leeway)) + func testOfIntervals() { + let ints = [1.second, 2.hours, 3.days, 4.weeks] + let p = Plan.of(ints) + XCTAssertTrue(p.makeIterator().isAlmostEqual(to: ints, leeway: leeway)) + } - let d0 = Date() + intervals[0] - let d1 = d0 + intervals[1] - let d2 = d1 + intervals[2] - let d3 = d2 + intervals[3] + func testOfDates() { + let ints = [1.second, 2.hours, 3.days, 4.weeks] - let s2 = Plan.of(d0, d1, d2, d3) - XCTAssertTrue(s2.makeIterator().isAlmostEqual(to: intervals, leeway: leeway)) + let d0 = Date() + ints[0] + let d1 = d0 + ints[1] + let d2 = d1 + ints[2] + let d3 = d2 + ints[3] - let longTime = (100 * 365).days - XCTAssertTrue(Plan.distantPast.makeIterator().next()!.isLonger(than: longTime)) - XCTAssertTrue(Plan.distantFuture.makeIterator().next()!.isLonger(than: longTime)) + let p = Plan.of(d0, d1, d2, d3) + XCTAssertTrue(p.makeIterator().isAlmostEqual(to: ints, leeway: leeway)) } func testDates() { - let iterator = Plan.of(1.days, 2.weeks).dates.makeIterator() - var next = iterator.next() - XCTAssertNotNil(next) - XCTAssertTrue(next!.intervalSinceNow.isAlmostEqual(to: 1.days, leeway: leeway)) - next = iterator.next() - XCTAssertNotNil(next) - XCTAssertTrue(next!.intervalSinceNow.isAlmostEqual(to: 2.weeks + 1.days, leeway: leeway)) + let dates = Plan.of(1.days, 2.weeks).dates.makeIterator() + + var n = dates.next() + XCTAssertNotNil(n) + XCTAssertTrue(n!.intervalSinceNow.isAlmostEqual(to: 1.days, leeway: leeway)) + + n = dates.next() + XCTAssertNotNil(n) + XCTAssertTrue(n!.intervalSinceNow.isAlmostEqual(to: 2.weeks + 1.days, leeway: leeway)) + } + + func testDistant() { + let distantPast = Plan.distantPast.makeIterator().next() + XCTAssertNotNil(distantPast) + XCTAssertTrue(distantPast!.isAlmostEqual(to: Date.distantPast.intervalSinceNow, leeway: leeway)) + + let distantFuture = Plan.distantFuture.makeIterator().next() + XCTAssertNotNil(distantFuture) + XCTAssertTrue(distantFuture!.isAlmostEqual(to: Date.distantFuture.intervalSinceNow, leeway: leeway)) } func testNever() { @@ -40,32 +50,25 @@ final class PlanTests: XCTestCase { } func testConcat() { - let s0: [Interval] = [1.second, 2.minutes, 3.hours] - let s1: [Interval] = [4.days, 5.weeks] - let s3 = Plan.from(s0).concat(Plan.from(s1)) - let s4 = Plan.from(s0 + s1) - XCTAssertTrue(s3.isAlmostEqual(to: s4, leeway: leeway)) + let p0: [Interval] = [1.second, 2.minutes, 3.hours] + let p1: [Interval] = [4.days, 5.weeks] + let p2 = Plan.of(p0).concat(Plan.of(p1)) + let p3 = Plan.of(p0 + p1) + XCTAssertTrue(p2.isAlmostEqual(to: p3, leeway: leeway)) } func testMerge() { - let intervals0: [Interval] = [1.second, 2.minutes, 1.hour] - let intervals1: [Interval] = [2.seconds, 1.minutes, 1.seconds] - let scheudle0 = Plan.from(intervals0).merge(Plan.from(intervals1)) - let scheudle1 = Plan.of(1.second, 1.second, 1.minutes, 1.seconds, 58.seconds, 1.hour) - XCTAssertTrue(scheudle0.isAlmostEqual(to: scheudle1, leeway: leeway)) - } - - func testAt() { - let s = Plan.at(Date() + 1.second) - let next = s.makeIterator().next() - XCTAssertNotNil(next) - XCTAssertTrue(next!.isAlmostEqual(to: 1.second, leeway: leeway)) + let ints0: [Interval] = [1.second, 2.minutes, 1.hour] + let ints1: [Interval] = [2.seconds, 1.minutes, 1.seconds] + let p0 = Plan.of(ints0).merge(Plan.of(ints1)) + let p1 = Plan.of(1.second, 1.second, 1.minutes, 1.seconds, 58.seconds, 1.hour) + XCTAssertTrue(p0.isAlmostEqual(to: p1, leeway: leeway)) } func testFirst() { var count = 10 - let s = Plan.every(1.second).first(count) - let i = s.makeIterator() + let p = Plan.every(1.second).first(count) + let i = p.makeIterator() while count > 0 { XCTAssertNotNil(i.next()) count -= 1 @@ -75,29 +78,36 @@ final class PlanTests: XCTestCase { func testUntil() { let until = Date() + 10.seconds - let s = Plan.every(1.second).until(until).dates - let i = s.makeIterator() + let p = Plan.every(1.second).until(until).dates + let i = p.makeIterator() while let date = i.next() { XCTAssertLessThan(date, until) } } func testNow() { - let s0 = Plan.now - let s1 = Plan.of(Date()) - XCTAssertTrue(s0.isAlmostEqual(to: s1, leeway: leeway)) + let p0 = Plan.now + let p1 = Plan.of(Date()) + XCTAssertTrue(p0.isAlmostEqual(to: p1, leeway: leeway)) + } + + func testAt() { + let p = Plan.at(Date() + 1.second) + let next = p.makeIterator().next() + XCTAssertNotNil(next) + XCTAssertTrue(next!.isAlmostEqual(to: 1.second, leeway: leeway)) } func testAfterAndRepeating() { - let s0 = Plan.after(1.day, repeating: 1.hour).first(3) - let s1 = Plan.of(1.day, 1.hour, 1.hour) - XCTAssertTrue(s0.isAlmostEqual(to: s1, leeway: leeway)) + let p0 = Plan.after(1.day, repeating: 1.hour).first(3) + let p1 = Plan.of(1.day, 1.hour, 1.hour) + XCTAssertTrue(p0.isAlmostEqual(to: p1, leeway: leeway)) } func testEveryPeriod() { - let s = Plan.every("1 year").first(10) + let p = Plan.every("1 year").first(10) var date = Date() - for i in s.dates { + for i in p.dates { XCTAssertEqual(i.dateComponents.year!, date.dateComponents.year! + 1) XCTAssertEqual(i.dateComponents.month!, date.dateComponents.month!) XCTAssertEqual(i.dateComponents.day!, date.dateComponents.day!) @@ -106,121 +116,47 @@ final class PlanTests: XCTestCase { } func testEveryWeekday() { - let s = Plan.every(.friday, .monday).at("11:11:00").first(5) - for i in s.dates { + let p = Plan.every(.friday, .monday).at("11:11:00").first(5) + for i in p.dates { XCTAssertTrue(i.dateComponents.weekday == 6 || i.dateComponents.weekday == 2) XCTAssertEqual(i.dateComponents.hour, 11) } } func testEveryMonthday() { - let s = Plan.every(.april(1), .october(1)).at(11, 11).first(5) - for i in s.dates { + let p = Plan.every(.april(1), .october(1)).at(11, 11).first(5) + for i in p.dates { XCTAssertTrue(i.dateComponents.month == 4 || i.dateComponents.month == 10) XCTAssertEqual(i.dateComponents.day, 1) XCTAssertEqual(i.dateComponents.hour, 11) } } - - func testPassingEmptyArrays() { - XCTAssertTrue(Plan.of([Interval]()).isNever()) - XCTAssertTrue(Plan.of([Date]()).isNever()) - - XCTAssertTrue(Plan.every([Weekday]()).at(11, 11).isNever()) - XCTAssertTrue(Plan.every([Monthday]()).at(11, 11).isNever()) - - XCTAssertTrue(Plan.every(.monday).at([]).isNever()) - - XCTAssertTrue(Plan.every([Weekday]()).at("11:11:00").isNever()) - } - - func testIntervalOffset() { - // Non-offset plan - let e1 = expectation(description: "testIntervalOffset_1") - let plan1 = Plan.after(1.second) - var date1: Date? - - // Offset plan - let e2 = expectation(description: "testIntervalOffset_2") - let plan2 = plan1.offset(by: 1.second) - var date2: Date? - - let task1 = plan1.do { date1 = Date(); e1.fulfill() } - let task2 = plan2.do { date2 = Date(); e2.fulfill() } - _ = task1 - _ = task2 - - waitForExpectations(timeout: 3.5) - - XCTAssertNotNil(date1) - XCTAssertNotNil(date2) - XCTAssertTrue(date2!.interval(since: date1!).isAlmostEqual(to: 1.second, leeway: 0.1.seconds)) - } - - func testNegativeIntervalOffset() { - // Non-offset plan - let e1 = expectation(description: "testIntervalOffset_1") - let plan1 = Plan.after(2.seconds) - var date1: Date? - - // Offset plan - let e2 = expectation(description: "testIntervalOffset_2") - let plan2 = plan1.offset(by: -1.second) - var date2: Date? - - let task1 = plan1.do { date1 = Date(); e1.fulfill() } - let task2 = plan2.do { date2 = Date(); e2.fulfill() } - _ = task1 - _ = task2 - - waitForExpectations(timeout: 2.5) - - XCTAssertNotNil(date1) - XCTAssertNotNil(date2) - XCTAssertTrue(date2!.interval(since: date1!).isAlmostEqual(to: -1.second, leeway: 0.1.seconds)) - } - - func testNilIntervalOffset() { - // Non-offset plan - let e1 = expectation(description: "testIntervalOffset_1") - let plan1 = Plan.after(1.second) - var date1: Date? - - // Offset plan - let e2 = expectation(description: "testIntervalOffset_2") - let plan2 = plan1.offset(by: nil) - var date2: Date? - - let task1 = plan1.do { date1 = Date(); e1.fulfill() } - let task2 = plan2.do { date2 = Date(); e2.fulfill() } - - _ = task1 - _ = task2 - - waitForExpectations(timeout: 1.5) - - XCTAssertNotNil(date1) - XCTAssertNotNil(date2) - XCTAssertTrue(date2!.interval(since: date1!).isAlmostEqual(to: 0.seconds, leeway: 0.1.seconds)) - } - + + func testOffset() { + let p1 = Plan.after(1.second).first(100) + let p2 = p1.offset(by: 1.second).first(100) + + for (d1, d2) in zip(p1.dates, p2.dates) { + XCTAssertTrue(d2.interval(since: d1).isAlmostEqual(to: 1.second, leeway: leeway)) + } + } + static var allTests = [ - ("testMake", testMake), + ("testOfIntervals", testOfIntervals), + ("testOfDates", testOfDates), ("testDates", testDates), + ("testDistant", testDistant), ("testNever", testNever), ("testConcat", testConcat), ("testMerge", testMerge), - ("testAt", testAt), ("testFirst", testFirst), ("testUntil", testUntil), ("testNow", testNow), + ("testAt", testAt), ("testAfterAndRepeating", testAfterAndRepeating), ("testEveryPeriod", testEveryPeriod), ("testEveryWeekday", testEveryWeekday), ("testEveryMonthday", testEveryMonthday), - ("testPassingEmptyArrays", testPassingEmptyArrays), - ("testIntervalOffset", testIntervalOffset), - ("testNegativeIntervalOffset", testNegativeIntervalOffset), - ("testNilIntervalOffset", testNilIntervalOffset), + ("testOffset", testOffset) ] } diff --git a/Tests/ScheduleTests/TaskCenterTests.swift b/Tests/ScheduleTests/TaskCenterTests.swift index 429f3b3..7474067 100644 --- a/Tests/ScheduleTests/TaskCenterTests.swift +++ b/Tests/ScheduleTests/TaskCenterTests.swift @@ -15,81 +15,90 @@ final class TaskCenterTests: XCTestCase { func testDefault() { let task = makeTask() XCTAssertTrue(center.allTasks.contains(task)) - center.clear() + center.removeAll() } func testAdd() { - let c = TaskCenter() - let task = makeTask() XCTAssertEqual(center.allTasks.count, 1) + let c = TaskCenter() c.add(task) + XCTAssertEqual(center.allTasks.count, 0) XCTAssertEqual(c.allTasks.count, 1) - c.add(task) - XCTAssertEqual(c.allTasks.count, 1) + center.add(task) + XCTAssertEqual(center.allTasks.count, 1) + XCTAssertEqual(c.allTasks.count, 0) - center.clear() + center.removeAll() } func testRemove() { let task = makeTask() + let tag = UUID().uuidString + center.addTag(tag, to: task) + center.remove(task) + XCTAssertFalse(center.allTasks.contains(task)) + XCTAssertFalse(center.allTags.contains(tag)) + + center.removeAll() } func testTag() { let task = makeTask() - let tag = UUID().uuidString center.addTag(tag, to: task) - XCTAssertTrue(center.tasksForTag(tag).contains(task)) - XCTAssertTrue(center.tagsForTask(task).contains(tag)) + XCTAssertTrue(center.tasks(forTag: tag).contains(task)) + XCTAssertTrue(center.tags(forTask: task).contains(tag)) center.removeTag(tag, from: task) - XCTAssertFalse(center.tasksForTag(tag).contains(task)) - XCTAssertFalse(center.tagsForTask(task).contains(tag)) + XCTAssertFalse(center.tasks(forTag: tag).contains(task)) + XCTAssertFalse(center.tags(forTask: task).contains(tag)) - center.clear() + center.removeAll() } func testAll() { let task = makeTask() + let tag1 = UUID().uuidString + let tag2 = UUID().uuidString - let tag = UUID().uuidString + center.addTags([tag1, tag2], to: task) - center.addTag(tag, to: task) - XCTAssertEqual(center.allTags, [tag]) + XCTAssertEqual(center.allTags.sorted(), [tag1, tag2].sorted()) XCTAssertEqual(center.allTasks, [task]) - center.clear() + center.removeAll() } func testOperation() { let task = makeTask() - let tag = UUID().uuidString center.addTag(tag, to: task) - center.suspendByTag(tag) - XCTAssertEqual(task.suspensions, 1) + center.suspend(byTag: tag) + XCTAssertEqual(task.suspensionCount, 1) - center.resumeByTag(tag) - XCTAssertEqual(task.suspensions, 0) + center.resume(byTag: tag) + XCTAssertEqual(task.suspensionCount, 0) - center.cancelByTag(tag) + center.cancel(byTag: tag) XCTAssertTrue(task.isCancelled) - center.clear() + center.removeAll() } func testWeak() { let block = { - _ = self.makeTask() + let task = self.makeTask() + XCTAssertEqual(self.center.allTasks.count, 1) + _ = task } block() diff --git a/Tests/ScheduleTests/TaskTests.swift b/Tests/ScheduleTests/TaskTests.swift index eee3467..adb4c0a 100644 --- a/Tests/ScheduleTests/TaskTests.swift +++ b/Tests/ScheduleTests/TaskTests.swift @@ -3,45 +3,85 @@ import XCTest final class TaskTests: XCTestCase { + let leeway = 0.01.second + + func testMetrics() { + let e = expectation(description: "testMetrics") + let date = Date() + + let task = Plan.after(0.01.second, repeating: 0.01.second).do(queue: .global()) { + e.fulfill() + } + XCTAssertTrue(task.creationDate.interval(since: date).isAlmostEqual(to: 0.second, leeway: leeway)) + + waitForExpectations(timeout: 0.1) + + XCTAssertNotNil(task.firstExecutionDate) + XCTAssertTrue(task.firstExecutionDate!.interval(since: date).isAlmostEqual(to: 0.01.second, leeway: leeway)) + + XCTAssertNotNil(task.lastExecutionDate) + XCTAssertTrue(task.lastExecutionDate!.interval(since: date).isAlmostEqual(to: 0.01.second, leeway: leeway)) + + XCTAssertEqual(task.executionDates!.count, 1) + } + func testAfter() { let e = expectation(description: "testSchedule") let date = Date() - let task = Plan.after(0.1.second).do { - XCTAssertTrue(Date().timeIntervalSince(date).isAlmostEqual(to: 0.1, leeway: 0.1)) + let task = Plan.after(0.01.second).do(queue: .global()) { + XCTAssertTrue(Date().interval(since: date).isAlmostEqual(to: 0.01.second, leeway: self.leeway)) e.fulfill() } - waitForExpectations(timeout: 0.5) - task.cancel() + waitForExpectations(timeout: 0.1) + + _ = task } func testRepeat() { let e = expectation(description: "testRepeat") - var t = 0 - let task = Plan.every(0.1.second).first(3).do { - t += 1 - if t == 3 { e.fulfill() } + var count = 0 + let task = Plan.every(0.01.second).first(3).do(queue: .global()) { + count += 1 + if count == 3 { e.fulfill() } } - waitForExpectations(timeout: 1) - task.cancel() + waitForExpectations(timeout: 0.1) + + _ = task + } + + func testTaskCenter() { + let task = Plan.never.do { } + XCTAssertTrue(task.taskCenter === TaskCenter.default) + + task.removeFromTaskCenter(TaskCenter()) + XCTAssertNotNil(task.taskCenter) + + task.removeFromTaskCenter(task.taskCenter!) + XCTAssertNil(task.taskCenter) + + let center = TaskCenter() + task.addToTaskCenter(center) + XCTAssertTrue(task.taskCenter === center) } func testDispatchQueue() { let e = expectation(description: "testQueue") - let queue = DispatchQueue(label: "testQueue") + let q = DispatchQueue(label: UUID().uuidString) - let task = Plan.after(0.1.second).do(queue: queue) { - XCTAssertTrue(DispatchQueue.is(queue)) + let task = Plan.after(0.01.second).do(queue: q) { + XCTAssertTrue(DispatchQueue.is(q)) e.fulfill() } - waitForExpectations(timeout: 0.5) - task.cancel() + waitForExpectations(timeout: 0.1) + + _ = task } func testThread() { let e = expectation(description: "testThread") DispatchQueue.global().async { let thread = Thread.current - let task = Plan.after(0.1.second).do { task in + let task = Plan.after(0.01.second).do { task in XCTAssertTrue(thread === Thread.current) e.fulfill() task.cancel() @@ -49,103 +89,84 @@ final class TaskTests: XCTestCase { _ = task RunLoop.current.run() } - waitForExpectations(timeout: 0.5) - } - - func testSuspendResume() { - let task1 = Plan.distantFuture.do { } - XCTAssertEqual(task1.suspensions, 0) - task1.suspend() - task1.suspend() - task1.suspend() - XCTAssertEqual(task1.suspensions, 3) - task1.resume() - XCTAssertEqual(task1.suspensions, 2) + waitForExpectations(timeout: 0.1) } - func testAddAndRemoveActions() { - let e = expectation(description: "testAddAndRemoveActions") - let task = Plan.after(0.1.second).do { } - let date = Date() - let key = task.addAction { _ in - XCTAssertTrue(Date().timeIntervalSince(date).isAlmostEqual(to: 0.1, leeway: 0.1)) + func testExecuteNow() { + let e = expectation(description: "testExecuteNow") + let task = Plan.never.do { e.fulfill() } - XCTAssertEqual(task.countOfActions, 2) - waitForExpectations(timeout: 0.5) - - task.removeAction(byKey: key) - XCTAssertEqual(task.countOfActions, 1) - - task.cancel() - - task.removeAllActions() - XCTAssertEqual(task.countOfActions, 0) + task.executeNow() + waitForExpectations(timeout: 0.1) } func testReschedule() { let e = expectation(description: "testReschedule") var i = 0 - let task = Plan.after(0.1.second).do { (task) in + let task = Plan.after(0.01.second).do(queue: .global()) { (task) in i += 1 - if task.countOfExecutions == 6 && task.timeline.estimatedNextExecution == nil { + if task.executionCount == 3 && task.estimatedNextExecutionDate == nil { e.fulfill() } - if task.countOfExecutions > 6 { + if task.executionCount > 3 { XCTFail("should never come here") } } - DispatchQueue.global().async(after: 0.5.second) { - task.reschedule(Plan.every(0.1.second).first(5)) + DispatchQueue.global().async(after: 0.02.second) { + task.reschedule(Plan.every(0.01.second).first(2)) } - waitForExpectations(timeout: 2) - task.cancel() + waitForExpectations(timeout: 0.1) } - func testHost() { - let e = expectation(description: "testHost") - let fn = { - let obj = NSObject() - let task = Plan.after(0.1.second).do(queue: .main, onElapse: { - XCTFail() - }) - task.host(on: obj) - } - fn() - DispatchQueue.main.async(after: 0.2.seconds) { - e.fulfill() - } - waitForExpectations(timeout: 1) + func testSuspendResume() { + let task = Plan.never.do { } + XCTAssertEqual(task.suspensionCount, 0) + task.suspend() + task.suspend() + task.suspend() + XCTAssertEqual(task.suspensionCount, 3) + task.resume() + XCTAssertEqual(task.suspensionCount, 2) } - func testLifetime() { - let e = expectation(description: "testLifetime") - let task = Plan.after(1.hour).do { } - task.setLifetime(1.second) - XCTAssertEqual(task.lifetime, 1.second) + func testCancel() { + let task = Plan.never.do { } + XCTAssertFalse(task.isCancelled) + task.cancel() + XCTAssertTrue(task.isCancelled) + } - DispatchQueue.global().async(after: 0.5.second) { - XCTAssertTrue(task.restOfLifetime.isAlmostEqual(to: 0.5.second, leeway: 0.1.second)) - task.subtractLifetime(-0.5.second) - } - DispatchQueue.global().async(after: 1.second) { - XCTAssertFalse(task.isCancelled) - } - DispatchQueue.global().async(after: 2.second) { - XCTAssertTrue(task.isCancelled) + func testAddAndRemoveActions() { + let e = expectation(description: "testAddAndRemoveActions") + let task = Plan.after(0.1.second).do { } + let date = Date() + let key = task.addAction { _ in + XCTAssertTrue(Date().timeIntervalSince(date).isAlmostEqual(to: 0.1, leeway: 0.1)) e.fulfill() } - waitForExpectations(timeout: 3) + XCTAssertEqual(task.actionCount, 2) + waitForExpectations(timeout: 0.5) + + task.removeAction(byKey: key) + XCTAssertEqual(task.actionCount, 1) + + task.cancel() + + task.removeAllActions() + XCTAssertEqual(task.actionCount, 0) } static var allTests = [ ("testAfter", testAfter), ("testRepeat", testRepeat), + ("testTaskCenter", testTaskCenter), ("testDispatchQueue", testDispatchQueue), ("testThread", testThread), - ("testAddAndRemoveActions", testAddAndRemoveActions), + ("testExecuteNow", testExecuteNow), ("testReschedule", testReschedule), - ("testHost", testHost), - ("testLifetime", testLifetime) + ("testSuspendResume", testSuspendResume), + ("testCancel", testCancel), + ("testAddAndRemoveActions", testAddAndRemoveActions) ] } diff --git a/Tests/ScheduleTests/TimeTests.swift b/Tests/ScheduleTests/TimeTests.swift new file mode 100644 index 0000000..f8518f0 --- /dev/null +++ b/Tests/ScheduleTests/TimeTests.swift @@ -0,0 +1,52 @@ +import XCTest +@testable import Schedule + +final class TimeTests: XCTestCase { + + func testTime() { + let t1 = Time("11:12:13.456") + XCTAssertNotNil(t1) + XCTAssertEqual(t1?.hour, 11) + XCTAssertEqual(t1?.minute, 12) + XCTAssertEqual(t1?.second, 13) + if let i = t1?.nanosecond.nanoseconds { + XCTAssertTrue(i.isAlmostEqual(to: 0.456.second, leeway: 0.001.seconds)) + } + + let t2 = Time("11 pm") + XCTAssertNotNil(t2) + XCTAssertEqual(t2?.hour, 23) + + let t3 = Time("12 am") + XCTAssertNotNil(t3) + XCTAssertEqual(t3?.hour, 0) + + let t4 = Time("schedule") + XCTAssertNil(t4) + } + + func testIntervalSinceStartOfDay() { + XCTAssertEqual(Time(hour: 1)!.intervalSinceStartOfDay, 1.hour) + } + + func testAsDateComponents() { + let time = Time(hour: 11, minute: 12, second: 13, nanosecond: 456) + let components = time?.asDateComponents() + XCTAssertEqual(components?.hour, 11) + XCTAssertEqual(components?.minute, 12) + XCTAssertEqual(components?.second, 13) + XCTAssertEqual(components?.nanosecond, 456) + } + + func testDescription() { + let time = Time("11:12:13.456") + XCTAssertEqual(time!.debugDescription, "Time: 11:12:13.456") + } + + static var allTests = [ + ("testTime", testTime), + ("testIntervalSinceStartOfDay", testIntervalSinceStartOfDay), + ("testAsDateComponents", testAsDateComponents), + ("testDescription", testDescription) + ] +} diff --git a/Tests/ScheduleTests/WeekdayTests.swift b/Tests/ScheduleTests/WeekdayTests.swift new file mode 100644 index 0000000..8e8768b --- /dev/null +++ b/Tests/ScheduleTests/WeekdayTests.swift @@ -0,0 +1,25 @@ +import XCTest +@testable import Schedule + +final class WeekdayTests: XCTestCase { + + func testIs() { + let d = Date(year: 2019, month: 1, day: 1) + XCTAssertTrue(d.is(.tuesday, in: TimeZone.shanghai)) + } + + func testAsDateComponents() { + XCTAssertEqual(Weekday.monday.asDateComponents().weekday!, 2) + } + + func testDescription() { + let wd = Weekday.tuesday + XCTAssertEqual(wd.debugDescription, "Weekday: Tuesday") + } + + static var allTests = [ + ("testIs", testIs), + ("testAsDateComponents", testAsDateComponents), + ("testDescription", testDescription) + ] +} diff --git a/Tests/ScheduleTests/XCTestManifests.swift b/Tests/ScheduleTests/XCTestManifests.swift index 39c0505..94f1e06 100644 --- a/Tests/ScheduleTests/XCTestManifests.swift +++ b/Tests/ScheduleTests/XCTestManifests.swift @@ -3,14 +3,17 @@ import XCTest #if os(Linux) public func allTests() -> [XCTestCaseEntry] { return [ - testCase(DateTimeTests.allTests), + testCase(AtomicTests.allTests), + testCase(BagTests.allTests), + testCase(ExtensionsTests.allTests), + testCase(IntervalTests.allTests), + testCase(MonthdayTests.allTests), + testCase(PeriodTests.allTests), testCase(PlanTests.allTests), testCase(TaskCenterTests.allTests), testCase(TaskTests.allTests), - testCase(AtomicTests.allTests), - testCase(CabinetTests.allTests), - testCase(CalendarTests.allTests), - testCase(ExtensionsTests.allTests) + testCase(TimeTests.allTests), + testCase(WeekdayTests.allTests) ] } #endif diff --git a/assets/demo.png b/assets/demo.png index 97e1c35..d1ef88d 100644 Binary files a/assets/demo.png and b/assets/demo.png differ diff --git a/ducumentations/Schedule 2.0 Migration Guide.md b/ducumentations/Schedule 2.0 Migration Guide.md new file mode 100644 index 0000000..e69de29