Improve api, add tests

This commit is contained in:
QuentinJin 2018-07-26 22:06:18 +08:00
parent f48758d4f7
commit 792eabf3d4
17 changed files with 639 additions and 347 deletions

View File

@ -1,18 +1,20 @@
# Schedule
⏰ A interval-based and date-based task scheduler for swift, with incredibly sweet apis.
⏰ An interval-based and date-based task scheduler for swift, with incredibly sweet api.
## Features
- 📆 Date-based scheduling
- ⏳ Interval-based scheduling
- 📝 Mixture rules scheduling
- 📝 Mixture rules
- 👩‍🎓‍ Human readable period parsing
- 🚦 Suspend, resume, cancel
- 🏷 Tag related management
- 🏷 Tag-based management
- 🍰 Action appending/removing
- 🍻 No need to concern about runloop
- 👻 No need to concern about circular reference
- 🍭 **Sweet apis**
- 🍭 **Incredibly Sweet API**
## Usage
@ -20,10 +22,12 @@
Scheduling a task can not be simplier.
```swift
func heartBeat() { }
Schedule.every(0.5.seconds).do(heartBeat)
Schedule.every(1.second).do {
print("heart beat")
}
```
### Interval-based Scheduling
```swift
@ -43,7 +47,6 @@ Schedule.from([1.second, 2.minutes, 3.hours]).do { }
```
### Date-based Scheduling
```swift
@ -55,14 +58,15 @@ Schedule.every(.monday, .tuesday).at("11:11").do { }
Schedule.every(.september(30)).at("10:00:00").do { }
Schedule.every("one month and ten days").do { }
Schedule.of(date0, date1, date2).do { }
Schedule.from([date0, date1, date2]).do { }
```
### Custom Scheduling
### Mixture rules
```swift
import Schedule
@ -76,28 +80,27 @@ birthdaySchedule.do {
}
/// merge
let s3 = Schedule.every(.january(1)).at(8, 30)
let s4 = Schedule.every(.october(1)).at(8, 30)
let s3 = Schedule.every(.january(1)).at("8:00")
let s4 = Schedule.every(.october(1)).at("9:00 AM")
let holiday = s3.merge(s3)
holidaySchedule.do {
print("Happy holiday")
}
/// count
/// cut
let s5 = Schedule.after(5.seconds).concat(Schedule.every(1.day))
let s6 = s5.count(10)
let s6 = s5.cut(10)
/// until
let s7 = Schedule.every(.monday).at(11, 12)
let s8 = s7.until(date)
```
### Task management
In general, you don't need to care about the reference management of task. All tasks will be held by a inner shared instance of class `TaskCenter`, so they won't be released, unless you do that yourself.
In general, you don't need to concern about the reference management of task. All tasks will be retained internally, so they won't be released, unless you do it yourself.
#### Operation
#### Manipulation
```swift
let task = Schedule.every(1.day).do { }
@ -142,7 +145,22 @@ let key = dailyTask.addAction {
dailyTask.removeAction(byKey: key)
```
There is also a more elegant way to deal with task's lifecycle:
### Lifecycle
You can get the current timeline of the task:
```swift
let timeline = task.timeline
print(timeline.firstSchedule)
print(timeline.lastSchedule)
print(timeline.lastSchedule)
print(timeline.activate)
print(timeline.cancel)
```
### Parasitism
There is a more elegant way to deal with task's lifecycle:
```swift
Schedule.every(1.second).do(dependOn: self) {
@ -152,11 +170,11 @@ Schedule.every(1.second).do(dependOn: self) {
## Contribution
Feel free to criticize!
Feel free to criticize. 🍺
## Installation
Schedul supports all popular dependency managers.
Schedule supports all popular dependency managers.
### Cocoapods
@ -174,6 +192,6 @@ github "jianstm/Schedule"
```swift
dependencies: [
.package(url: "https://github.com/jianstm/Schedule", .exact("0.0.4"))
.package(url: "https://github.com/jianstm/Schedule", .upToNextMinor("0.0.0"))
]
```

View File

@ -30,6 +30,7 @@
6624105421075FDC00013B00 /* ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6624105221075F8A00013B00 /* ExtensionsTests.swift */; };
66241056210761F000013B00 /* BucketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66241055210761F000013B00 /* BucketTests.swift */; };
662410592107804400013B00 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662410582107804400013B00 /* Lock.swift */; };
666AEAF32109F90C00A50F27 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 666AEAF22109F90C00A50F27 /* Atomic.swift */; };
669D215020FE1B7300AFFDF7 /* Interval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669D214F20FE1B7300AFFDF7 /* Interval.swift */; };
669D215220FE1B8A00AFFDF7 /* Time.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669D215120FE1B8A00AFFDF7 /* Time.swift */; };
669D215420FE1BA600AFFDF7 /* Period.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669D215320FE1BA600AFFDF7 /* Period.swift */; };
@ -41,8 +42,8 @@
OBJ_29 /* Schedule.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* Schedule.swift */; };
OBJ_37 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; };
OBJ_48 /* DateTimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* DateTimeTests.swift */; };
OBJ_49 /* ScheduleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_16 /* ScheduleTests.swift */; };
OBJ_50 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* Util.swift */; };
OBJ_49 /* SchedulesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_16 /* SchedulesTests.swift */; };
OBJ_50 /* Misc.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* Misc.swift */; };
OBJ_51 /* XCTestManifests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_18 /* XCTestManifests.swift */; };
OBJ_53 /* Schedule.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "Schedule::Schedule::Product" /* Schedule.framework */; };
/* End PBXBuildFile section */
@ -66,6 +67,7 @@
/* Begin PBXFileReference section */
62661F402108433300055501 /* WeakSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakSet.swift; sourceTree = "<group>"; };
626C7209210968C6005F10A7 /* LinuxMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LinuxMain.swift; path = Tests/LinuxMain.swift; sourceTree = "<group>"; };
62EF93F821085309001F7A47 /* WeakSetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakSetTests.swift; sourceTree = "<group>"; };
62EF93FD21086411001F7A47 /* TaskCenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskCenterTests.swift; sourceTree = "<group>"; };
62EF940721086F63001F7A47 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = "<group>"; };
@ -75,6 +77,7 @@
6624105221075F8A00013B00 /* ExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionsTests.swift; sourceTree = "<group>"; };
66241055210761F000013B00 /* BucketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BucketTests.swift; sourceTree = "<group>"; };
662410582107804400013B00 /* Lock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lock.swift; sourceTree = "<group>"; };
666AEAF22109F90C00A50F27 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; };
669D214F20FE1B7300AFFDF7 /* Interval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interval.swift; sourceTree = "<group>"; };
669D215120FE1B8A00AFFDF7 /* Time.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Time.swift; sourceTree = "<group>"; };
669D215320FE1BA600AFFDF7 /* Period.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Period.swift; sourceTree = "<group>"; };
@ -86,8 +89,8 @@
OBJ_10 /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = "<group>"; };
OBJ_11 /* Schedule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Schedule.swift; sourceTree = "<group>"; };
OBJ_15 /* DateTimeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeTests.swift; sourceTree = "<group>"; };
OBJ_16 /* ScheduleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleTests.swift; sourceTree = "<group>"; };
OBJ_17 /* Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Util.swift; sourceTree = "<group>"; };
OBJ_16 /* SchedulesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchedulesTests.swift; sourceTree = "<group>"; };
OBJ_17 /* Misc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Misc.swift; sourceTree = "<group>"; };
OBJ_18 /* XCTestManifests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestManifests.swift; sourceTree = "<group>"; };
OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
"Schedule::Schedule::Product" /* Schedule.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Schedule.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -113,22 +116,14 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
62EF93FB21085312001F7A47 /* Utils */ = {
62EF93FB21085312001F7A47 /* UtilsTests */ = {
isa = PBXGroup;
children = (
6624105221075F8A00013B00 /* ExtensionsTests.swift */,
66241055210761F000013B00 /* BucketTests.swift */,
62EF93F821085309001F7A47 /* WeakSetTests.swift */,
);
name = Utils;
sourceTree = "<group>";
};
62EF93FC210863FA001F7A47 /* DateTime */ = {
isa = PBXGroup;
children = (
OBJ_15 /* DateTimeTests.swift */,
);
name = DateTime;
name = UtilsTests;
sourceTree = "<group>";
};
662410462104A0EC00013B00 /* Utils */ = {
@ -138,6 +133,7 @@
6624104D2104AF2100013B00 /* Extensions.swift */,
662410582107804400013B00 /* Lock.swift */,
62661F402108433300055501 /* WeakSet.swift */,
666AEAF22109F90C00A50F27 /* Atomic.swift */,
);
name = Utils;
sourceTree = "<group>";
@ -166,6 +162,7 @@
OBJ_13 /* Tests */ = {
isa = PBXGroup;
children = (
626C7209210968C6005F10A7 /* LinuxMain.swift */,
OBJ_14 /* ScheduleTests */,
);
name = Tests;
@ -174,12 +171,12 @@
OBJ_14 /* ScheduleTests */ = {
isa = PBXGroup;
children = (
62EF93FC210863FA001F7A47 /* DateTime */,
62EF93FB21085312001F7A47 /* Utils */,
OBJ_16 /* ScheduleTests.swift */,
OBJ_17 /* Util.swift */,
OBJ_18 /* XCTestManifests.swift */,
OBJ_15 /* DateTimeTests.swift */,
OBJ_17 /* Misc.swift */,
OBJ_16 /* SchedulesTests.swift */,
62EF93FD21086411001F7A47 /* TaskCenterTests.swift */,
OBJ_18 /* XCTestManifests.swift */,
62EF93FB21085312001F7A47 /* UtilsTests */,
);
name = ScheduleTests;
path = Tests/ScheduleTests;
@ -315,6 +312,7 @@
669D215020FE1B7300AFFDF7 /* Interval.swift in Sources */,
669D215620FE1BB400AFFDF7 /* Weekday.swift in Sources */,
62EF940821086F63001F7A47 /* Timeline.swift in Sources */,
666AEAF32109F90C00A50F27 /* Atomic.swift in Sources */,
OBJ_28 /* Task.swift in Sources */,
OBJ_29 /* Schedule.swift in Sources */,
62661F412108433400055501 /* WeakSet.swift in Sources */,
@ -343,8 +341,8 @@
6624105421075FDC00013B00 /* ExtensionsTests.swift in Sources */,
62EF93FA2108530D001F7A47 /* WeakSetTests.swift in Sources */,
OBJ_48 /* DateTimeTests.swift in Sources */,
OBJ_49 /* ScheduleTests.swift in Sources */,
OBJ_50 /* Util.swift in Sources */,
OBJ_49 /* SchedulesTests.swift in Sources */,
OBJ_50 /* Misc.swift in Sources */,
OBJ_51 /* XCTestManifests.swift in Sources */,
66241056210761F000013B00 /* BucketTests.swift in Sources */,
62EF93FF21086420001F7A47 /* TaskCenterTests.swift in Sources */,

View File

@ -0,0 +1,34 @@
//
// Atomic.swift
// Schedule
//
// Created by Quentin Jin on 2018/7/26.
//
import Foundation
class Atomic<T> {
private var value: T
private var lock = Lock()
init(_ value: T) {
self.value = value
}
func execute(_ body: (T) -> Void) {
lock.withLock { body(value) }
}
func mutate(_ body: (inout T) -> Void) {
lock.withLock { body(&value) }
}
func read() -> T {
return lock.withLock { value }
}
func write(_ new: T) {
lock.withLock { value = new }
}
}

View File

@ -8,21 +8,14 @@
import Foundation
/// `Interval` represents a length of time.
///
/// The value range of interval is [Int.min.nanoseconds...Int.max.nanoseconds],
/// that is, about -292.years ~ 292.years, enough for us!
public struct Interval {
let ns: Int
/// The length of this interval in nanoseconds.
public var nanoseconds: Double {
return Double(ns)
}
public var nanoseconds: Double
/// Creates an interval from the given number of nanoseconds.
public init(nanoseconds: Double) {
self.ns = nanoseconds.clampedToInt()
self.nanoseconds = nanoseconds
}
/// A boolean value indicating whether this interval is negative.
@ -37,13 +30,13 @@ public struct Interval {
/// but the interval comparing `1.hour` to `3.hour` is `-2.hour`.
/// In this case, `-2.hour` means **two hours shorter**
public var isNegative: Bool {
return ns < 0
return nanoseconds.isLess(than: 0)
}
/// The absolute value of the length of this interval,
/// measured in nanoseconds, but disregarding its sign.
public var magnitude: UInt {
return ns.magnitude
public var magnitude: Double {
return nanoseconds.magnitude
}
}
@ -72,25 +65,25 @@ extension Interval {
return intervals.sorted(by: { $0.magnitude < $1.magnitude })[0]
}
/// Returns a new interval by multipling the left interval by the right number.
/// Returns a new interval by multipling this interval by a double number.
///
/// 1.hour * 2 == 2.hours
public static func *(lhs: Interval, rhs: Double) -> Interval {
return Interval(nanoseconds: lhs.nanoseconds * rhs)
public func multiplying(by number: Double) -> Interval {
return Interval(nanoseconds: nanoseconds * number)
}
/// Returns a new interval by adding the right interval to the left interval.
/// Returns a new interval by adding an interval to this interval.
///
/// 1.hour + 1.hour == 2.hours
public static func +(lhs: Interval, rhs: Interval) -> Interval {
return Interval(nanoseconds: lhs.nanoseconds + rhs.nanoseconds)
public func adding(_ other: Interval) -> Interval {
return Interval(nanoseconds: nanoseconds + other.nanoseconds)
}
/// Returns a new instarval by subtracting the right interval from the left interval.
/// Returns a new interval by subtracting an interval from this interval.
///
/// 2.hours - 1.hour == 1.hour
public static func -(lhs: Interval, rhs: Interval) -> Interval {
return Interval(nanoseconds: lhs.nanoseconds - rhs.nanoseconds)
public func subtracting(_ other: Interval) -> Interval {
return Interval(nanoseconds: nanoseconds - other.nanoseconds)
}
}
@ -127,6 +120,31 @@ extension Interval {
}
}
extension Interval {
/// Returns a new interval by multipling the left interval by the right number.
///
/// 1.hour * 2 == 2.hours
public static func *(lhs: Interval, rhs: Double) -> Interval {
return lhs.multiplying(by: rhs)
}
/// Returns a new interval by adding the right interval to the left interval.
///
/// 1.hour + 1.hour == 2.hours
public static func +(lhs: Interval, rhs: Interval) -> Interval {
return lhs.adding(rhs)
}
/// Returns a new interval by subtracting the right interval from the left interval.
///
/// 2.hours - 1.hour == 1.hour
public static func -(lhs: Interval, rhs: Interval) -> Interval {
return lhs.subtracting(rhs)
}
}
extension Interval: Hashable {
/// The hashValue of this interval.
@ -140,6 +158,7 @@ extension Interval: Hashable {
}
}
extension Date {
/// The interval between this date and the current date and time.
@ -157,21 +176,17 @@ extension Date {
}
/// Returns a new date by adding an interval to this date.
public func addingInterval(_ interval: Interval) -> Date {
public func adding(_ interval: Interval) -> Date {
return addingTimeInterval(interval.seconds)
}
/// Returns a date with an interval added to it.
public static func +(lhs: Date, rhs: Interval) -> Date {
return lhs.addingInterval(rhs)
}
/// Adds a interval to the date.
public static func +=(lhs: inout Date, rhs: Interval) {
lhs = lhs + rhs
return lhs.adding(rhs)
}
}
extension DispatchSourceTimer {
func schedule(after interval: Interval) {
@ -179,16 +194,20 @@ extension DispatchSourceTimer {
schedule(wallDeadline: .distantFuture)
return
}
schedule(wallDeadline: .now() + DispatchTimeInterval.nanoseconds(interval.ns))
let ns = interval.nanoseconds.clampedToInt()
schedule(wallDeadline: .now() + DispatchTimeInterval.nanoseconds(ns))
}
}
/// `IntervalConvertible` provides a set of intuitive api for creating interval.
public protocol IntervalConvertible {
var nanoseconds: Interval { get }
}
extension Int: IntervalConvertible {
public var nanoseconds: Interval {
@ -196,6 +215,7 @@ extension Int: IntervalConvertible {
}
}
extension Double: IntervalConvertible {
public var nanoseconds: Interval {
@ -203,6 +223,7 @@ extension Double: IntervalConvertible {
}
}
extension IntervalConvertible {
public var nanosecond: Interval {

View File

@ -7,7 +7,7 @@
import Foundation
/// `MonthDay` represents a day in month, without a time.
/// `Monthday` represents a day in month, without a time.
public enum MonthDay {
case january(Int)

View File

@ -11,21 +11,23 @@ extension Schedule {
/// Schedules a task with this schedule.
///
/// This method will receive a `host` object as parameter,
/// This method will receive a `host` object as a parameter,
/// the returned task will not retain this object, on the contrary,
/// it will observe this object: when this object is dealloced,
/// task will not be scheduled any more, something parasitism.
/// it will observe this object, when this object is dealloced,
/// task will not be scheduled any more, something like parasitism.
///
/// This feature is very useful when you want a timer live and die
/// with a controller.
///
/// - Parameters:
/// - queue: The dispatch queue to which the task will be submitted.
/// - tag: The tag to associate with the task.
/// - host: The object to host on.
/// - onElapse: The action to do when time is out.
/// - Returns: The task just created.
@discardableResult
public func `do`(queue: DispatchQueue? = nil,
tag: String? = nil,
host: AnyObject,
onElapse: @escaping (Task) -> Void) -> Task {
return ParasiticTask(schedule: self, queue: queue, host: host, onElapse: onElapse)
@ -33,9 +35,9 @@ extension Schedule {
/// Schedules a task with this schedule.
///
/// This method will receive a `host` object as parameter,
/// This method will receive a `host` object as a parameter,
/// the returned task will not retain this object, on the contrary,
/// it will observe this object: when this object is dealloced,
/// it will observe this object, when this object is dealloced,
/// task will not scheduled any more, something like parasitism.
///
/// This feature is very useful when you want a timer live and die
@ -43,11 +45,13 @@ extension Schedule {
///
/// - Parameters:
/// - queue: The dispatch queue to which the task will be submitted.
/// - tag: The tag to associate with the task.
/// - host: The object to host on.
/// - onElapse: The action to do when time is out.
/// - Returns: The task just created.
@discardableResult
public func `do`(queue: DispatchQueue? = nil,
tag: String? = nil,
host: AnyObject,
onElapse: @escaping () -> Void) -> Task {
return self.do(queue: queue, host: host, onElapse: { (_) in onElapse() })

View File

@ -24,19 +24,19 @@ import Foundation
/// in these two cases are quite different.
public struct Period {
public let years: Int
public private(set) var years: Int
public let months: Int
public private(set) var months: Int
public let days: Int
public private(set) var days: Int
public let hours: Int
public private(set) var hours: Int
public let minutes: Int
public private(set) var minutes: Int
public let seconds: Int
public private(set) var seconds: Int
public let nanoseconds: Int
public private(set) var nanoseconds: Int
public init(years: Int = 0, months: Int = 0, days: Int = 0,
hours: Int = 0, minutes: Int = 0, seconds: Int = 0,
@ -50,27 +50,121 @@ public struct Period {
self.nanoseconds = nanoseconds
}
/// Returns a new date by adding the right period to the left period.
private static var map: Atomic<[String: Int]> = Atomic([
"one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6,
"seven": 7, "eight": 8, "nine": 9, "ten": 10, "eleven": 11, "twelve": 12
])
public static func registerQuantifier(_ word: String, for number: Int) {
map.mutate {
$0[word] = number
}
}
/// Creates 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)
public init?(_ string: String) {
var period = string
for (word, number) in Period.map.read() {
period = period.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 word = String(pair[1])
if word.last == "s" { word.removeLast() }
switch word {
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
default: break
}
}
self = result
}
/// Returns a new period by adding a 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))
}
/// Returns a new period by adding an interval to this period.
///
/// You can tidy the new period by specify the unit parameter.
///
/// 1.month.adding(25.hour, tiyding: .day) => Period(months: 1, days: 1, hours: 1)
public func adding(_ interval: Interval, tidying unit: Unit = .day) -> Period {
var period = Period(years: years, months: months, days: days,
hours: hours, minutes: minutes, seconds: seconds,
nanoseconds: nanoseconds.clampedAdding(interval.nanoseconds.clampedToInt()))
period = period.tidied(to: unit)
return period
}
/// Adds two periods and produces their sum.
public static func +(lhs: Period, rhs: Period) -> Period {
return Period(years: lhs.years.clampedAdding(rhs.years),
months: lhs.months.clampedAdding(rhs.months),
days: lhs.days.clampedAdding(rhs.days),
hours: lhs.hours.clampedAdding(rhs.hours),
minutes: lhs.minutes.clampedAdding(rhs.minutes),
seconds: lhs.seconds.clampedAdding(rhs.seconds),
nanoseconds: lhs.nanoseconds.clampedAdding(rhs.nanoseconds))
return lhs.adding(rhs)
}
/// Returns a date with a period added to it.
public static func +(lhs: Date, rhs: Period) -> Date {
return Calendar.autoupdatingCurrent.date(byAdding: rhs.asDateComponents(), to: lhs) ?? .distantFuture
}
/// Return a period with a interval added to it.
/// Returns a period with an interval added to it.
public static func +(lhs: Period, rhs: Interval) -> Period {
return Period(years: lhs.years, months: lhs.months, days: lhs.days,
hours: lhs.hours, minutes: lhs.minutes, seconds: lhs.seconds,
nanoseconds: lhs.nanoseconds.clampedAdding(rhs.ns))
return lhs.adding(rhs)
}
/// Type used to as tidy's parameter.
public enum Unit {
case day, hour, minute, second, nanosecond
}
/// Returns the tidied period.
///
/// Period(hours: 25).tidied(to .day) => Period(days: 1, hours: 1)
public func tidied(to unit: Unit) -> Period {
var period = self
if case .nanosecond = unit { return period }
if period.nanoseconds.magnitude >= UInt(NSEC_PER_SEC) {
period.seconds += period.nanoseconds / Int(NSEC_PER_SEC)
period.nanoseconds %= Int(NSEC_PER_SEC)
}
if case .second = unit { return period }
if period.seconds.magnitude >= 60 {
period.minutes += period.seconds / 60
period.seconds %= 60
}
if case .minute = unit { return period }
if period.minutes.magnitude >= 60 {
period.hours += period.minutes / 60
period.minutes %= 60
}
if case .hour = unit { return period }
if period.hours.magnitude >= 24 {
period.days += period.hours / 24
period.hours %= 24
}
return period
}
func asDateComponents() -> DateComponents {
@ -80,6 +174,19 @@ public struct Period {
}
}
extension Date {
/// Returns a new date by adding a period to this date.
public func adding(_ period: Period) -> Date {
return Calendar(identifier: .gregorian).date(byAdding: period.asDateComponents(), to: self) ?? .distantFuture
}
/// Returns a date with a period added to it.
public static func +(lhs: Date, rhs: Period) -> Date {
return lhs.adding(rhs)
}
}
extension Int {
public var years: Period {

View File

@ -8,12 +8,9 @@
import Foundation
/// `Schedule` represents a plan that gives the times
/// at which a task should be invoked.
/// at which a task should be executed.
///
/// `Schedule` is interval based.
/// When a new task is created in the `do` method, it will ask for the first
/// interval in this schedule, then set up a timer to invoke itself
/// after the interval.
public struct Schedule {
private var sequence: AnySequence<Interval>
@ -29,7 +26,7 @@ public struct Schedule {
///
/// - Parameters:
/// - queue: The dispatch queue to which the task will be submitted.
/// - tag: The tag to attach to the task.
/// - tag: The tag to associate with the task.
/// - onElapse: The task to invoke when time is out.
/// - Returns: The task just created.
@discardableResult
@ -43,7 +40,7 @@ public struct Schedule {
///
/// - Parameters:
/// - queue: The dispatch queue to which the task will be submitted.
/// - tag: The tag to attach to the queue
/// - tag: The tag to associate with the queue
/// - onElapse: The task to invoke when time is out.
/// - Returns: The task just created.
@discardableResult
@ -58,7 +55,7 @@ extension Schedule {
/// Creates a schedule from a `makeUnderlyingIterator()` method.
///
/// The task will be invoke after each interval
/// The task will be executed after each interval
/// produced by the iterator that `makeUnderlyingIterator` returns.
///
/// For example:
@ -84,13 +81,13 @@ extension Schedule {
}
/// Creates a schedule from an interval sequence.
/// The task will be invoke after each interval in the sequence.
/// The task will be executed after each interval in the sequence.
public static func from<S>(_ sequence: S) -> Schedule where S: Sequence, S.Element == Interval {
return Schedule(sequence)
}
/// Creates a schedule from an interval array.
/// The task will be invoke after each interval in the array.
/// The task will be executed after each interval in the array.
public static func of(_ intervals: Interval...) -> Schedule {
return Schedule(intervals)
}
@ -100,7 +97,7 @@ extension Schedule {
/// Creates a schedule from a `makeUnderlyingIterator()` method.
///
/// The task will be invoke at each date
/// The task will be executed at each date
/// produced by the iterator that `makeUnderlyingIterator` returns.
///
/// For example:
@ -116,33 +113,33 @@ extension Schedule {
/// }
///
/// > "now: 2001-01-01 00:00:00"
/// > "task: 2001-01-01 00:00:03
/// > "task: 2001-01-01 00:00:03"
/// ...
///
/// You should not return `Date()` in making interator
/// if you want to invoke a task immediately,
/// You are not supposed to return `Date()` in making interator,
/// if you want to execute a task immediately,
/// use `Schedule.now` then `concat` another schedule instead.
public static func make<I>(_ makeUnderlyingIterator: @escaping () -> I) -> Schedule where I: IteratorProtocol, I.Element == Date {
return Schedule.make { () -> AnyIterator<Interval> in
var iterator = makeUnderlyingIterator()
var previous: Date!
var last: Date!
return AnyIterator {
previous = previous ?? Date()
last = last ?? Date()
guard let next = iterator.next() else { return nil }
defer { previous = next }
return next.interval(since: previous)
defer { last = next }
return next.interval(since: last)
}
}
}
/// Creates a schedule from a date sequence.
/// The task will be invoke at each date in the sequence.
/// The task will be executed at each date in the sequence.
public static func from<S>(_ sequence: S) -> Schedule where S: Sequence, S.Element == Date {
return Schedule.make(sequence.makeIterator)
}
/// Creates a schedule from a date array.
/// The task will be invoke at each date in the array.
/// The task will be executed at each date in the array.
public static func of(_ dates: Date...) -> Schedule {
return Schedule.from(dates)
}
@ -151,12 +148,12 @@ extension Schedule {
public var dates: AnySequence<Date> {
return AnySequence { () -> AnyIterator<Date> in
let iterator = self.makeIterator()
var previous: Date!
var last: Date!
return AnyIterator {
previous = previous ?? Date()
last = last ?? Date()
guard let interval = iterator.next() else { return nil }
previous = previous + interval
return previous
last = last + interval
return last
}
}
}
@ -174,7 +171,7 @@ extension Schedule {
return Schedule.of(Date.distantFuture)
}
/// A schedule with no date.
/// A schedule that is never going to happen.
public static var never: Schedule {
return Schedule.make {
AnyIterator<Date> { nil }
@ -191,6 +188,7 @@ extension Schedule {
/// let s0 = Schedule.of(1.second, 2.seconds, 3.seconds)
/// let s1 = Schedule.of(4.seconds, 4.seconds, 4.seconds)
/// let s2 = s0.concat(s1)
///
/// > s2
/// > 1.second, 2.seconds, 3.seconds, 4.seconds, 4.seconds, 4.seconds
public func concat(_ schedule: Schedule) -> Schedule {
@ -212,14 +210,14 @@ extension Schedule {
/// let s1 = Schedule.of(2.seconds, 4.seconds, 6.seconds)
/// let s2 = s0.concat(s1)
/// > s2
/// > 1.second, 1.second, 1.second, 1.second, 1.second, 1.second
/// > 1.second, 1.seconds, 2.seconds, 2.seconds, 3.seconds, 3.seconds
public func merge(_ schedule: Schedule) -> Schedule {
return Schedule.make { () -> AnyIterator<Interval> in
return Schedule.make { () -> AnyIterator<Date> in
let i0 = self.dates.makeIterator()
let i1 = schedule.dates.makeIterator()
var buffer0: Date!
var buffer1: Date!
let iterator = AnyIterator<Date> {
return AnyIterator<Date> {
if buffer0 == nil { buffer0 = i0.next() }
if buffer1 == nil { buffer1 = i1.next() }
@ -234,149 +232,149 @@ extension Schedule {
if d == buffer1 { buffer1 = nil }
return d
}
return Schedule.make({ iterator }).makeIterator()
}
}
/// Returns a new schedule by cutting out a specific number of this schedule.
/// Returns a new schedule by only taking the first specific number of this schedule.
///
/// For example:
///
/// let s0 = Schedule.every(1.second)
/// let s1 = s0.count(3)
/// let s1 = s0.first(3)
/// > s1
/// 1.second, 1.second, 1.second
public func count(_ count: Int) -> Schedule {
public func first(_ count: Int) -> Schedule {
return Schedule.make { () -> AnyIterator<Interval> in
let iterator = self.makeIterator()
var tick = 0
var num = 0
return AnyIterator {
guard tick < count, let interval = iterator.next() else { return nil }
tick += 1
guard num < count, let interval = iterator.next() else { return nil }
num += 1
return interval
}
}
}
/// Returns a new schedule by cutting out the part which is before the date.
/// Returns a new schedule by only taking the part before the date.
public func until(_ date: Date) -> Schedule {
return Schedule.make { () -> AnyIterator<Interval> in
let iterator = self.makeIterator()
var previous: Date!
return Schedule.make { () -> AnyIterator<Date> in
let iterator = self.dates.makeIterator()
return AnyIterator {
previous = previous ?? Date()
guard let interval = iterator.next(),
previous.addingTimeInterval(interval.seconds) < date else {
guard let next = iterator.next(), next < date else {
return nil
}
previous.addTimeInterval(interval.seconds)
return interval
return next
}
}
}
/// Creates a schedule that invokes the task immediately.
/// Creates a schedule that executes the task immediately.
public static var now: Schedule {
return Schedule.of(0.nanosecond)
}
/// Creates a schedule that invokes the task after the delay.
/// Creates a schedule that executes the task after delay.
public static func after(_ delay: Interval) -> Schedule {
return Schedule.of(delay)
}
/// Creates a schedule that invokes the task every interval.
/// Creates a schedule that executes the task every interval.
public static func every(_ interval: Interval) -> Schedule {
return Schedule.make {
AnyIterator { interval }
}
}
/// Creates a schedule that invokes the task at the specific date.
/// Creates a schedule that executes the task at the specific date.
public static func at(_ date: Date) -> Schedule {
return Schedule.of(date)
}
/// Creates a schedule that invokes the task after the delay then repeat
/// Creates a schedule that executes the task after delay then repeat
/// every interval.
public static func after(_ delay: Interval, repeating interval: Interval) -> Schedule {
return Schedule.after(delay).concat(Schedule.every(interval))
}
/// Creates a schedule that invokes the task every period.
/// Creates a schedule that executes the task every period.
public static func every(_ period: Period) -> Schedule {
return Schedule.make { () -> AnyIterator<Interval> in
let calendar = Calendar.autoupdatingCurrent
var previous: Date!
let calendar = Calendar(identifier: .gregorian)
var last: Date!
return AnyIterator {
previous = previous ?? Date()
last = last ?? Date()
guard let next = calendar.date(byAdding: period.asDateComponents(),
to: previous) else {
to: last) else {
return nil
}
defer { previous = next }
return next.interval(since: previous)
defer { last = next }
return next.interval(since: last)
}
}
}
/// Creates a schedule that executes the task every period.
///
/// See Period's constructor
public static func every(_ period: String) -> Schedule {
guard let p = Period(period) else {
return Schedule.never
}
return Schedule.every(p)
}
}
extension Schedule {
/// `EveryDateMiddleware` represents a middleware that wraps a schedule
/// which only specify date without time.
/// `DateMiddleware` represents a middleware that wraps a schedule
/// which was only specified date without time.
///
/// You should call `at` method to get the time specified schedule.
public struct EveryDateMiddleware {
/// You should call `at` method to get the schedule with time specified.
public struct DateMiddleware {
fileprivate let schedule: Schedule
/// Returns a schedule at the specific timing.
public func at(_ timing: Time) -> Schedule {
return Schedule.make { () -> AnyIterator<Interval> in
/// Returns a schedule at the specific time.
public func at(_ time: Time) -> Schedule {
return Schedule.make { () -> AnyIterator<Date> in
let iterator = self.schedule.dates.makeIterator()
let calendar = Calendar.autoupdatingCurrent
var previous: Date!
var calendar = Calendar(identifier: .gregorian)
var last: Date!
return AnyIterator {
previous = previous ?? Date()
last = last ?? Date()
guard let date = iterator.next(),
let next = calendar.nextDate(after: date,
matching: timing.asDateComponents(),
matching: time.asDateComponents(),
matchingPolicy: .strict) else {
return nil
}
defer { previous = next }
return next.interval(since: previous)
defer { last = next }
return next
}
}
}
/// Returns a schedule at the specific timing.
/// Returns a schedule at the specific time.
///
/// For example:
///
/// let s = Schedule.every(.monday).at("11:11")
///
/// Available format:
///
/// 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: 123)
public func at(_ timing: String) -> Schedule {
guard let time = Time(timing: timing) else {
/// See Time's constructor
public func at(_ time: String) -> Schedule {
guard let time = Time(time) else {
return Schedule.never
}
return at(time)
}
/// Returns a schedule at the specific timing.
public func at(_ timing: Int...) -> Schedule {
let hour = timing[0]
let minute = timing.count > 1 ? timing[1] : 0
let second = timing.count > 2 ? timing[2] : 0
let nanosecond = timing.count > 3 ? timing[3]: 0
/// Returns a schedule at the specific time.
///
/// .at(1) => 01
/// .at(1, 2) => 01:02
/// .at(1, 2, 3) => 01:02:03
/// .at(1, 2, 3, 456) => 01:02:03.456
public func at(_ time: Int...) -> Schedule {
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 {
return Schedule.never
@ -385,13 +383,13 @@ extension Schedule {
}
}
/// Creates a schedule that invokes the task every specific weekday.
public static func every(_ weekday: Weekday) -> EveryDateMiddleware {
let schedule = Schedule.make { () -> AnyIterator<Interval> in
let calendar = Calendar.autoupdatingCurrent
/// Creates a schedule that executes the task every specific weekday.
public static func every(_ weekday: Weekday) -> DateMiddleware {
let schedule = Schedule.make { () -> AnyIterator<Date> in
let calendar = Calendar(identifier: .gregorian)
let components = DateComponents(weekday: weekday.rawValue)
var date: Date!
let iterator = AnyIterator<Date> {
return AnyIterator<Date> {
if date == nil {
date = calendar.nextDate(after: Date(), matching: components, matchingPolicy: .strict)
} else {
@ -399,30 +397,29 @@ extension Schedule {
}
return date
}
return Schedule.make({ iterator }).makeIterator()
}
return EveryDateMiddleware(schedule: schedule)
return DateMiddleware(schedule: schedule)
}
/// Creates a schedule that invokes the task every specific weekdays.
public static func every(_ weekdays: Weekday...) -> EveryDateMiddleware {
/// Creates a schedule that executes the task every specific weekdays.
public static func every(_ weekdays: Weekday...) -> DateMiddleware {
var schedule = every(weekdays[0]).schedule
if weekdays.count > 1 {
for i in 1..<weekdays.count {
schedule = schedule.merge(Schedule.every(weekdays[i]).schedule)
}
}
return EveryDateMiddleware(schedule: schedule)
return DateMiddleware(schedule: schedule)
}
/// Creates a schedule that invokes the task every specific day in the month.
public static func every(_ monthDay: MonthDay) -> EveryDateMiddleware {
let schedule = Schedule.make { () -> AnyIterator<Interval> in
let calendar = Calendar.autoupdatingCurrent
/// Creates a schedule that executes the task every specific day in the month.
public static func every(_ monthDay: MonthDay) -> DateMiddleware {
let schedule = Schedule.make { () -> AnyIterator<Date> in
let calendar = Calendar(identifier: .gregorian)
let components = monthDay.asDateComponents()
var date: Date!
let iterator = AnyIterator<Date> {
return AnyIterator<Date> {
if date == nil {
date = calendar.nextDate(after: Date(), matching: components, matchingPolicy: .strict)
} else {
@ -430,19 +427,18 @@ extension Schedule {
}
return date
}
return Schedule.make({ iterator }).makeIterator()
}
return EveryDateMiddleware(schedule: schedule)
return DateMiddleware(schedule: schedule)
}
/// Creates a schedule that invokes the task every specific days in the months.
public static func every(_ mondays: MonthDay...) -> EveryDateMiddleware {
/// Creates a schedule that executes the task every specific days in the months.
public static func every(_ mondays: MonthDay...) -> DateMiddleware {
var schedule = every(mondays[0]).schedule
if mondays.count > 1 {
for i in 1..<mondays.count {
schedule = schedule.merge(Schedule.every(mondays[i]).schedule)
}
}
return EveryDateMiddleware(schedule: schedule)
return DateMiddleware(schedule: schedule)
}
}

View File

@ -53,7 +53,7 @@ public class Task {
let now = Date()
self._timeline.activate = now
self._timeline.nextSchedule = now.addingInterval(interval)
self._timeline.nextSchedule = now.adding(interval)
TaskCenter.shared.add(self, withTag: tag)
self._timer?.resume()
@ -75,7 +75,7 @@ public class Task {
return
}
_timeline.nextSchedule = _timeline.nextSchedule?.addingInterval(interval)
_timeline.nextSchedule = _timeline.nextSchedule?.adding(interval)
_timer?.schedule(after: (_timeline.nextSchedule ?? Date.distantFuture).interval(since: now))
let actions = _actions

View File

@ -26,8 +26,9 @@ public struct Time {
///
/// If any parameter is illegal, return nil.
///
/// Time(hour: 25) == nil
/// Time(hour: 1, minute: 61) == nil
/// Time(hour: 11, minute: 11) => "11:11:00.000"
/// Time(hour: 25) => nil
/// Time(hour: 1, minute: 61) => nil
public init?(hour: Int, minute: Int = 0, second: Int = 0, nanosecond: Int = 0) {
guard (0...23).contains(hour) else { return nil }
guard (0...59).contains(minute) else { return nil }
@ -40,7 +41,13 @@ public struct Time {
self.nanosecond = nanosecond
}
/// Creates a time with a timing string.
private static let cache: NSCache<NSString, DateFormatter> = {
let c = NSCache<NSString, DateFormatter>()
c.countLimit = 5
return c
}()
/// Creates a time with a string.
///
/// If parameter is illegal, return nil.
///
@ -50,31 +57,57 @@ public struct Time {
/// Time("11:12:13.123") == Time(hour: 11, minute: 12, second: 13, nanosecond: 123000000)
///
/// Time("-1.0") == nil
public init?(timing: String) {
let fields = timing.split(separator: ":")
if fields.count > 3 { return nil }
var h = 0, m = 0, s = 0, ns = 0
guard let _h = Int(fields[0]) else { return nil }
h = _h
if fields.count > 1 {
guard let _m = Int(fields[1]) else { return nil }
m = _m
}
if fields.count > 2 {
let values = fields[2].split(separator: ".")
if values.count > 2 { return nil }
guard let _s = Int(values[0]) else { return nil }
s = _s
if values.count > 1 {
guard let _ns = Int(values[1]) else { return nil }
let digits = values[1].count
ns = Int(Double(_ns) * pow(10, Double(9 - digits)))
///
/// Each of previous examples can have a period suffixes("am", "AM", "pm", "PM") separated by spaces.
public init?(_ string: String) {
var is12HourClock = false
for word in ["am", "pm", "AM", "PM"] {
if string.contains(word) {
is12HourClock = true
break
}
}
self.init(hour: h, minute: m, second: s, nanosecond: ns)
let supportedFormats = [
"HH", // 09
"HH:mm", // 09:30
"HH:mm:ss", // 09:30:26
"HH:mm:ss.SSS", // 09:30:26.123
]
for format in supportedFormats {
var fmt = format
if is12HourClock {
fmt = fmt.replacingOccurrences(of: "HH", with: "hh")
fmt += " a"
}
var formatter: DateFormatter! = Time.cache.object(forKey: fmt as NSString)
if formatter == nil {
formatter = DateFormatter()
formatter?.calendar = Calendar(identifier: .gregorian)
formatter?.locale = Locale(identifier: "en_US_POSIX")
formatter?.timeZone = TimeZone.autoupdatingCurrent
formatter.dateFormat = fmt
}
if let date = formatter.date(from: string) {
Time.cache.setObject(formatter, forKey: fmt as NSString)
let calendar = Calendar(identifier: .gregorian)
let components = calendar.dateComponents(in: TimeZone.autoupdatingCurrent, from: date)
if let hour = components.hour,
let minute = components.minute,
let second = components.second,
let nanosecond = components.nanosecond {
self.init(hour: hour, minute: minute, second: second, nanosecond: nanosecond)
return
}
}
}
return nil
}
/// Interval since zero time.
public var intervalSinceZeroTime: Interval {
return Int(hour).hours + Int(minute).minutes + Int(second).seconds + Int(nanosecond).nanoseconds
}
func asDateComponents() -> DateComponents {

View File

@ -26,6 +26,8 @@ extension WeakBox: Hashable {
}
}
/// An alternative to `NSHashTable`, since it is unavailable on linux.
struct WeakSet<T: AnyObject> {
private var set = Set<WeakBox<T>>()

View File

@ -1,7 +1,6 @@
import XCTest
import ScheduleTests
var tests = [XCTestCaseEntry]()
tests += ScheduleTests.allTests()
XCTMain(tests)
XCTMain(tests)

View File

@ -10,45 +10,88 @@ import XCTest
final class DateTimeTests: XCTestCase {
func testIntervalConvertible() {
func testInterval() {
XCTAssertTrue((-1).second.isNegative)
XCTAssertEqual(1.1.second.magnitude, 1.1 * Constants.NSEC_PER_SEC)
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.shortest(1.hour, 59.minutes, 3000.seconds), 3000.seconds)
XCTAssertEqual(1.second * 60, 1.minute)
XCTAssertEqual(59.minutes + 60.seconds, 1.hour)
XCTAssertEqual(1.week - 24.hours, 6.days)
XCTAssertEqual(1.nanoseconds, Interval(nanoseconds: 1))
XCTAssertEqual(2.microseconds, Interval(nanoseconds: 2 * K.ns_per_us))
XCTAssertEqual(3.milliseconds, Interval(nanoseconds: 3 * K.ns_per_ms))
XCTAssertEqual(4.seconds, Interval(nanoseconds: 4 * K.ns_per_s))
XCTAssertEqual(5.1.minutes, Interval(nanoseconds: 5.1 * K.ns_per_m))
XCTAssertEqual(6.2.hours, Interval(nanoseconds: 6.2 * K.ns_per_h))
XCTAssertEqual(7.3.days, Interval(nanoseconds: 7.3 * K.ns_per_d))
XCTAssertEqual(8.4.weeks, Interval(nanoseconds: 8.4 * K.ns_per_w))
XCTAssertEqual(2.microseconds, Interval(nanoseconds: 2 * Constants.NSEC_PER_USEC))
XCTAssertEqual(3.milliseconds, Interval(nanoseconds: 3 * Constants.NSEC_PER_MSEC))
XCTAssertEqual(4.seconds, Interval(nanoseconds: 4 * Constants.NSEC_PER_SEC))
XCTAssertEqual(5.1.minutes, Interval(nanoseconds: 5.1 * Constants.NSEC_PER_M))
XCTAssertEqual(6.2.hours, Interval(nanoseconds: 6.2 * Constants.NSEC_PER_H))
XCTAssertEqual(7.3.days, Interval(nanoseconds: 7.3 * Constants.NSEC_PER_D))
XCTAssertEqual(8.4.weeks, Interval(nanoseconds: 8.4 * Constants.NSEC_PER_W))
}
func testTimeConstructor() {
func testTime() {
let t0 = Time(hour: -1, minute: -2, second: -3, nanosecond: -4)
XCTAssertNil(t0)
let t1 = Time(timing: "11:12:13.456")
let t1 = Time("11:12:13.456")
XCTAssertNotNil(t1)
XCTAssertEqual(t1?.hour, 11)
XCTAssertEqual(t1?.minute, 12)
XCTAssertEqual(t1?.second, 13)
XCTAssertEqual(t1?.nanosecond, Int(0.456 * K.ns_per_s))
if let i = t1?.nanosecond.nanoseconds {
XCTAssertTrue(i.isAlmostEqual(to: (0.456 * Constants.NSEC_PER_SEC).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)
}
func testPeriodAnd() {
let dateComponents: Period = 1.year + 2.months + 3.days
XCTAssertEqual(dateComponents.years, 1)
XCTAssertEqual(dateComponents.months, 2)
XCTAssertEqual(dateComponents.days, 3)
func testPeriod() {
let p0 = (1.year + 2.months + 3.days).tidied(to: .day)
XCTAssertEqual(p0.years, 1)
XCTAssertEqual(p0.months, 2)
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")
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)
let date = Date(year: 1989, month: 6, day: 4) + 1.year
let year = Calendar(identifier: .gregorian).component(.year, from: date)
XCTAssertEqual(year, 1990)
let p4 = Period(hours: 25).tidied(to: .day)
XCTAssertEqual(p4.days, 1)
}
func testMonthDay() {
func testMonthday() {
XCTAssertEqual(MonthDay.april(3).asDateComponents(), DateComponents(month: 4, day: 3))
}
static var allTests = [
("testInterval", testIntervalConvertible),
("testTimeConstructor", testTimeConstructor),
("testPeriodAnd", testPeriodAnd),
("testMonthDay", testMonthDay)
("testInterval", testInterval),
("testTime", testTime),
("testPeriod", testPeriod),
("testMonthday", testMonthday)
]
}

View File

@ -0,0 +1,78 @@
//
// Misc.swift
// Schedule
//
// Created by Quentin Jin on 2018/7/2.
//
import Foundation
@testable import Schedule
extension Date {
var dateComponents: DateComponents {
return Calendar(identifier: .gregorian).dateComponents(in: TimeZone.autoupdatingCurrent, from: self)
}
var localizedDescription: String {
let formatter = DateFormatter()
formatter.timeZone = TimeZone.autoupdatingCurrent
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
return formatter.string(from: self)
}
init(year: Int, month: Int, day: Int, hour: Int = 0, minute: Int = 0, second: Int = 0, nanosecond: Int = 0) {
let components = DateComponents(calendar: Calendar(identifier: .gregorian),
timeZone: TimeZone.autoupdatingCurrent,
year: year, month: month, day: day,
hour: hour, minute: minute, second: second,
nanosecond: nanosecond)
self = components.date ?? Date.distantPast
}
}
extension Interval {
func isAlmostEqual(to interval: Interval, leeway: Interval) -> Bool {
return (interval - self).magnitude <= leeway.magnitude
}
}
extension Double {
func isAlmostEqual(to double: Double, leeway: Double) -> Bool {
return (double - self).magnitude <= leeway
}
}
extension Sequence where Element == Interval {
func isAlmostEqual<S>(to sequence: S, leeway: Interval) -> Bool where S: Sequence, S.Element == Element {
var i0 = self.makeIterator()
var i1 = sequence.makeIterator()
while let l = i0.next(), let r = i1.next() {
if (l - r).magnitude > leeway.magnitude {
return false
}
}
return i0.next() == i1.next()
}
}
extension Schedule {
func isAlmostEqual(to schedule: Schedule, leeway: Interval) -> Bool {
return makeIterator().isAlmostEqual(to: schedule.makeIterator(), leeway: leeway)
}
}
struct Constants {
static let NSEC_PER_USEC = Double(Foundation.NSEC_PER_USEC)
static let NSEC_PER_MSEC = Double(Foundation.NSEC_PER_MSEC)
static let NSEC_PER_SEC = Double(Foundation.NSEC_PER_SEC)
static let NSEC_PER_M = Double(Foundation.NSEC_PER_SEC) * 60
static let NSEC_PER_H = Double(Foundation.NSEC_PER_SEC) * 60 * 60
static let NSEC_PER_D = Double(Foundation.NSEC_PER_SEC) * 60 * 60 * 24
static let NSEC_PER_W = Double(Foundation.NSEC_PER_SEC) * 60 * 60 * 24 * 7
}

View File

@ -1,14 +1,16 @@
import XCTest
@testable import Schedule
final class ScheduleTests: XCTestCase {
final class SchedulesTests: XCTestCase {
func testMakeSchedule() {
let leeway = 0.01.seconds
func testMake() {
let intervals = [1.second, 2.hours, 3.days, 4.weeks]
let s0 = Schedule.of(intervals[0], intervals[1], intervals[2], intervals[3])
XCTAssert(s0.makeIterator().isEqual(to: intervals, leeway: 0.001.seconds))
XCTAssert(s0.makeIterator().isAlmostEqual(to: intervals, leeway: leeway))
let s1 = Schedule.from(intervals)
XCTAssert(s1.makeIterator().isEqual(to: intervals, leeway: 0.001.seconds))
XCTAssert(s1.makeIterator().isAlmostEqual(to: intervals, leeway: leeway))
let d0 = Date() + intervals[0]
let d1 = d0 + intervals[1]
@ -17,18 +19,18 @@ final class ScheduleTests: XCTestCase {
let s2 = Schedule.of(d0, d1, d2, d3)
let s3 = Schedule.from([d0, d1, d2, d3])
XCTAssert(s2.makeIterator().isEqual(to: intervals, leeway: 0.001.seconds))
XCTAssert(s3.makeIterator().isEqual(to: intervals, leeway: 0.001.seconds))
XCTAssert(s2.makeIterator().isAlmostEqual(to: intervals, leeway: leeway))
XCTAssert(s3.makeIterator().isAlmostEqual(to: intervals, leeway: leeway))
}
func testDates() {
let iterator = Schedule.of(1.days, 2.weeks).dates.makeIterator()
var next = iterator.next()
XCTAssertNotNil(next)
XCTAssert(next!.intervalSinceNow.isEqual(to: 1.days, leeway: 0.001.seconds))
XCTAssert(next!.intervalSinceNow.isAlmostEqual(to: 1.days, leeway: leeway))
next = iterator.next()
XCTAssertNotNil(next)
XCTAssert(next!.intervalSinceNow.isEqual(to: 2.weeks + 1.days, leeway: 0.001.seconds))
XCTAssert(next!.intervalSinceNow.isAlmostEqual(to: 2.weeks + 1.days, leeway: leeway))
}
func testNever() {
@ -40,19 +42,27 @@ final class ScheduleTests: XCTestCase {
let s1: [Interval] = [4.days, 5.weeks]
let s3 = Schedule.from(s0).concat(Schedule.from(s1))
let s4 = Schedule.from(s0 + s1)
XCTAssert(s3.makeIterator().isEqual(to: s4.makeIterator(), leeway: 0.001.seconds))
XCTAssert(s3.isAlmostEqual(to: s4, leeway: leeway))
}
func testMerge() {
let intervals0: [Interval] = [1.second, 2.minutes, 1.hour]
let intervals1: [Interval] = [2.seconds, 1.minutes, 1.seconds]
let scheudle0 = Schedule.from(intervals0).merge(Schedule.from(intervals1))
let scheudle1 = Schedule.of(1.second, 1.second, 1.minutes, 1.seconds, 58.seconds, 1.hour)
XCTAssert(scheudle0.isAlmostEqual(to: scheudle1, leeway: leeway))
}
func testAt() {
let s = Schedule.at(Date() + 1.second)
let next = s.makeIterator().next()
XCTAssertNotNil(next)
XCTAssert(next!.isEqual(to: 1.second, leeway: 0.001.seconds))
XCTAssert(next!.isAlmostEqual(to: 1.second, leeway: leeway))
}
func testCount() {
func testFirst() {
var count = 10
let s = Schedule.every(1.second).count(count)
let s = Schedule.every(1.second).first(count)
let i = s.makeIterator()
while count > 0 {
XCTAssertNotNil(i.next())
@ -70,16 +80,20 @@ final class ScheduleTests: XCTestCase {
}
}
func testMerge() {
let intervals0: [Interval] = [1.second, 2.minutes, 1.hour]
let intervals1: [Interval] = [2.seconds, 1.minutes, 1.seconds]
let scheudle0 = Schedule.from(intervals0).merge(Schedule.from(intervals1))
let scheudle1 = Schedule.of(1.second, 1.second, 1.minutes, 1.seconds, 58.seconds, 1.hour)
XCTAssert(scheudle0.makeIterator().isEqual(to: scheudle1.makeIterator(), leeway: 0.001.seconds))
func testNow() {
let s0 = Schedule.now
let s1 = Schedule.of(Date())
XCTAssertTrue(s0.isAlmostEqual(to: s1, leeway: leeway))
}
func testAfterAndRepeating() {
let s0 = Schedule.after(1.day, repeating: 1.hour).first(3)
let s1 = Schedule.of(1.day, 1.hour, 1.hour)
XCTAssertTrue(s0.isAlmostEqual(to: s1, leeway: leeway))
}
func testEveryPeriod() {
let s = Schedule.every(1.year).count(10)
let s = Schedule.every(1.year).first(10)
var date = Date()
for i in s.dates {
XCTAssertEqual(i.dateComponents.year!, date.dateComponents.year! + 1)
@ -90,7 +104,7 @@ final class ScheduleTests: XCTestCase {
}
func testEveryWeekday() {
let s = Schedule.every(.friday).at("11:11:00").count(5)
let s = Schedule.every(.friday).at("11:11:00").first(5)
for i in s.dates {
XCTAssertEqual(i.dateComponents.weekday, 6)
XCTAssertEqual(i.dateComponents.hour, 11)
@ -98,7 +112,7 @@ final class ScheduleTests: XCTestCase {
}
func testEveryMonthday() {
let s = Schedule.every(.april(2)).at(11, 11).count(5)
let s = Schedule.every(.april(2)).at(11, 11).first(5)
for i in s.dates {
XCTAssertEqual(i.dateComponents.month, 4)
XCTAssertEqual(i.dateComponents.day, 2)
@ -107,14 +121,16 @@ final class ScheduleTests: XCTestCase {
}
static var allTests = [
("testMakeSchedule", testMakeSchedule),
("testMake", testMake),
("testDates", testDates),
("testNever", testNever),
("testConcat", testConcat),
("testAt", testAt),
("testCount", testCount),
("testUntil", testUntil),
("testMerge", testMerge),
("testAt", testAt),
("testFirst", testFirst),
("testUntil", testUntil),
("testNow", testNow),
("testAfterAndRepeating", testAfterAndRepeating),
("testEveryPeriod", testEveryPeriod),
("testEveryWeekday", testEveryWeekday),
("testEveryMonthday", testEveryMonthday)

View File

@ -1,62 +0,0 @@
//
// Util.swift
// Schedule
//
// Created by Quentin Jin on 2018/7/2.
//
import Foundation
@testable import Schedule
extension Date {
var dateComponents: DateComponents {
return Calendar.current.dateComponents(in: TimeZone.current, from: self)
}
var localizedDescription: String {
let formatter = DateFormatter()
formatter.timeZone = TimeZone.autoupdatingCurrent
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
return formatter.string(from: self)
}
init(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) {
let components = DateComponents(year: year, month: month, day: day, hour: hour, minute: minute, second: second, nanosecond: nanosecond)
let date = Calendar.current.date(from: components) ?? Date.distantPast
self.init(timeIntervalSinceNow: date.timeIntervalSinceNow)
}
}
extension Sequence where Element == Interval {
func isEqual<S>(to sequence: S, leeway: Interval) -> Bool where S: Sequence, S.Element == Element {
var i0 = self.makeIterator()
var i1 = sequence.makeIterator()
while let l = i0.next(), let r = i1.next() {
let diff = Interval.longest(l, r) - Interval.shortest(l, r)
if diff.isShorter(than: leeway) { continue }
return false
}
return i0.next() == i1.next()
}
}
extension Interval {
func isEqual(to interval: Interval, leeway: Interval) -> Bool {
let diff = Interval.longest(self, interval) - Interval.shortest(self, interval)
return diff.isShorter(than: leeway)
}
}
struct K {
static let ns_per_us = Double(NSEC_PER_USEC)
static let ns_per_ms = Double(NSEC_PER_MSEC)
static let ns_per_s = Double(NSEC_PER_SEC)
static let ns_per_m = Double(NSEC_PER_SEC) * 60
static let ns_per_h = Double(NSEC_PER_SEC) * 60 * 60
static let ns_per_d = Double(NSEC_PER_SEC) * 60 * 60 * 24
static let ns_per_w = Double(NSEC_PER_SEC) * 60 * 60 * 24 * 7
}

View File

@ -3,7 +3,12 @@ import XCTest
#if !os(macOS)
public func allTests() -> [XCTestCaseEntry] {
return [
testCase(ScheduleTests.allTests),
testCase(DateTimeTests.allTests),
testCase(SchedulesTests.allTests),
testCase(TaskCenterTests.allTests),
testCase(ExtensionsTests.allTests),
testCase(BucketTests.allTests),
testCase(WeakSetTests.allTests),
]
}
#endif