redesign task's api

This commit is contained in:
Quentin Jin 2019-04-06 00:30:13 +08:00
parent a88a95ce13
commit a3fdd633a6
29 changed files with 671 additions and 803 deletions

View File

@ -7,3 +7,4 @@ disabled_rules:
- file_length
- function_body_length
- identifier_name
- type_name

View File

@ -37,7 +37,6 @@
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_62 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_22 /* Timeline.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 */; };
@ -86,7 +85,6 @@
OBJ_19 /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = "<group>"; };
OBJ_20 /* TaskCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskCenter.swift; sourceTree = "<group>"; };
OBJ_21 /* Time.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Time.swift; sourceTree = "<group>"; };
OBJ_22 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = "<group>"; };
OBJ_23 /* Weekday.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weekday.swift; sourceTree = "<group>"; };
OBJ_26 /* AtomicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicTests.swift; sourceTree = "<group>"; };
OBJ_27 /* BagTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BagTests.swift; sourceTree = "<group>"; };
@ -205,7 +203,6 @@
OBJ_19 /* Task.swift */,
OBJ_20 /* TaskCenter.swift */,
OBJ_21 /* Time.swift */,
OBJ_22 /* Timeline.swift */,
OBJ_23 /* Weekday.swift */,
);
name = Schedule;
@ -309,7 +306,6 @@
OBJ_59 /* Task.swift in Sources */,
OBJ_60 /* TaskCenter.swift in Sources */,
OBJ_61 /* Time.swift in Sources */,
OBJ_62 /* Timeline.swift in Sources */,
OBJ_63 /* Weekday.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -28,9 +28,7 @@ struct BagKeyGenerator: Sequence, IteratorProtocol {
/// 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
}
if k.i == UInt64.max { return nil }
defer { k = BagKey(underlying: k.i + 1) }
return k
}

View File

@ -331,7 +331,7 @@ extension Date {
extension DispatchSourceTimer {
/// Schedule this timer later.
/// Schedule this timer after the given interval.
func schedule(after timeout: Interval) {
if timeout.isNegative { return }
let ns = timeout.nanoseconds.clampedToInt()

View File

@ -29,7 +29,7 @@ public enum Monthday {
/// Returns a dateComponenets of this monthday, using gregorian calender and
/// current time zone.
public func asDateComponents() -> DateComponents {
public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents {
var month, day: Int
switch self {
case .january(let n): month = 1; day = n
@ -47,7 +47,7 @@ public enum Monthday {
}
return DateComponents(
calendar: Calendar.gregorian,
timeZone: TimeZone.current,
timeZone: timeZone,
month: month,
day: day)
}
@ -56,8 +56,8 @@ public enum Monthday {
extension Date {
/// Returns a Boolean value indicating whether this date is the monthday in current time zone..
public func `is`(_ monthday: Monthday) -> Bool {
let components = monthday.asDateComponents()
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)

View File

@ -1,6 +1,6 @@
import Foundation
/// Type used to represents a date-based amount of time in the ISO-8601 calendar system,
/// 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`:
@ -65,11 +65,19 @@ public struct Period {
for (word, number) in Period.quantifiers.read({ $0 }) {
str = str.replacingOccurrences(of: word, with: "\(number)")
}
// swiftlint:disable force_try
let regexp = try! NSRegularExpression(pattern: "( and |, )")
str = regexp.stringByReplacingMatches(in: str, range: NSRange(location: 0, length: str.count), withTemplate: "$")
let mark: Character = ""
str = regexp.stringByReplacingMatches(
in: str,
range: NSRange(location: 0, length: str.count),
withTemplate: String(mark)
)
var period = 0.year
for pair in str.split(separator: "$").map({ $0.split(separator: " ") }) {
for pair in str.split(separator: mark).map({ $0.split(separator: " ") }) {
guard
pair.count == 2,
let number = Int(pair[0])
@ -171,10 +179,18 @@ public struct Period {
/// Returns a dateComponenets of this period, using gregorian calender and
/// current time zone.
public func asDateComponents() -> 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
)
}
}

View File

@ -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<Interval>
private var seq: AnySequence<Interval>
private init<S>(_ sequence: S) where S: Sequence, S.Element == Interval {
iSeq = AnySequence(sequence)
seq = AnySequence(sequence)
}
func makeIterator() -> AnyIterator<Interval> {
return iSeq.makeIterator()
/// Returns an iterator over the interval of this sequence.
public func makeIterator() -> AnyIterator<Interval> {
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 block should be dispatched.
/// - block: 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,
block: @escaping (Task) -> Void
) -> Task {
return Task(plan: self, queue: queue, block: block)
}
/// 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 block should be dispatched.
/// - block: 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,
block: @escaping () -> Void
) -> Task {
return self.do(queue: queue, block: { (_) in block() })
}
}
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"
@ -71,15 +75,15 @@ extension Plan {
}
/// 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<S>(_ intervals: S) -> Plan where S: Sequence, S.Element == Interval {
return Plan(intervals)
}
@ -87,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:
///
@ -99,42 +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.
/// You should not return `Date()` in making iterator.
/// If you want to execute a task immediately, use `Plan.now`.
public static func make<I>(
_ makeUnderlyingIterator: @escaping () -> I
) -> Plan where I: IteratorProtocol, I.Element == Date {
return Plan.make { () -> AnyIterator<Interval> 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<S>(_ sequence: S) -> Plan where S: Sequence, S.Element == Date {
return Plan.make(sequence.makeIterator)
}
@ -143,13 +148,13 @@ extension Plan {
public var dates: AnySequence<Date> {
return AnySequence { () -> AnyIterator<Date> 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
}
}
}
@ -157,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<Date> { nil }
AnyIterator<Interval> { 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:
///
@ -198,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<Date> 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<Date> {
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:
///
@ -251,7 +260,7 @@ 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<Date> in
let iterator = self.dates.makeIterator()
@ -263,58 +272,57 @@ extension Plan {
}
}
}
}
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.
/// 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 given date.
public static func at(_ date: Date) -> Plan {
return Plan.of(date)
}
/// 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 after delay then repeat
/// every interval.
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.
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 period.
public static func every(_ period: Period) -> Plan {
return Plan.make { () -> AnyIterator<Interval> in
let calendar = Calendar.gregorian
var last: Date!
var prev: Date!
return AnyIterator {
last = last ?? Date()
guard let next = calendar.date(byAdding: period.asDateComponents(),
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
@ -326,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<Interval> in
@ -350,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<Date> in
let calendar = Calendar.gregorian
@ -419,14 +425,12 @@ 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) }
@ -439,7 +443,7 @@ extension Plan {
return DateMiddleware(plan: plan)
}
/// Creates a plan that executes the task every specific day in the month.
/// Creates a date middleware that executes the task on every specific month day.
public static func every(_ monthday: Monthday) -> DateMiddleware {
let plan = Plan.make { () -> AnyIterator<Date> in
let calendar = Calendar.gregorian
@ -459,14 +463,12 @@ 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) }
@ -484,11 +486,12 @@ extension Plan {
/// Returns a Boolean value indicating whether this plan is empty.
public func isNever() -> Bool {
return 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.
///
@ -497,23 +500,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<Interval> 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 })
}
}

View File

@ -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 block should be added.
/// - block: 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,
block: @escaping (Task) -> Void
) -> Task {
return RunLoopTask(plan: self, mode: mode, block: block)
}
/// 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 block should be added.
/// - block: 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,
block: @escaping () -> Void
) -> Task {
return self.do(mode: mode) { _ in
block()
}
}
}
@ -45,24 +49,26 @@ private final class RunLoopTask: Task {
var timer: Timer!
init(plan: Plan, mode: RunLoop.Mode, onElapse: @escaping (Task) -> Void) {
init(
plan: Plan,
mode: RunLoop.Mode,
block: @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()
}
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)
timer = Timer(
fire: Date.distantFuture,
interval: .greatestFiniteMagnitude,
repeats: false
) { [weak self] _ in
guard let self = self else { return }
block(self)
}
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()
}
this = self
}
deinit {

View File

@ -13,7 +13,7 @@ extension BagKey {
}
}
/// `Task` represents a timed task.
/// `Task` represents a timing task.
open class Task {
/// The unique id of this task.
@ -21,50 +21,58 @@ open class Task {
public typealias Action = (Task) -> Void
private let _mutex = NSRecursiveLock()
private let _lock = NSRecursiveLock()
private var _iterator: AnyIterator<Interval>
private var _timer: DispatchSourceTimer
private lazy var _onElapseActions = Bag<Action>()
private lazy var _actions = Bag<Action>()
private lazy var _suspensions: UInt64 = 0
private lazy var _timeline = Timeline()
private lazy var _suspensionCount: UInt64 = 0
private lazy var _executionCount: Int = 0
private lazy var _countOfExecutions: Int = 0
private var _firstExecutionDate: Date?
private var _lastExecutionDate: Date?
private var _estimatedNextExecutionDate: Date?
private lazy var _lifetime: Interval = Int.max.seconds
private lazy var _lifetimeTimer: DispatchSourceTimer = {
let timer = DispatchSource.makeTimerSource()
timer.setEventHandler { [weak self] in
self?.cancel()
/// The date of creation.
public let creationDate = Date()
/// The date of first execution.
open var firstExecutionDate: Date? {
return _lock.withLock { _firstExecutionDate }
}
/// The date of last execution.
open var lastExecutionDate: Date? {
return _lock.withLock { _lastExecutionDate }
}
/// The date of estimated next execution.
open var estimatedNextExecutionDate: Date? {
return _lock.withLock { _estimatedNextExecutionDate }
}
timer.schedule(after: _lifetime)
timer.resume()
return timer
}()
private weak var _taskCenter: TaskCenter?
/// The task center which this task currently in.
/// The task center to which this task currently belongs.
open var taskCenter: TaskCenter? {
return _taskCenter
return _lock.withLock { _taskCenter }
}
/// The mutex used to guard task center operations.
private let _taskCenterLock = NSRecursiveLock()
/// Adds this task to the given task center.
///
/// If this task is already in a task center, it will be removed from that center first.
func addToTaskCenter(_ center: TaskCenter) {
_taskCenterLock.lock()
defer { _taskCenterLock.unlock() }
if _taskCenter === center { return }
_taskCenter?.remove(self)
let c = _taskCenter
_taskCenter = center
c?.remove(self)
}
/// Removes this task from the given task center.
@ -74,24 +82,25 @@ open class Task {
if _taskCenter !== center { return }
_taskCenter?.remove(self)
_taskCenter = nil
center.remove(self)
}
/// Initializes a normal task with specified plan and dispatch queue.
/// Initializes a timing task.
///
/// - Parameters:
/// - plan: The plan.
/// - queue: The dispatch queue to which all actions should be added.
/// - onElapse: The action to do when time is out.
init(plan: Plan,
/// - queue: The dispatch queue to which the block should be dispatched.
/// - block: A block to be executed when time is up.
init(
plan: Plan,
queue: DispatchQueue?,
onElapse: @escaping (Task) -> Void) {
block: @escaping (Task) -> Void
) {
_iterator = plan.makeIterator()
_timer = DispatchSource.makeTimerSource(queue: queue)
_onElapseActions.append(onElapse)
_actions.append(block)
_timer.setEventHandler { [weak self] in
guard let self = self else { return }
@ -100,17 +109,18 @@ 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 {
while _suspensions > 0 {
while _suspensionCount > 0 {
_timer.resume()
_suspensions -= 1
_suspensionCount -= 1
}
cancel()
@ -118,226 +128,144 @@ open class Task {
taskCenter?.remove(self)
}
private func scheduleNext() {
_mutex.withLockVoid {
private func elapse() {
scheduleNextExecution()
execute()
}
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 { () -> Bag<Task.Action> in
/// Execute this task now, without interrupting its plan.
open func execute() {
let actions = _lock.withLock { () -> Bag<Task.Action> in
let now = Date()
if _timeline.firstExecution == nil {
_timeline.firstExecution = now
if _firstExecutionDate == nil {
_firstExecutionDate = now
}
_timeline.lastExecution = now
_countOfExecutions += 1
return _onElapseActions
_lastExecutionDate = now
_executionCount += 1
return _actions
}
actions.forEach { $0(self) }
}
private func elapse() {
scheduleNext()
execute()
}
/// Host this task to an object, that is, when the object deallocates, this task will be cancelled.
#if canImport(ObjectiveC)
open func host(on target: AnyObject) {
open func host(to 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
/// The number of task executions.
public var executionCount: Int {
return _lock.withLock {
_executionCount
}
}
/// A Boolean indicating whether the task was canceled.
public var isCancelled: Bool {
return _mutex.withLock {
return _lock.withLock {
_timer.isCancelled
}
}
// MARK: - Manage
/// Reschedules this task with the new plan.
public func reschedule(_ new: Plan) {
_mutex.withLockVoid {
_lock.withLockVoid {
_iterator = new.makeIterator()
}
scheduleNext()
scheduleNextExecution()
}
/// Suspensions of this task.
public var suspensions: UInt64 {
return _mutex.withLock {
_suspensions
/// The number of task suspensions.
public var suspensionCount: UInt64 {
return _lock.withLock {
_suspensionCount
}
}
/// Suspends this task.
public func suspend() {
_mutex.withLockVoid {
if _suspensions < UInt64.max {
_lock.withLockVoid {
if _suspensionCount < UInt64.max {
_timer.suspend()
_suspensions += 1
_suspensionCount += 1
}
}
}
/// Resumes this task.
public func resume() {
_mutex.withLockVoid {
if _suspensions > 0 {
_lock.withLockVoid {
if _suspensionCount > 0 {
_timer.resume()
_suspensions -= 1
_suspensionCount -= 1
}
}
}
/// Cancels this task.
public func cancel() {
_mutex.withLockVoid {
_lock.withLockVoid {
_timer.cancel()
}
TaskCenter.default.remove(self)
}
// 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 {
if restOfLifetime.isNegative {
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
if rest.isNegative { return false }
rest += interval
if rest.isNegative { return false }
_mutex.withLockVoid {
_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.negated)
}
// MARK: - Action
/// The number of actions in this task.
public var countOfActions: Int {
return _mutex.withLock {
_onElapseActions.count
return _lock.withLock {
_actions.count
}
}
/// 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.withLockVoid {
_ = _onElapseActions.removeValue(for: key.bagKey)
_lock.withLockVoid {
_ = _actions.removeValue(for: key.bagKey)
}
}
/// Removes all actions from this task.
public func removeAllActions() {
_mutex.withLockVoid {
_onElapseActions.removeAll()
_lock.withLockVoid {
_actions.removeAll()
}
}
// MARK: - Tag
open func add(to: TaskCenter) {
_mutex.lock()
}
}
extension Task: Hashable {
/// Hashes the essential components of this value by feeding them into the given hasher.
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
@ -347,25 +275,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), " +
"\"countOfElapseActions\": \(_onElapseActions.count), " +
"\"countOfExecutions\": \(_countOfExecutions), " +
"\"lifeTime\": \(_lifetime), " +
"\"timeline\": \(_timeline)" +
" }"
}
}
extension Task: CustomDebugStringConvertible {
/// A textual representation of this task for debugging.
public var debugDescription: String {
return description
}
}

View File

@ -30,8 +30,8 @@ open class TaskCenter {
private let lock = NSLock()
private var tasksOfTag: [String: Set<TaskBox>] = [:]
private var tagsOfTask: [TaskBox: Set<String>] = [:]
private var tags: [String: Set<TaskBox>] = [:]
private var tasks: [TaskBox: Set<String>] = [:]
/// Default task center.
open class var `default`: TaskCenter {
@ -39,14 +39,12 @@ open class TaskCenter {
}
/// Adds the given task to this center.
///
/// Center won't retain the task.
open func add(_ task: Task) {
task.addToTaskCenter(self)
lock.withLockVoid {
let box = TaskBox(task)
tagsOfTask[box] = []
self.tasks[box] = []
}
}
@ -56,15 +54,14 @@ open class TaskCenter {
lock.withLockVoid {
let box = TaskBox(task)
if let tags = self.tagsOfTask[box] {
if let tags = self.tasks[box] {
for tag in tags {
self.tasksOfTag[tag]?.remove(box)
if self.tasksOfTag[tag]?.count == 0 {
self.tasksOfTag[tag] = nil
self.tags[tag]?.remove(box)
if self.tags[tag]?.count == 0 {
self.tags[tag] = nil
}
}
self.tagsOfTask[box] = nil
self.tasks[box] = nil
}
}
}
@ -84,15 +81,12 @@ open class TaskCenter {
lock.withLockVoid {
let box = TaskBox(task)
if tagsOfTask[box] == nil {
tagsOfTask[box] = []
}
for tag in tags {
tagsOfTask[box]?.insert(tag)
if tasksOfTag[tag] == nil {
tasksOfTag[tag] = []
tasks[box]?.insert(tag)
if self.tags[tag] == nil {
self.tags[tag] = []
}
tasksOfTag[tag]?.insert(box)
self.tags[tag]?.insert(box)
}
}
}
@ -113,64 +107,67 @@ open class TaskCenter {
lock.withLockVoid {
let box = TaskBox(task)
for tag in tags {
tagsOfTask[box]?.remove(tag)
tasksOfTag[tag]?.remove(box)
self.tasks[box]?.remove(tag)
self.tags[tag]?.remove(box)
if self.tags[tag]?.count == 0 {
self.tags[tag] = nil
}
}
}
}
/// Returns all tags on the task.
/// Returns all tags for the task.
///
/// If the task is not in this center, return an empty array.
open func tagsForTask(_ task: Task) -> [String] {
open func tags(forTask task: Task) -> [String] {
guard task.taskCenter === self else { return [] }
return lock.withLock {
Array(tagsOfTask[TaskBox(task)] ?? [])
Array(tasks[TaskBox(task)] ?? [])
}
}
/// Returns all tasks that have the tag.
open func tasksForTag(_ tag: String) -> [Task] {
/// Returns all tasks for the tag.
open func tasks(forTag tag: String) -> [Task] {
return lock.withLock {
tasksOfTag[tag]?.compactMap { $0.task } ?? []
tags[tag]?.compactMap { $0.task } ?? []
}
}
/// Returns all tasks in this center.
open var allTasks: [Task] {
return lock.withLock {
tagsOfTask.compactMap { $0.key.task }
tasks.compactMap { $0.key.task }
}
}
/// Returns all existing tags in this center.
/// Returns all tags in this center.
open var allTags: [String] {
return lock.withLock {
tasksOfTag.map { $0.key }
tags.map { $0.key }
}
}
/// Removes all tasks from this center.
open func removeAll() {
lock.withLockVoid {
tagsOfTask = [:]
tasksOfTag = [:]
tasks = [:]
tags = [:]
}
}
/// Suspends all tasks that have the tag.
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() }
}
/// Resumes all tasks that have the tag.
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() }
}
/// Cancels all tasks that have the tag.
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() }
}
}

View File

@ -48,8 +48,15 @@ public struct Time {
public init?(_ string: String) {
let pattern = "^(\\d{1,2})(:(\\d{1,2})(:(\\d{1,2})(.(\\d{1,3}))?)?)?( (am|AM|pm|PM))?$"
// swiftlint:disable force_try
let regexp = try! NSRegularExpression(pattern: pattern, options: [])
guard let matches = regexp.matches(in: string, options: [], range: NSRange(location: 0, length: string.count)).first else { return nil }
guard let matches = regexp.matches(
in: string,
options: [],
range: NSRange(location: 0, length: string.count)).first
else {
return nil
}
var hasAM = false
var hasPM = false
@ -87,9 +94,9 @@ public struct Time {
/// Returns a dateComponenets of the time, using gregorian calender and
/// current time zone.
public func asDateComponents() -> DateComponents {
public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents {
return DateComponents(calendar: Calendar.gregorian,
timeZone: TimeZone.current,
timeZone: timeZone,
hour: hour,
minute: minute,
second: second,

View File

@ -1,49 +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 {
enum Lazy {
static let fmt = ISO8601DateFormatter()
}
let desc = { (d: Date?) -> String in
guard let d = d else { return "nil" }
return Lazy.fmt.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
}
}

View File

@ -7,10 +7,10 @@ public enum Weekday: Int {
/// Returns dateComponenets of the weekday, using gregorian calender and
/// current time zone.
public func asDateComponents() -> DateComponents {
public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents {
return DateComponents(
calendar: Calendar.gregorian,
timeZone: TimeZone.current,
timeZone: timeZone,
weekday: rawValue)
}
}
@ -18,8 +18,10 @@ public enum Weekday: Int {
extension Date {
/// Returns a Boolean value indicating whether this date is the weekday in current time zone.
public func `is`(_ weekday: Weekday) -> Bool {
return Calendar.gregorian.component(.weekday, from: self) == weekday.rawValue
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
}
}

View File

@ -39,6 +39,6 @@ final class AtomicTests: XCTestCase {
("testRead", testRead),
("testReadVoid", testReadVoid),
("testWrite", testWrite),
("testWriteVoid", testWriteVoid),
("testWriteVoid", testWriteVoid)
]
}

View File

@ -18,14 +18,14 @@ final class ExtensionsTests: XCTestCase {
func testStartOfToday() {
let components = Date().startOfToday.dateComponents
guard
let h = components.hour,
let m = components.minute,
let h = components.hour
let m = components.minute
let s = components.second
else {
XCTFail()
return
}
XCTAssertNotNil(h)
XCTAssertNotNil(m)
XCTAssertNotNil(s)
XCTAssertEqual(h, 0)
XCTAssertEqual(m, 0)
XCTAssertEqual(s, 0)

View File

@ -64,7 +64,7 @@ extension Plan {
extension DispatchQueue {
func async(after interval: Interval, execute body: @escaping () -> Void) {
asyncAfter(deadline: .now() + interval.asSeconds(), execute: body)
asyncAfter(wallDeadline: .now() + interval.asSeconds(), execute: body)
}
static func `is`(_ queue: DispatchQueue) -> Bool {
@ -76,3 +76,8 @@ extension DispatchQueue {
return DispatchQueue.getSpecific(key: key) != nil
}
}
extension TimeZone {
static let shanghai = TimeZone(identifier: "Asia/Shanghai")!
}

View File

@ -34,6 +34,7 @@ final class IntervalTests: XCTestCase {
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)
@ -105,6 +106,6 @@ final class IntervalTests: XCTestCase {
("testAdding", testAdding),
("testOperators", testOperators),
("testAs", testAs),
("testDate", testDate),
("testDate", testDate)
]
}

View File

@ -6,7 +6,7 @@ final class MonthdayTests: XCTestCase {
func testIs() {
// ! Be careful the time zone problem.
let d = Date(year: 2019, month: 1, day: 1)
XCTAssertTrue(d.is(.january(1)))
XCTAssertTrue(d.is(.january(1), in: TimeZone.shanghai))
}
func testAsDateComponents() {

View File

@ -29,6 +29,9 @@ final class PeriodTests: XCTestCase {
Period.registerQuantifier("many", for: 100 * 1000)
let p4 = Period("many days")
XCTAssertEqual(p4!.days, 100 * 1000)
let p5 = Period("hi, 😈")
XCTAssertNil(p5)
}
func testAdd() {

View File

@ -3,34 +3,47 @@ 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))
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() {
@ -38,32 +51,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.of(s0).concat(Plan.of(s1))
let s4 = Plan.of(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.of(intervals0).merge(Plan.of(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
@ -73,29 +79,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!)
@ -104,121 +117,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())
func testOffset() {
let p1 = Plan.after(1.second).first(100)
let p2 = p1.offset(by: 1.second).first(100)
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())
for (d1, d2) in zip(p1.dates, p2.dates) {
XCTAssertTrue(d2.interval(since: d1).isAlmostEqual(to: 1.second, leeway: leeway))
}
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))
}
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)
]
}

View File

@ -19,50 +19,58 @@ final class TaskCenterTests: XCTestCase {
}
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.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.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.removeAll()
@ -70,18 +78,17 @@ final class TaskCenterTests: XCTestCase {
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.removeAll()
@ -89,7 +96,9 @@ final class TaskCenterTests: XCTestCase {
func testWeak() {
let block = {
_ = self.makeTask()
let task = self.makeTask()
XCTAssertEqual(self.center.allTasks.count, 1)
_ = task
}
block()

View File

@ -3,45 +3,83 @@ 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))
}
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,18 +87,68 @@ final class TaskTests: XCTestCase {
_ = task
RunLoop.current.run()
}
waitForExpectations(timeout: 0.5)
waitForExpectations(timeout: 0.1)
}
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)
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 testCancel() {
let task = Plan.never.do { }
XCTAssertFalse(task.isCancelled)
task.cancel()
XCTAssertTrue(task.isCancelled)
}
func testExecuteNow() {
let e = expectation(description: "testExecuteNow")
let task = Plan.never.do {
e.fulfill()
}
task.execute()
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, block: {
XCTFail("should never come here")
})
task.host(to: obj)
}
fn()
DispatchQueue.main.async(after: 0.2.seconds) {
e.fulfill()
}
waitForExpectations(timeout: 1)
}
func testReschedule() {
let e = expectation(description: "testReschedule")
var i = 0
let task = Plan.after(0.01.second).do(queue: .global()) { (task) in
i += 1
if task.executionCount == 3 && task.estimatedNextExecutionDate == nil {
e.fulfill()
}
if task.executionCount > 3 {
XCTFail("should never come here")
}
}
DispatchQueue.global().async(after: 0.02.second) {
task.reschedule(Plan.every(0.01.second).first(2))
}
waitForExpectations(timeout: 1)
}
func testAddAndRemoveActions() {
@ -83,69 +171,17 @@ final class TaskTests: XCTestCase {
XCTAssertEqual(task.countOfActions, 0)
}
func testReschedule() {
let e = expectation(description: "testReschedule")
var i = 0
let task = Plan.after(0.1.second).do { (task) in
i += 1
if task.countOfExecutions == 6 && task.timeline.estimatedNextExecution == nil {
e.fulfill()
}
if task.countOfExecutions > 6 {
XCTFail("should never come here")
}
}
DispatchQueue.global().async(after: 0.5.second) {
task.reschedule(Plan.every(0.1.second).first(5))
}
waitForExpectations(timeout: 2)
task.cancel()
}
func testHost() {
let e = expectation(description: "testHost")
let fn = {
let obj = NSObject()
let task = Plan.after(0.1.second).do(queue: .main, onElapse: {
XCTFail("should never come here")
})
task.host(on: obj)
}
fn()
DispatchQueue.main.async(after: 0.2.seconds) {
e.fulfill()
}
waitForExpectations(timeout: 1)
}
func testLifetime() {
let e = expectation(description: "testLifetime")
let task = Plan.after(1.hour).do { }
task.setLifetime(1.second)
XCTAssertEqual(task.lifetime, 1.second)
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)
e.fulfill()
}
waitForExpectations(timeout: 3)
}
static var allTests = [
("testAfter", testAfter),
("testRepeat", testRepeat),
("testTaskCenter", testTaskCenter),
("testDispatchQueue", testDispatchQueue),
("testThread", testThread),
("testAddAndRemoveActions", testAddAndRemoveActions),
("testReschedule", testReschedule),
("testSuspendResume", testSuspendResume),
("testCancel", testCancel),
("testExecuteNow", testExecuteNow),
("testHost", testHost),
("testLifetime", testLifetime)
("testReschedule", testReschedule),
("testAddAndRemoveActions", testAddAndRemoveActions)
]
}

View File

@ -6,7 +6,7 @@ final class WeekdayTests: XCTestCase {
func testIs() {
// ! Be careful the time zone problem.
let d = Date(year: 2019, month: 1, day: 1)
XCTAssertTrue(d.is(.tuesday))
XCTAssertTrue(d.is(.tuesday, in: TimeZone.shanghai))
}
func testAsDateComponents() {