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 - file_length
- function_body_length - function_body_length
- identifier_name - identifier_name
- type_name

View File

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

View File

@ -17,7 +17,7 @@ final class Atomic<T> {
func read<U>(_ body: (T) -> U) -> U { func read<U>(_ body: (T) -> U) -> U {
return lock.withLock { body(val) } return lock.withLock { body(val) }
} }
/// Reads the current value atomically. /// Reads the current value atomically.
@inline(__always) @inline(__always)
func readVoid(_ body: (T) -> Void) { func readVoid(_ body: (T) -> Void) {
@ -29,7 +29,7 @@ final class Atomic<T> {
func write<U>(_ body: (inout T) -> U) -> U { func write<U>(_ body: (inout T) -> U) -> U {
return lock.withLock { body(&val) } return lock.withLock { body(&val) }
} }
/// Writes the current value atomically. /// Writes the current value atomically.
@inline(__always) @inline(__always)
func writeVoid(_ body: (inout T) -> Void) { func writeVoid(_ body: (inout T) -> Void) {

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. /// Advances to the next key and returns it, or nil if no next key exists.
mutating func next() -> BagKey? { mutating func next() -> BagKey? {
if k.i == UInt64.max { if k.i == UInt64.max { return nil }
return nil
}
defer { k = BagKey(underlying: k.i + 1) } defer { k = BagKey(underlying: k.i + 1) }
return k return k
} }

View File

@ -10,9 +10,9 @@ import Foundation
/// ///
/// observer.cancel() /// observer.cancel()
class DeinitObserver { class DeinitObserver {
private var associateKey: Void = () private var associateKey: Void = ()
private(set) weak var observed: AnyObject? private(set) weak var observed: AnyObject?
private var block: (() -> Void)? private var block: (() -> Void)?

View File

@ -20,7 +20,7 @@ extension Int {
} }
extension Locale { extension Locale {
static let posix = Locale(identifier: "en_US_POSIX") static let posix = Locale(identifier: "en_US_POSIX")
} }

View File

@ -13,7 +13,7 @@ public struct Interval {
} }
extension Interval: Hashable { extension Interval: Hashable {
/// Returns a boolean value indicating whether two intervals are equal. /// Returns a boolean value indicating whether two intervals are equal.
public static func == (lhs: Interval, rhs: Interval) -> Bool { public static func == (lhs: Interval, rhs: Interval) -> Bool {
return lhs.nanoseconds == rhs.nanoseconds return lhs.nanoseconds == rhs.nanoseconds
@ -72,7 +72,7 @@ extension Interval: Comparable {
/// A positive interval is always ordered ascending to a negative interval. /// A positive interval is always ordered ascending to a negative interval.
public func compare(_ other: Interval) -> ComparisonResult { public func compare(_ other: Interval) -> ComparisonResult {
let d = nanoseconds - other.nanoseconds let d = nanoseconds - other.nanoseconds
if d < 0 { return .orderedAscending } if d < 0 { return .orderedAscending }
if d > 0 { return .orderedDescending } if d > 0 { return .orderedDescending }
return .orderedSame return .orderedSame
@ -102,7 +102,7 @@ extension Interval: Comparable {
// MARK: - Adding & Subtracting // MARK: - Adding & Subtracting
extension Interval { extension Interval {
/// Returns a new interval by multipling this interval by the given number. /// Returns a new interval by multipling this interval by the given number.
/// ///
/// 1.hour * 2 == 2.hours /// 1.hour * 2 == 2.hours
@ -331,7 +331,7 @@ extension Date {
extension DispatchSourceTimer { extension DispatchSourceTimer {
/// Schedule this timer later. /// Schedule this timer after the given interval.
func schedule(after timeout: Interval) { func schedule(after timeout: Interval) {
if timeout.isNegative { return } if timeout.isNegative { return }
let ns = timeout.nanoseconds.clampedToInt() let ns = timeout.nanoseconds.clampedToInt()

View File

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

View File

@ -1,6 +1,6 @@
import Foundation 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'. /// such as '2 years, 3 months and 4 days'.
/// ///
/// It's a little different from `Interval`: /// It's a little different from `Interval`:
@ -65,18 +65,26 @@ public struct Period {
for (word, number) in Period.quantifiers.read({ $0 }) { for (word, number) in Period.quantifiers.read({ $0 }) {
str = str.replacingOccurrences(of: word, with: "\(number)") str = str.replacingOccurrences(of: word, with: "\(number)")
} }
// swiftlint:disable force_try
let regexp = try! NSRegularExpression(pattern: "( and |, )") 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 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 guard
pair.count == 2, pair.count == 2,
let number = Int(pair[0]) let number = Int(pair[0])
else { else {
return nil return nil
} }
var unit = String(pair[1]) var unit = String(pair[1])
if unit.last == "s" { unit.removeLast() } if unit.last == "s" { unit.removeLast() }
switch unit { switch unit {
@ -141,27 +149,27 @@ public struct Period {
/// Period(hours: 25).tidied(to .day) => Period(days: 1, hours: 1) /// Period(hours: 25).tidied(to .day) => Period(days: 1, hours: 1)
public func tidied(to level: TideLevel) -> Period { public func tidied(to level: TideLevel) -> Period {
var period = self var period = self
if case .nanosecond = level { return period } if case .nanosecond = level { return period }
if period.nanoseconds.magnitude >= UInt(1.second.nanoseconds) { if period.nanoseconds.magnitude >= UInt(1.second.nanoseconds) {
period.seconds += period.nanoseconds / Int(1.second.nanoseconds) period.seconds += period.nanoseconds / Int(1.second.nanoseconds)
period.nanoseconds %= Int(1.second.nanoseconds) period.nanoseconds %= Int(1.second.nanoseconds)
} }
if case .second = level { return period } if case .second = level { return period }
if period.seconds.magnitude >= 60 { if period.seconds.magnitude >= 60 {
period.minutes += period.seconds / 60 period.minutes += period.seconds / 60
period.seconds %= 60 period.seconds %= 60
} }
if case .minute = level { return period } if case .minute = level { return period }
if period.minutes.magnitude >= 60 { if period.minutes.magnitude >= 60 {
period.hours += period.minutes / 60 period.hours += period.minutes / 60
period.minutes %= 60 period.minutes %= 60
} }
if case .hour = level { return period } if case .hour = level { return period }
if period.hours.magnitude >= 24 { if period.hours.magnitude >= 24 {
period.days += period.hours / 24 period.days += period.hours / 24
period.hours %= 24 period.hours %= 24
@ -171,10 +179,18 @@ public struct Period {
/// Returns a dateComponenets of this period, using gregorian calender and /// Returns a dateComponenets of this period, using gregorian calender and
/// current time zone. /// current time zone.
public func asDateComponents() -> DateComponents { public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents {
return DateComponents(year: years, month: months, day: days, return DateComponents(
hour: hours, minute: minutes, second: seconds, calendar: Calendar.gregorian,
nanosecond: nanoseconds) timeZone: timeZone,
year: years,
month: months,
day: days,
hour: hours,
minute: minutes,
second: seconds,
nanosecond: nanoseconds
)
} }
} }

View File

@ -1,50 +1,54 @@
import Foundation 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. /// executed.
/// ///
/// `Plan` is `Interval` based. /// `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 { private init<S>(_ sequence: S) where S: Sequence, S.Element == Interval {
iSeq = AnySequence(sequence) seq = AnySequence(sequence)
} }
func makeIterator() -> AnyIterator<Interval> { /// Returns an iterator over the interval of this sequence.
return iSeq.makeIterator() public func makeIterator() -> AnyIterator<Interval> {
return seq.makeIterator()
} }
/// Schedules a task with this plan. /// Schedules a task with this plan.
/// ///
/// - Parameters: /// - Parameters:
/// - queue: The queue to which the task will be dispatched. /// - queue: The dispatch queue to which the block should be dispatched.
/// - onElapse: The action to do when time is out. /// - block: A block to be executed when time is up.
/// - Returns: The task just created. /// - Returns: The task just created.
public func `do`(queue: DispatchQueue, public func `do`(
onElapse: @escaping (Task) -> Void) -> Task { queue: DispatchQueue,
return Task(plan: self, queue: queue, onElapse: onElapse) block: @escaping (Task) -> Void
) -> Task {
return Task(plan: self, queue: queue, block: block)
} }
/// Schedules a task with this plan. /// Schedules a task with this plan.
/// ///
/// - Parameters: /// - Parameters:
/// - queue: The queue to which the task will be dispatched. /// - queue: The dispatch queue to which the block should be dispatched.
/// - onElapse: The action to do when time is out. /// - block: A block to be executed when time is up.
/// - Returns: The task just created. /// - Returns: The task just created.
public func `do`(queue: DispatchQueue, public func `do`(
onElapse: @escaping () -> Void) -> Task { queue: DispatchQueue,
return self.do(queue: queue, onElapse: { (_) in onElapse() }) block: @escaping () -> Void
) -> Task {
return self.do(queue: queue, block: { (_) in block() })
} }
} }
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 after each interval produced by the iterator /// The task will be executed after each interval.
/// that `makeUnderlyingIterator` returns.
/// ///
/// For example: /// For example:
/// ///
@ -52,11 +56,11 @@ extension Plan {
/// var i = 0 /// var i = 0
/// return AnyIterator { /// return AnyIterator {
/// i += 1 /// i += 1
/// return i /// return i // 1, 2, 3, ...
/// } /// }
/// } /// }
/// plan.do { /// plan.do {
/// print(Date()) /// logTimestamp()
/// } /// }
/// ///
/// > "2001-01-01 00:00:00" /// > "2001-01-01 00:00:00"
@ -71,15 +75,15 @@ extension Plan {
} }
/// Creates a plan from a list of intervals. /// Creates a plan from a list of intervals.
///
/// The task will be executed after each interval in the array. /// 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 { public static func of(_ intervals: Interval...) -> Plan {
return Plan.of(intervals) return Plan.of(intervals)
} }
/// Creates a plan from a list of intervals. /// Creates a plan from a list of intervals.
///
/// The task will be executed after each interval in the array. /// 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 { public static func of<S>(_ intervals: S) -> Plan where S: Sequence, S.Element == Interval {
return Plan(intervals) return Plan(intervals)
} }
@ -87,10 +91,9 @@ extension Plan {
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 /// The task will be executed at each date.
/// produced by the iterator that `makeUnderlyingIterator` returns.
/// ///
/// For example: /// For example:
/// ///
@ -99,42 +102,44 @@ extension Plan {
/// return Date().addingTimeInterval(3) /// return Date().addingTimeInterval(3)
/// } /// }
/// } /// }
/// print("now:", Date()) ///
/// plan.do { /// plan.do {
/// print("task", Date()) /// logTimestamp()
/// } /// }
/// ///
/// > "now: 2001-01-01 00:00:00" /// > "2001-01-01 00:00:00"
/// > "task: 2001-01-01 00:00:03" /// > "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. /// You should not return `Date()` in making iterator.
/// If you want to execute a task immediately, /// If you want to execute a task immediately, use `Plan.now`.
/// use `Plan.now` then `concat` another plan instead.
public static func make<I>( public static func make<I>(
_ makeUnderlyingIterator: @escaping () -> I _ makeUnderlyingIterator: @escaping () -> I
) -> Plan where I: IteratorProtocol, I.Element == Date { ) -> Plan where I: IteratorProtocol, I.Element == Date {
return Plan.make { () -> AnyIterator<Interval> in return Plan.make { () -> AnyIterator<Interval> in
var iterator = makeUnderlyingIterator() var iterator = makeUnderlyingIterator()
var last: Date! var prev: Date!
return AnyIterator { return AnyIterator {
last = last ?? Date() prev = prev ?? Date()
guard let next = iterator.next() else { return nil } guard let next = iterator.next() else { return nil }
defer { last = next } defer { prev = next }
return next.interval(since: last) return next.interval(since: prev)
} }
} }
} }
/// Creates a plan from a list of dates. /// Creates a plan from a list of dates.
///
/// The task will be executed at each date in the array. /// The task will be executed at each date in the array.
public static func of(_ dates: Date...) -> Plan { public static func of(_ dates: Date...) -> Plan {
return Plan.of(dates) return Plan.of(dates)
} }
/// Creates a plan from a list of dates. /// Creates a plan from a list of dates.
///
/// The task will be executed at each date in the array. /// 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 { public static func of<S>(_ sequence: S) -> Plan where S: Sequence, S.Element == Date {
return Plan.make(sequence.makeIterator) return Plan.make(sequence.makeIterator)
} }
@ -143,13 +148,13 @@ extension Plan {
public var dates: AnySequence<Date> { public var dates: AnySequence<Date> {
return AnySequence { () -> AnyIterator<Date> in return AnySequence { () -> AnyIterator<Date> in
let iterator = self.makeIterator() let iterator = self.makeIterator()
var last: Date! var prev: Date!
return AnyIterator { return AnyIterator {
last = last ?? Date() prev = prev ?? Date()
guard let interval = iterator.next() else { return nil } guard let interval = iterator.next() else { return nil }
// swiftlint:disable shorthand_operator // swiftlint:disable shorthand_operator
last = last + interval prev = prev + interval
return last return prev
} }
} }
} }
@ -157,27 +162,27 @@ extension Plan {
extension Plan { extension Plan {
/// A plan with a distant past date. /// A plan of a distant past date.
public static var distantPast: Plan { public static var distantPast: Plan {
return Plan.of(Date.distantPast) return Plan.of(Date.distantPast)
} }
/// A plan with a distant future date. /// A plan of a distant future date.
public static var distantFuture: Plan { public static var distantFuture: Plan {
return Plan.of(Date.distantFuture) return Plan.of(Date.distantFuture)
} }
/// A plan that is never going to happen. /// A plan that will never happen.
public static var never: Plan { public static var never: Plan {
return Plan.make { return Plan.make {
AnyIterator<Date> { nil } AnyIterator<Interval> { nil }
} }
} }
} }
extension Plan { 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: /// 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: /// For example:
/// ///
/// let s0 = Plan.of(1.second, 3.seconds, 5.seconds) /// let s0 = Plan.of(1.second, 3.seconds, 5.seconds)
/// let s1 = Plan.of(2.seconds, 4.seconds, 6.seconds) /// let s1 = Plan.of(2.seconds, 4.seconds, 6.seconds)
/// let s2 = s0.concat(s1) /// let s2 = s0.merge(s1)
/// > s2 /// > s2
/// > 1.second, 1.seconds, 2.seconds, 2.seconds, 3.seconds, 3.seconds /// > 1.second, 1.seconds, 2.seconds, 2.seconds, 3.seconds, 3.seconds
public func merge(_ plan: Plan) -> Plan { public func merge(_ plan: Plan) -> Plan {
return Plan.make { () -> AnyIterator<Date> in return Plan.make { () -> AnyIterator<Date> in
let i0 = self.dates.makeIterator() let i0 = self.dates.makeIterator()
let i1 = plan.dates.makeIterator() let i1 = plan.dates.makeIterator()
var buffer0: Date!
var buffer1: Date! var buf0: Date!
var buf1: Date!
return AnyIterator<Date> { return AnyIterator<Date> {
if buffer0 == nil { buffer0 = i0.next() } if buf0 == nil { buf0 = i0.next() }
if buffer1 == nil { buffer1 = i1.next() } if buf1 == nil { buf1 = i1.next() }
var d: Date! var d: Date!
if let d0 = buffer0, let d1 = buffer1 { if let d0 = buf0, let d1 = buf1 {
d = Swift.min(d0, d1) d = Swift.min(d0, d1)
} else { } else {
d = buffer0 ?? buffer1 d = buf0 ?? buf1
} }
if d == buffer0 { buffer0 = nil } if d == nil { return d }
if d == buffer1 { buffer1 = nil }
if d == buf0 { buf0 = nil; return d }
if d == buf1 { buf1 = nil }
return d 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: /// For example:
/// ///
@ -251,70 +260,69 @@ extension Plan {
} }
} }
/// Returns a new plan by only taking the part before the date. /// Returns a new plan by taking the part before the given date.
public func until(_ date: Date) -> Plan { public func until(_ date: Date) -> Plan {
return Plan.make { () -> AnyIterator<Date> in return Plan.make { () -> AnyIterator<Date> in
let iterator = self.dates.makeIterator() let iterator = self.dates.makeIterator()
return AnyIterator { return AnyIterator {
guard let next = iterator.next(), next < date else { guard let next = iterator.next(), next < date else {
return nil return nil
} }
return next return next
} }
} }
} }
}
extension Plan {
/// Creates a plan that executes the task immediately. /// Creates a plan that executes the task immediately.
public static var now: Plan { public static var now: Plan {
return Plan.of(0.nanosecond) 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 { public static func after(_ delay: Interval) -> Plan {
return Plan.of(delay) return Plan.of(delay)
} }
/// 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 interval. /// Creates a plan that executes the task every given interval.
public static func every(_ interval: Interval) -> Plan { public static func every(_ interval: Interval) -> Plan {
return Plan.make { return Plan.make {
AnyIterator { interval } AnyIterator { interval }
} }
} }
/// Creates a plan that executes the task after delay then repeat /// Creates a plan that executes the task every given period.
/// 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.
public static func every(_ period: Period) -> Plan { public static func every(_ period: Period) -> Plan {
return Plan.make { () -> AnyIterator<Interval> in return Plan.make { () -> AnyIterator<Interval> in
let calendar = Calendar.gregorian let calendar = Calendar.gregorian
var last: Date! var prev: Date!
return AnyIterator { return AnyIterator {
last = last ?? Date() prev = prev ?? Date()
guard let next = calendar.date(byAdding: period.asDateComponents(), guard
to: last) else { let next = calendar.date(
byAdding: period.asDateComponents(),
to: prev)
else {
return nil return nil
} }
defer { last = next } defer { prev = next }
return next.interval(since: last) return next.interval(since: prev)
} }
} }
} }
/// Creates a plan that executes the task every period. /// 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 { public static func every(_ period: String) -> Plan {
guard let p = Period(period) else { guard let p = Period(period) else {
return Plan.never return Plan.never
@ -326,16 +334,16 @@ extension Plan {
extension Plan { extension Plan {
/// `DateMiddleware` represents a middleware that wraps a 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 { public struct DateMiddleware {
fileprivate let plan: Plan fileprivate let plan: Plan
/// Returns a plan at the specific time. /// Creates a plan with time specified.
public func at(_ time: Time) -> Plan { public func at(_ time: Time) -> Plan {
guard !self.plan.isNever() else { return .never } if plan.isNever() { return .never }
var interval = time.intervalSinceStartOfDay var interval = time.intervalSinceStartOfDay
return Plan.make { () -> AnyIterator<Interval> in 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 { public func at(_ time: String) -> Plan {
guard if plan.isNever() { return .never }
!self.plan.isNever(), guard let time = Time(time) else {
let time = Time(time)
else {
return .never return .never
} }
return at(time) return at(time)
} }
/// Returns a plan at the specific time. /// Creates a plan with time specified.
/// ///
/// .at(1) => 01 /// .at(1) => 01
/// .at(1, 2) => 01:02 /// .at(1, 2) => 01:02
/// .at(1, 2, 3) => 01:02:03 /// .at(1, 2, 3) => 01:02:03
/// .at(1, 2, 3, 456) => 01:02:03.456 /// .at(1, 2, 3, 456) => 01:02:03.456
///
/// - Note: Returns `Plan.never` if given no parameters.
public func at(_ time: Int...) -> Plan { public func at(_ time: Int...) -> Plan {
return self.at(time) return self.at(time)
} }
/// Returns a plan at the specific time. /// Creates a plan with time specified.
/// ///
/// .at([1]) => 01 /// .at([1]) => 01
/// .at([1, 2]) => 01:02 /// .at([1, 2]) => 01:02
/// .at([1, 2, 3]) => 01:02:03 /// .at([1, 2, 3]) => 01:02:03
/// .at([1, 2, 3, 456]) => 01:02:03.456 /// .at([1, 2, 3, 456]) => 01:02:03.456
///
/// - Note: Returns `Plan.never` if given an empty array.
public func at(_ time: [Int]) -> Plan { 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 hour = time[0]
let minute = time.count > 1 ? time[1] : 0 let minute = time.count > 1 ? time[1] : 0
let second = time.count > 2 ? time[2] : 0 let second = time.count > 2 ? time[2] : 0
let nanosecond = time.count > 3 ? time[3]: 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 Plan.never
} }
return at(time) 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 { public static func every(_ weekday: Weekday) -> DateMiddleware {
let plan = Plan.make { () -> AnyIterator<Date> in let plan = Plan.make { () -> AnyIterator<Date> in
let calendar = Calendar.gregorian let calendar = Calendar.gregorian
@ -419,14 +425,12 @@ extension Plan {
return DateMiddleware(plan: plan) return DateMiddleware(plan: plan)
} }
/// Creates a plan that executes the task every specific weekdays. /// Creates a date middleware that executes the task on every specific week day.
/// - Note: Returns initialized with `Plan.never` if given no parameters.
public static func every(_ weekdays: Weekday...) -> DateMiddleware { public static func every(_ weekdays: Weekday...) -> DateMiddleware {
return Plan.every(weekdays) return Plan.every(weekdays)
} }
/// Creates a plan that executes the task every specific weekdays. /// Creates a date middleware that executes the task on every specific week day.
/// - Note: Returns initialized with `Plan.never` if given an empty array.
public static func every(_ weekdays: [Weekday]) -> DateMiddleware { public static func every(_ weekdays: [Weekday]) -> DateMiddleware {
guard !weekdays.isEmpty else { return .init(plan: .never) } guard !weekdays.isEmpty else { return .init(plan: .never) }
@ -439,7 +443,7 @@ extension Plan {
return DateMiddleware(plan: 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 { public static func every(_ monthday: Monthday) -> DateMiddleware {
let plan = Plan.make { () -> AnyIterator<Date> in let plan = Plan.make { () -> AnyIterator<Date> in
let calendar = Calendar.gregorian let calendar = Calendar.gregorian
@ -459,14 +463,12 @@ extension Plan {
return DateMiddleware(plan: plan) return DateMiddleware(plan: plan)
} }
/// Creates a plan that executes the task every specific days in the months. /// Creates a date middleware that executes the task on every specific month day.
/// - Note: Returns initialized with `Plan.never` if given no parameters.
public static func every(_ mondays: Monthday...) -> DateMiddleware { public static func every(_ mondays: Monthday...) -> DateMiddleware {
return Plan.every(mondays) return Plan.every(mondays)
} }
/// Creates a plan that executes the task every specific days in the months. /// Creates a date middleware that executes the task on every specific month day.
/// - Note: Returns initialized with `Plan.never` if given an empty array.
public static func every(_ mondays: [Monthday]) -> DateMiddleware { public static func every(_ mondays: [Monthday]) -> DateMiddleware {
guard !mondays.isEmpty else { return .init(plan: .never) } guard !mondays.isEmpty else { return .init(plan: .never) }
@ -484,11 +486,12 @@ extension Plan {
/// Returns a Boolean value indicating whether this plan is empty. /// Returns a Boolean value indicating whether this plan is empty.
public func isNever() -> Bool { public func isNever() -> Bool {
return iSeq.makeIterator().next() == nil return seq.makeIterator().next() == nil
} }
} }
extension Plan { extension Plan {
/// Creates a new plan that is offset by the specified interval in the /// Creates a new plan that is offset by the specified interval in the
/// closure body. /// closure body.
/// ///
@ -497,23 +500,15 @@ extension Plan {
/// ///
/// If the returned interval offset is `nil`, then no offset is added /// If the returned interval offset is `nil`, then no offset is added
/// to that next-run date. /// 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 return Plan.make { () -> AnyIterator<Interval> in
let it = self.makeIterator() let it = self.makeIterator()
return AnyIterator { return AnyIterator {
if let next = it.next() { if let next = it.next() {
return next + (intervalOffset() ?? 0.second) return next + (interval() ?? 0.second)
} }
return nil 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. /// Schedules a task with this plan.
/// ///
/// When time is up, the task will be executed on current thread. It behaves /// 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 /// like a `Timer`, so you need to make sure that the current thread has a
/// runloop available. /// 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: _)`. /// `do(queue: _, onElapse: _)`.
/// ///
/// - Parameters: /// - Parameters:
/// - mode: The mode in which to add the task. /// - mode: The mode to which the block should be added.
/// - onElapse: The action to do when time is out. /// - block: A block to be executed when time is up.
/// - Returns: The task just created. /// - Returns: The task just created.
public func `do`(mode: RunLoop.Mode = .common, public func `do`(
onElapse: @escaping (Task) -> Void) -> Task { mode: RunLoop.Mode = .common,
return RunLoopTask(plan: self, mode: mode, onElapse: onElapse) block: @escaping (Task) -> Void
) -> Task {
return RunLoopTask(plan: self, mode: mode, block: block)
} }
/// Schedules a task with this plan. /// Schedules a task with this plan.
/// ///
/// When time is up, the task will be executed on current thread. It behaves /// 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 /// like a `Timer`, so you need to make sure that the current thread has a
/// runloop available. /// 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: _)`. /// `do(queue: _, onElapse: _)`.
/// ///
/// - Parameters: /// - Parameters:
/// - mode: The mode in which to add the task. /// - mode: The mode to which the block should be added.
/// - onElapse: The action to do when time is out. /// - block: A block to be executed when time is up.
/// - Returns: The task just created. /// - Returns: The task just created.
public func `do`(mode: RunLoop.Mode = .common, public func `do`(
onElapse: @escaping () -> Void) -> Task { mode: RunLoop.Mode = .common,
return self.do(mode: mode) { (_) in block: @escaping () -> Void
onElapse() ) -> Task {
return self.do(mode: mode) { _ in
block()
} }
} }
} }
@ -45,24 +49,26 @@ private final class RunLoopTask: Task {
var timer: Timer! 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? timer = Timer(
fire: Date.distantFuture,
let distant = Date.distantFuture.timeIntervalSinceReferenceDate interval: .greatestFiniteMagnitude,
timer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, distant, distant, 0, 0) { _ in repeats: false
guard let task = this else { return } ) { [weak self] _ in
onElapse(task) guard let self = self else { return }
block(self)
} }
RunLoop.current.add(timer, forMode: mode) 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 { deinit {

View File

@ -7,13 +7,13 @@ public struct ActionKey {
} }
extension BagKey { extension BagKey {
fileprivate func asActionKey() -> ActionKey { fileprivate func asActionKey() -> ActionKey {
return ActionKey(bagKey: self) return ActionKey(bagKey: self)
} }
} }
/// `Task` represents a timed task. /// `Task` represents a timing task.
open class Task { open class Task {
/// The unique id of this task. /// The unique id of this task.
@ -21,77 +21,86 @@ open class Task {
public typealias Action = (Task) -> Void public typealias Action = (Task) -> Void
private let _mutex = NSRecursiveLock() private let _lock = NSRecursiveLock()
private var _iterator: AnyIterator<Interval> private var _iterator: AnyIterator<Interval>
private var _timer: DispatchSourceTimer 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 _suspensionCount: UInt64 = 0
private lazy var _timeline = Timeline() private lazy var _executionCount: Int = 0
private lazy var _countOfExecutions: Int = 0
private lazy var _lifetime: Interval = Int.max.seconds
private lazy var _lifetimeTimer: DispatchSourceTimer = {
let timer = DispatchSource.makeTimerSource()
timer.setEventHandler { [weak self] in
self?.cancel()
}
timer.schedule(after: _lifetime)
timer.resume()
return timer
}()
private weak var _taskCenter: TaskCenter?
/// The task center which this task currently in. private var _firstExecutionDate: Date?
open var taskCenter: TaskCenter? { private var _lastExecutionDate: Date?
return _taskCenter private var _estimatedNextExecutionDate: Date?
/// The date of creation.
public let creationDate = Date()
/// The date of first execution.
open var firstExecutionDate: Date? {
return _lock.withLock { _firstExecutionDate }
} }
/// The mutex used to guard task center operations. /// The date of last execution.
private let _taskCenterLock = NSRecursiveLock() open var lastExecutionDate: Date? {
return _lock.withLock { _lastExecutionDate }
}
/// The date of estimated next execution.
open var estimatedNextExecutionDate: Date? {
return _lock.withLock { _estimatedNextExecutionDate }
}
private weak var _taskCenter: TaskCenter?
/// The task center to which this task currently belongs.
open var taskCenter: TaskCenter? {
return _lock.withLock { _taskCenter }
}
private let _taskCenterLock = NSRecursiveLock()
/// Adds this task to the given task center. /// 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) { func addToTaskCenter(_ center: TaskCenter) {
_taskCenterLock.lock() _taskCenterLock.lock()
defer { _taskCenterLock.unlock() } defer { _taskCenterLock.unlock() }
if _taskCenter === center { return } if _taskCenter === center { return }
_taskCenter?.remove(self) let c = _taskCenter
_taskCenter = center _taskCenter = center
c?.remove(self)
} }
/// Removes this task from the given task center. /// Removes this task from the given task center.
func removeFromTaskCenter(_ center: TaskCenter) { func removeFromTaskCenter(_ center: TaskCenter) {
_taskCenterLock.lock() _taskCenterLock.lock()
defer { _taskCenterLock.unlock() } defer { _taskCenterLock.unlock() }
if _taskCenter !== center { return } if _taskCenter !== center { return }
_taskCenter?.remove(self)
_taskCenter = nil _taskCenter = nil
center.remove(self)
} }
/// Initializes a normal task with specified plan and dispatch queue. /// Initializes a timing task.
/// ///
/// - Parameters: /// - Parameters:
/// - plan: The plan. /// - plan: The plan.
/// - queue: The dispatch queue to which all actions should be added. /// - queue: The dispatch queue to which the block should be dispatched.
/// - onElapse: The action to do when time is out. /// - block: A block to be executed when time is up.
init(plan: Plan, init(
queue: DispatchQueue?, plan: Plan,
onElapse: @escaping (Task) -> Void) { queue: DispatchQueue?,
block: @escaping (Task) -> Void
) {
_iterator = plan.makeIterator() _iterator = plan.makeIterator()
_timer = DispatchSource.makeTimerSource(queue: queue) _timer = DispatchSource.makeTimerSource(queue: queue)
_onElapseActions.append(onElapse) _actions.append(block)
_timer.setEventHandler { [weak self] in _timer.setEventHandler { [weak self] in
guard let self = self else { return } guard let self = self else { return }
@ -100,244 +109,163 @@ open class Task {
if let interval = _iterator.next(), !interval.isNegative { if let interval = _iterator.next(), !interval.isNegative {
_timer.schedule(after: interval) _timer.schedule(after: interval)
_timeline.estimatedNextExecution = Date().adding(interval) _estimatedNextExecutionDate = Date().adding(interval)
} }
_timer.resume() _timer.resume()
TaskCenter.default.add(self) TaskCenter.default.add(self)
} }
deinit { deinit {
while _suspensions > 0 { while _suspensionCount > 0 {
_timer.resume() _timer.resume()
_suspensions -= 1 _suspensionCount -= 1
} }
cancel() cancel()
taskCenter?.remove(self) taskCenter?.remove(self)
} }
private func elapse() {
scheduleNextExecution()
execute()
}
private func scheduleNext() { private func scheduleNextExecution() {
_mutex.withLockVoid { _lock.withLockVoid {
let now = Date() let now = Date()
var estimated = _timeline.estimatedNextExecution ?? now var estimated = _estimatedNextExecutionDate ?? now
repeat { repeat {
guard let interval = _iterator.next(), !interval.isNegative else { guard let interval = _iterator.next(), !interval.isNegative else {
_timeline.estimatedNextExecution = nil _estimatedNextExecutionDate = nil
return return
} }
estimated = estimated.adding(interval) estimated = estimated.adding(interval)
} while (estimated < now) } while (estimated < now)
_timeline.estimatedNextExecution = estimated _estimatedNextExecutionDate = estimated
_timer.schedule(after: _timeline.estimatedNextExecution!.interval(since: now)) _timer.schedule(after: _estimatedNextExecutionDate!.interval(since: now))
} }
} }
/// Execute this task now, without disrupting its plan. /// Execute this task now, without interrupting its plan.
public func execute() { open func execute() {
let actions = _mutex.withLock { () -> Bag<Task.Action> in let actions = _lock.withLock { () -> Bag<Task.Action> in
let now = Date() let now = Date()
if _timeline.firstExecution == nil { if _firstExecutionDate == nil {
_timeline.firstExecution = now _firstExecutionDate = now
} }
_timeline.lastExecution = now _lastExecutionDate = now
_countOfExecutions += 1 _executionCount += 1
return _onElapseActions return _actions
} }
actions.forEach { $0(self) } actions.forEach { $0(self) }
} }
private func elapse() { /// Host this task to an object, that is, when the object deallocates, this task will be cancelled.
scheduleNext()
execute()
}
#if canImport(ObjectiveC) #if canImport(ObjectiveC)
open func host(on target: AnyObject) { open func host(to target: AnyObject) {
DeinitObserver.observe(target) { [weak self] in DeinitObserver.observe(target) { [weak self] in
self?.cancel() self?.cancel()
} }
} }
#endif #endif
/// The number of times the task has been executed. /// The number of task executions.
public var countOfExecutions: Int { public var executionCount: Int {
return _mutex.withLock { return _lock.withLock {
_countOfExecutions _executionCount
} }
} }
/// A Boolean indicating whether the task was canceled. /// A Boolean indicating whether the task was canceled.
public var isCancelled: Bool { public var isCancelled: Bool {
return _mutex.withLock { return _lock.withLock {
_timer.isCancelled _timer.isCancelled
} }
} }
// MARK: - Manage
/// Reschedules this task with the new plan. /// Reschedules this task with the new plan.
public func reschedule(_ new: Plan) { public func reschedule(_ new: Plan) {
_mutex.withLockVoid { _lock.withLockVoid {
_iterator = new.makeIterator() _iterator = new.makeIterator()
} }
scheduleNext() scheduleNextExecution()
} }
/// Suspensions of this task. /// The number of task suspensions.
public var suspensions: UInt64 { public var suspensionCount: UInt64 {
return _mutex.withLock { return _lock.withLock {
_suspensions _suspensionCount
} }
} }
/// Suspends this task. /// Suspends this task.
public func suspend() { public func suspend() {
_mutex.withLockVoid { _lock.withLockVoid {
if _suspensions < UInt64.max { if _suspensionCount < UInt64.max {
_timer.suspend() _timer.suspend()
_suspensions += 1 _suspensionCount += 1
} }
} }
} }
/// Resumes this task. /// Resumes this task.
public func resume() { public func resume() {
_mutex.withLockVoid { _lock.withLockVoid {
if _suspensions > 0 { if _suspensionCount > 0 {
_timer.resume() _timer.resume()
_suspensions -= 1 _suspensionCount -= 1
} }
} }
} }
/// Cancels this task. /// Cancels this task.
public func cancel() { public func cancel() {
_mutex.withLockVoid { _lock.withLockVoid {
_timer.cancel() _timer.cancel()
} }
TaskCenter.default.remove(self) 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 // MARK: - Action
/// The number of actions in this task. /// The number of actions in this task.
public var countOfActions: Int { public var countOfActions: Int {
return _mutex.withLock { return _lock.withLock {
_onElapseActions.count _actions.count
} }
} }
/// Adds action to this task. /// Adds action to this task.
@discardableResult @discardableResult
public func addAction(_ action: @escaping (Task) -> Void) -> ActionKey { public func addAction(_ action: @escaping (Task) -> Void) -> ActionKey {
return _mutex.withLock { return _lock.withLock {
return _onElapseActions.append(action).asActionKey() return _actions.append(action).asActionKey()
} }
} }
/// Removes action by key from this task. /// Removes action by key from this task.
public func removeAction(byKey key: ActionKey) { public func removeAction(byKey key: ActionKey) {
_mutex.withLockVoid { _lock.withLockVoid {
_ = _onElapseActions.removeValue(for: key.bagKey) _ = _actions.removeValue(for: key.bagKey)
} }
} }
/// Removes all actions from this task. /// Removes all actions from this task.
public func removeAllActions() { public func removeAllActions() {
_mutex.withLockVoid { _lock.withLockVoid {
_onElapseActions.removeAll() _actions.removeAll()
} }
} }
// MARK: - Tag
open func add(to: TaskCenter) {
_mutex.lock()
}
} }
extension Task: Hashable { extension Task: Hashable {
/// Hashes the essential components of this value by feeding them into the given hasher.
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
hasher.combine(id) hasher.combine(id)
} }
@ -347,25 +275,3 @@ extension Task: Hashable {
return lhs === rhs 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 let lock = NSLock()
private var tasksOfTag: [String: Set<TaskBox>] = [:] private var tags: [String: Set<TaskBox>] = [:]
private var tagsOfTask: [TaskBox: Set<String>] = [:] private var tasks: [TaskBox: Set<String>] = [:]
/// Default task center. /// Default task center.
open class var `default`: TaskCenter { open class var `default`: TaskCenter {
@ -39,14 +39,12 @@ open class TaskCenter {
} }
/// Adds the given task to this center. /// Adds the given task to this center.
///
/// Center won't retain the task.
open func add(_ task: Task) { open func add(_ task: Task) {
task.addToTaskCenter(self) task.addToTaskCenter(self)
lock.withLockVoid { lock.withLockVoid {
let box = TaskBox(task) let box = TaskBox(task)
tagsOfTask[box] = [] self.tasks[box] = []
} }
} }
@ -56,15 +54,14 @@ open class TaskCenter {
lock.withLockVoid { lock.withLockVoid {
let box = TaskBox(task) let box = TaskBox(task)
if let tags = self.tagsOfTask[box] { if let tags = self.tasks[box] {
for tag in tags { for tag in tags {
self.tasksOfTag[tag]?.remove(box) self.tags[tag]?.remove(box)
if self.tags[tag]?.count == 0 {
if self.tasksOfTag[tag]?.count == 0 { self.tags[tag] = nil
self.tasksOfTag[tag] = nil
} }
} }
self.tagsOfTask[box] = nil self.tasks[box] = nil
} }
} }
} }
@ -84,15 +81,12 @@ open class TaskCenter {
lock.withLockVoid { lock.withLockVoid {
let box = TaskBox(task) let box = TaskBox(task)
if tagsOfTask[box] == nil {
tagsOfTask[box] = []
}
for tag in tags { for tag in tags {
tagsOfTask[box]?.insert(tag) tasks[box]?.insert(tag)
if tasksOfTag[tag] == nil { if self.tags[tag] == nil {
tasksOfTag[tag] = [] self.tags[tag] = []
} }
tasksOfTag[tag]?.insert(box) self.tags[tag]?.insert(box)
} }
} }
} }
@ -113,64 +107,67 @@ open class TaskCenter {
lock.withLockVoid { lock.withLockVoid {
let box = TaskBox(task) let box = TaskBox(task)
for tag in tags { for tag in tags {
tagsOfTask[box]?.remove(tag) self.tasks[box]?.remove(tag)
tasksOfTag[tag]?.remove(box) 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. /// 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 [] } guard task.taskCenter === self else { return [] }
return lock.withLock { return lock.withLock {
Array(tagsOfTask[TaskBox(task)] ?? []) Array(tasks[TaskBox(task)] ?? [])
} }
} }
/// Returns all tasks that have the tag. /// Returns all tasks for the tag.
open func tasksForTag(_ tag: String) -> [Task] { open func tasks(forTag tag: String) -> [Task] {
return lock.withLock { return lock.withLock {
tasksOfTag[tag]?.compactMap { $0.task } ?? [] tags[tag]?.compactMap { $0.task } ?? []
} }
} }
/// Returns all tasks in this center. /// Returns all tasks in this center.
open var allTasks: [Task] { open var allTasks: [Task] {
return lock.withLock { 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] { open var allTags: [String] {
return lock.withLock { return lock.withLock {
tasksOfTag.map { $0.key } tags.map { $0.key }
} }
} }
/// Removes all tasks from this center. /// Removes all tasks from this center.
open func removeAll() { open func removeAll() {
lock.withLockVoid { lock.withLockVoid {
tagsOfTask = [:] tasks = [:]
tasksOfTag = [:] tags = [:]
} }
} }
/// Suspends all tasks that have the tag. /// Suspends all tasks by tag.
open func suspendByTag(_ tag: String) { open func suspend(byTag tag: String) {
tasksForTag(tag).forEach { $0.suspend() } tasks(forTag: tag).forEach { $0.suspend() }
} }
/// Resumes all tasks that have the tag. /// Resumes all tasks by tag.
open func resumeByTag(_ tag: String) { open func resume(byTag tag: String) {
tasksForTag(tag).forEach { $0.resume() } tasks(forTag: tag).forEach { $0.resume() }
} }
/// Cancels all tasks that have the tag. /// Cancels all tasks by tag.
open func cancelByTag(_ tag: String) { open func cancel(byTag tag: String) {
tasksForTag(tag).forEach { $0.cancel() } tasks(forTag: tag).forEach { $0.cancel() }
} }
} }

View File

@ -48,8 +48,15 @@ public struct Time {
public init?(_ string: String) { public init?(_ string: String) {
let pattern = "^(\\d{1,2})(:(\\d{1,2})(:(\\d{1,2})(.(\\d{1,3}))?)?)?( (am|AM|pm|PM))?$" 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: []) 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 hasAM = false
var hasPM = false var hasPM = false
@ -87,9 +94,9 @@ public struct Time {
/// Returns a dateComponenets of the time, using gregorian calender and /// Returns a dateComponenets of the time, using gregorian calender and
/// current time zone. /// current time zone.
public func asDateComponents() -> DateComponents { public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents {
return DateComponents(calendar: Calendar.gregorian, return DateComponents(calendar: Calendar.gregorian,
timeZone: TimeZone.current, timeZone: timeZone,
hour: hour, hour: hour,
minute: minute, minute: minute,
second: second, 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 /// Returns dateComponenets of the weekday, using gregorian calender and
/// current time zone. /// current time zone.
public func asDateComponents() -> DateComponents { public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents {
return DateComponents( return DateComponents(
calendar: Calendar.gregorian, calendar: Calendar.gregorian,
timeZone: TimeZone.current, timeZone: timeZone,
weekday: rawValue) weekday: rawValue)
} }
} }
@ -18,8 +18,10 @@ public enum Weekday: Int {
extension Date { extension Date {
/// Returns a Boolean value indicating whether this date is the weekday in current time zone. /// Returns a Boolean value indicating whether this date is the weekday in current time zone.
public func `is`(_ weekday: Weekday) -> Bool { public func `is`(_ weekday: Weekday, in timeZone: TimeZone = .current) -> Bool {
return Calendar.gregorian.component(.weekday, from: self) == weekday.rawValue var cal = Calendar.gregorian
cal.timeZone = timeZone
return cal.component(.weekday, from: self) == weekday.rawValue
} }
} }

View File

@ -8,7 +8,7 @@ final class AtomicTests: XCTestCase {
let val = i.read { $0 } let val = i.read { $0 }
XCTAssertEqual(val, 1) XCTAssertEqual(val, 1)
} }
func testReadVoid() { func testReadVoid() {
let i = Atomic<Int>(1) let i = Atomic<Int>(1)
var val = 0 var val = 0
@ -24,7 +24,7 @@ final class AtomicTests: XCTestCase {
} }
XCTAssertEqual(i.read { $0 }, val) XCTAssertEqual(i.read { $0 }, val)
} }
func testWriteVoid() { func testWriteVoid() {
let i = Atomic<Int>(1) let i = Atomic<Int>(1)
var val = 0 var val = 0
@ -39,6 +39,6 @@ final class AtomicTests: XCTestCase {
("testRead", testRead), ("testRead", testRead),
("testReadVoid", testReadVoid), ("testReadVoid", testReadVoid),
("testWrite", testWrite), ("testWrite", testWrite),
("testWriteVoid", testWriteVoid), ("testWriteVoid", testWriteVoid)
] ]
} }

View File

@ -26,15 +26,15 @@ final class BagTests: XCTestCase {
var bag = Bag<Fn>() var bag = Bag<Fn>()
let k1 = bag.append { 1 } let k1 = bag.append { 1 }
let k2 = bag.append { 2 } let k2 = bag.append { 2 }
let fn1 = bag.value(for: k1) let fn1 = bag.value(for: k1)
XCTAssertNotNil(fn1) XCTAssertNotNil(fn1)
let fn2 = bag.value(for: k2) let fn2 = bag.value(for: k2)
XCTAssertNotNil(fn2) XCTAssertNotNil(fn2)
guard let _fn1 = fn1, let _fn2 = fn2 else { return } guard let _fn1 = fn1, let _fn2 = fn2 else { return }
XCTAssertEqual(_fn1(), 1) XCTAssertEqual(_fn1(), 1)
XCTAssertEqual(_fn2(), 2) XCTAssertEqual(_fn2(), 2)
} }
@ -52,24 +52,24 @@ final class BagTests: XCTestCase {
let fn2 = bag.removeValue(for: k2) let fn2 = bag.removeValue(for: k2)
XCTAssertNotNil(fn2) XCTAssertNotNil(fn2)
XCTAssertNil(bag.removeValue(for: k2)) XCTAssertNil(bag.removeValue(for: k2))
guard let _fn1 = fn1, let _fn2 = fn2 else { return } guard let _fn1 = fn1, let _fn2 = fn2 else { return }
XCTAssertEqual(_fn1(), 1) XCTAssertEqual(_fn1(), 1)
XCTAssertEqual(_fn2(), 2) XCTAssertEqual(_fn2(), 2)
} }
func testCount() { func testCount() {
var bag = Bag<Fn>() var bag = Bag<Fn>()
let k1 = bag.append { 1 } let k1 = bag.append { 1 }
let k2 = bag.append { 2 } let k2 = bag.append { 2 }
XCTAssertEqual(bag.count, 2) XCTAssertEqual(bag.count, 2)
bag.removeValue(for: k1) bag.removeValue(for: k1)
bag.removeValue(for: k2) bag.removeValue(for: k2)
XCTAssertEqual(bag.count, 0) XCTAssertEqual(bag.count, 0)
} }

View File

@ -16,7 +16,7 @@ final class DeinitObserverTests: XCTestCase {
fn() fn()
XCTAssertEqual(i, 1) XCTAssertEqual(i, 1)
} }
func testCancel() { func testCancel() {
var i = 0 var i = 0
let fn = { let fn = {

View File

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

View File

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

View File

@ -9,64 +9,65 @@ import XCTest
@testable import Schedule @testable import Schedule
final class IntervalTests: XCTestCase { final class IntervalTests: XCTestCase {
private let leeway = 0.01.second private let leeway = 0.01.second
func testEquatable() { func testEquatable() {
XCTAssertEqual(1.second, 1.second) XCTAssertEqual(1.second, 1.second)
XCTAssertEqual(1.week, 7.days) XCTAssertEqual(1.week, 7.days)
} }
func testIsNegative() { func testIsNegative() {
XCTAssertFalse(1.second.isNegative) XCTAssertFalse(1.second.isNegative)
XCTAssertTrue((-1).second.isNegative) XCTAssertTrue((-1).second.isNegative)
} }
func testAbs() { func testAbs() {
XCTAssertEqual(1.second, (-1).second.abs) XCTAssertEqual(1.second, (-1).second.abs)
} }
func testNegated() { func testNegated() {
XCTAssertEqual(1.second.negated, (-1).second) XCTAssertEqual(1.second.negated, (-1).second)
XCTAssertEqual(1.second.negated.negated, 1.second) XCTAssertEqual(1.second.negated.negated, 1.second)
} }
func testCompare() { func testCompare() {
XCTAssertEqual((-1).second.compare(1.second), ComparisonResult.orderedAscending) XCTAssertEqual((-1).second.compare(1.second), ComparisonResult.orderedAscending)
XCTAssertEqual(8.days.compare(1.week), ComparisonResult.orderedDescending) XCTAssertEqual(8.days.compare(1.week), ComparisonResult.orderedDescending)
XCTAssertEqual(1.day.compare(24.hours), ComparisonResult.orderedSame)
XCTAssertTrue(23.hours < 1.day) XCTAssertTrue(23.hours < 1.day)
XCTAssertTrue(25.hours > 1.day) XCTAssertTrue(25.hours > 1.day)
} }
func testLongerShorter() { func testLongerShorter() {
XCTAssertTrue((-25).hour.isLonger(than: 1.day)) XCTAssertTrue((-25).hour.isLonger(than: 1.day))
XCTAssertTrue(1.week.isShorter(than: 8.days)) XCTAssertTrue(1.week.isShorter(than: 8.days))
} }
func testMultiplying() { func testMultiplying() {
XCTAssertEqual(7.days * 2, 2.week) XCTAssertEqual(7.days * 2, 2.week)
} }
func testAdding() { func testAdding() {
XCTAssertEqual(6.days + 1.day, 1.week) XCTAssertEqual(6.days + 1.day, 1.week)
XCTAssertEqual(1.1.weeks, 1.week + 0.1.weeks) XCTAssertEqual(1.1.weeks, 1.week + 0.1.weeks)
} }
func testOperators() { func testOperators() {
XCTAssertEqual(1.week - 6.days, 1.day) XCTAssertEqual(1.week - 6.days, 1.day)
var i = 6.days var i = 6.days
i += 1.day i += 1.day
XCTAssertEqual(i, 1.week) XCTAssertEqual(i, 1.week)
XCTAssertEqual(-(7.days), (-1).week) XCTAssertEqual(-(7.days), (-1).week)
} }
func testAs() { func testAs() {
XCTAssertEqual(1.millisecond.asNanoseconds(), 1.microsecond.asNanoseconds() * pow(10, 3)) XCTAssertEqual(1.millisecond.asNanoseconds(), 1.microsecond.asNanoseconds() * pow(10, 3))
XCTAssertEqual(1.second.asNanoseconds(), pow(10, 9)) XCTAssertEqual(1.second.asNanoseconds(), pow(10, 9))
XCTAssertEqual(1.second.asMicroseconds(), pow(10, 6)) XCTAssertEqual(1.second.asMicroseconds(), pow(10, 6))
XCTAssertEqual(1.second.asMilliseconds(), pow(10, 3)) XCTAssertEqual(1.second.asMilliseconds(), pow(10, 3))
@ -77,23 +78,23 @@ final class IntervalTests: XCTestCase {
XCTAssertEqual(1.week.asDays(), 7) XCTAssertEqual(1.week.asDays(), 7)
XCTAssertEqual(7.days.asWeeks(), 1) XCTAssertEqual(7.days.asWeeks(), 1)
} }
func testDate() { func testDate() {
let date0 = Date() let date0 = Date()
let date1 = date0.addingTimeInterval(100) let date1 = date0.addingTimeInterval(100)
XCTAssertTrue(date1.intervalSinceNow.isAlmostEqual(to: 100.seconds, leeway: leeway)) XCTAssertTrue(date1.intervalSinceNow.isAlmostEqual(to: 100.seconds, leeway: leeway))
XCTAssertEqual(date0.interval(since: date1), date0.timeIntervalSince(date1).seconds) XCTAssertEqual(date0.interval(since: date1), date0.timeIntervalSince(date1).seconds)
XCTAssertEqual(date0.adding(1.seconds), date0.addingTimeInterval(1)) XCTAssertEqual(date0.adding(1.seconds), date0.addingTimeInterval(1))
XCTAssertEqual(date0 + 1.seconds, date0.addingTimeInterval(1)) XCTAssertEqual(date0 + 1.seconds, date0.addingTimeInterval(1))
} }
func testDescription() { func testDescription() {
XCTAssertEqual(1.nanosecond.debugDescription, "Interval: 1 nanosecond(s)") XCTAssertEqual(1.nanosecond.debugDescription, "Interval: 1 nanosecond(s)")
} }
static var allTests = [ static var allTests = [
("testEquatable", testEquatable), ("testEquatable", testEquatable),
("testIsNegative", testIsNegative), ("testIsNegative", testIsNegative),
@ -105,6 +106,6 @@ final class IntervalTests: XCTestCase {
("testAdding", testAdding), ("testAdding", testAdding),
("testOperators", testOperators), ("testOperators", testOperators),
("testAs", testAs), ("testAs", testAs),
("testDate", testDate), ("testDate", testDate)
] ]
} }

View File

@ -2,24 +2,24 @@ import XCTest
@testable import Schedule @testable import Schedule
final class MonthdayTests: XCTestCase { final class MonthdayTests: XCTestCase {
func testIs() { func testIs() {
// ! Be careful the time zone problem. // ! Be careful the time zone problem.
let d = Date(year: 2019, month: 1, day: 1) let d = Date(year: 2019, month: 1, day: 1)
XCTAssertTrue(d.is(.january(1))) XCTAssertTrue(d.is(.january(1), in: TimeZone.shanghai))
} }
func testAsDateComponents() { func testAsDateComponents() {
let comps = Monthday.april(1).asDateComponents() let comps = Monthday.april(1).asDateComponents()
XCTAssertEqual(comps.month, 4) XCTAssertEqual(comps.month, 4)
XCTAssertEqual(comps.day, 1) XCTAssertEqual(comps.day, 1)
} }
func testDescription() { func testDescription() {
let md = Monthday.april(1) let md = Monthday.april(1)
XCTAssertEqual(md.debugDescription, "Monthday: April 1st") XCTAssertEqual(md.debugDescription, "Monthday: April 1st")
} }
static var allTests = [ static var allTests = [
("testIs", testIs), ("testIs", testIs),
("testAsDateComponents", testAsDateComponents), ("testAsDateComponents", testAsDateComponents),

View File

@ -2,45 +2,48 @@ import XCTest
@testable import Schedule @testable import Schedule
final class PeriodTests: XCTestCase { final class PeriodTests: XCTestCase {
func testPeriod() { func testPeriod() {
let period = (1.year + 2.years + 1.month + 2.months + 3.days) let period = (1.year + 2.years + 1.month + 2.months + 3.days)
XCTAssertEqual(period.years, 3) XCTAssertEqual(period.years, 3)
XCTAssertEqual(period.months, 3) XCTAssertEqual(period.months, 3)
XCTAssertEqual(period.days, 3) XCTAssertEqual(period.days, 3)
} }
func testInitWithString() { func testInitWithString() {
let p1 = Period("one second") let p1 = Period("one second")
XCTAssertNotNil(p1) XCTAssertNotNil(p1)
XCTAssertEqual(p1!.seconds, 1) XCTAssertEqual(p1!.seconds, 1)
let p2 = Period("two hours and ten minutes") let p2 = Period("two hours and ten minutes")
XCTAssertNotNil(p2) XCTAssertNotNil(p2)
XCTAssertEqual(p2!.hours, 2) XCTAssertEqual(p2!.hours, 2)
XCTAssertEqual(p2!.minutes, 10) XCTAssertEqual(p2!.minutes, 10)
let p3 = Period("1 year, 2 months and 3 days") let p3 = Period("1 year, 2 months and 3 days")
XCTAssertNotNil(p3) XCTAssertNotNil(p3)
XCTAssertEqual(p3!.years, 1) XCTAssertEqual(p3!.years, 1)
XCTAssertEqual(p3!.months, 2) XCTAssertEqual(p3!.months, 2)
XCTAssertEqual(p3!.days, 3) XCTAssertEqual(p3!.days, 3)
Period.registerQuantifier("many", for: 100 * 1000) Period.registerQuantifier("many", for: 100 * 1000)
let p4 = Period("many days") let p4 = Period("many days")
XCTAssertEqual(p4!.days, 100 * 1000) XCTAssertEqual(p4!.days, 100 * 1000)
let p5 = Period("hi, 😈")
XCTAssertNil(p5)
} }
func testAdd() { func testAdd() {
XCTAssertEqual(1.month.adding(1.month).months, 2) XCTAssertEqual(1.month.adding(1.month).months, 2)
XCTAssertEqual(Period(days: 1).adding(1.day).days, 2) XCTAssertEqual(Period(days: 1).adding(1.day).days, 2)
} }
func testTidy() { func testTidy() {
let period = 1.month.adding(25.hour).tidied(to: .day) let period = 1.month.adding(25.hour).tidied(to: .day)
XCTAssertEqual(period.days, 1) XCTAssertEqual(period.days, 1)
} }
func testAsDateComponents() { func testAsDateComponents() {
let period = Period(years: 1, months: 2, days: 3, hours: 4, minutes: 5, seconds: 6, nanoseconds: 7) let period = Period(years: 1, months: 2, days: 3, hours: 4, minutes: 5, seconds: 6, nanoseconds: 7)
let comps = period.asDateComponents() let comps = period.asDateComponents()
@ -52,13 +55,13 @@ final class PeriodTests: XCTestCase {
XCTAssertEqual(comps.second, 6) XCTAssertEqual(comps.second, 6)
XCTAssertEqual(comps.nanosecond, 7) XCTAssertEqual(comps.nanosecond, 7)
} }
func testDate() { func testDate() {
let d = Date(year: 1989, month: 6, day: 4) + 1.year let d = Date(year: 1989, month: 6, day: 4) + 1.year
let year = d.dateComponents.year let year = d.dateComponents.year
XCTAssertEqual(year, 1990) XCTAssertEqual(year, 1990)
} }
func testDescription() { func testDescription() {
let period = Period(years: 1, nanoseconds: 1) let period = Period(years: 1, nanoseconds: 1)
XCTAssertEqual(period.debugDescription, "Period: 1 year(s) 1 nanosecond(s)") XCTAssertEqual(period.debugDescription, "Period: 1 year(s) 1 nanosecond(s)")

View File

@ -3,34 +3,47 @@ import XCTest
final class PlanTests: XCTestCase { final class PlanTests: XCTestCase {
let leeway = 0.01.seconds private let leeway = 0.01.seconds
func testMake() { func testOfIntervals() {
let intervals = [1.second, 2.hours, 3.days, 4.weeks] let ints = [1.second, 2.hours, 3.days, 4.weeks]
let s0 = Plan.of(intervals[0], intervals[1], intervals[2], intervals[3]) let p = Plan.of(ints)
XCTAssertTrue(s0.makeIterator().isAlmostEqual(to: intervals, leeway: leeway)) XCTAssertTrue(p.makeIterator().isAlmostEqual(to: ints, leeway: leeway))
}
let d0 = Date() + intervals[0]
let d1 = d0 + intervals[1] func testOfDates() {
let d2 = d1 + intervals[2] let ints = [1.second, 2.hours, 3.days, 4.weeks]
let d3 = d2 + intervals[3]
let d0 = Date() + ints[0]
let s2 = Plan.of(d0, d1, d2, d3) let d1 = d0 + ints[1]
XCTAssertTrue(s2.makeIterator().isAlmostEqual(to: intervals, leeway: leeway)) let d2 = d1 + ints[2]
let d3 = d2 + ints[3]
let longTime = (100 * 365).days
XCTAssertTrue(Plan.distantPast.makeIterator().next()!.isLonger(than: longTime)) let p = Plan.of(d0, d1, d2, d3)
XCTAssertTrue(Plan.distantFuture.makeIterator().next()!.isLonger(than: longTime)) XCTAssertTrue(p.makeIterator().isAlmostEqual(to: ints, leeway: leeway))
}
func testDates() {
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 testDates() {
let iterator = Plan.of(1.days, 2.weeks).dates.makeIterator() func testDistant() {
var next = iterator.next() let distantPast = Plan.distantPast.makeIterator().next()
XCTAssertNotNil(next) XCTAssertNotNil(distantPast)
XCTAssertTrue(next!.intervalSinceNow.isAlmostEqual(to: 1.days, leeway: leeway)) XCTAssertTrue(distantPast!.isAlmostEqual(to: Date.distantPast.intervalSinceNow, leeway: leeway))
next = iterator.next()
XCTAssertNotNil(next) let distantFuture = Plan.distantFuture.makeIterator().next()
XCTAssertTrue(next!.intervalSinceNow.isAlmostEqual(to: 2.weeks + 1.days, leeway: leeway)) XCTAssertNotNil(distantFuture)
XCTAssertTrue(distantFuture!.isAlmostEqual(to: Date.distantFuture.intervalSinceNow, leeway: leeway))
} }
func testNever() { func testNever() {
@ -38,64 +51,64 @@ final class PlanTests: XCTestCase {
} }
func testConcat() { func testConcat() {
let s0: [Interval] = [1.second, 2.minutes, 3.hours] let p0: [Interval] = [1.second, 2.minutes, 3.hours]
let s1: [Interval] = [4.days, 5.weeks] let p1: [Interval] = [4.days, 5.weeks]
let s3 = Plan.of(s0).concat(Plan.of(s1)) let p2 = Plan.of(p0).concat(Plan.of(p1))
let s4 = Plan.of(s0 + s1) let p3 = Plan.of(p0 + p1)
XCTAssertTrue(s3.isAlmostEqual(to: s4, leeway: leeway)) XCTAssertTrue(p2.isAlmostEqual(to: p3, leeway: leeway))
} }
func testMerge() { func testMerge() {
let intervals0: [Interval] = [1.second, 2.minutes, 1.hour] let ints0: [Interval] = [1.second, 2.minutes, 1.hour]
let intervals1: [Interval] = [2.seconds, 1.minutes, 1.seconds] let ints1: [Interval] = [2.seconds, 1.minutes, 1.seconds]
let scheudle0 = Plan.of(intervals0).merge(Plan.of(intervals1)) let p0 = Plan.of(ints0).merge(Plan.of(ints1))
let scheudle1 = Plan.of(1.second, 1.second, 1.minutes, 1.seconds, 58.seconds, 1.hour) let p1 = Plan.of(1.second, 1.second, 1.minutes, 1.seconds, 58.seconds, 1.hour)
XCTAssertTrue(scheudle0.isAlmostEqual(to: scheudle1, leeway: leeway)) XCTAssertTrue(p0.isAlmostEqual(to: p1, 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))
}
func testFirst() { func testFirst() {
var count = 10 var count = 10
let s = Plan.every(1.second).first(count) let p = Plan.every(1.second).first(count)
let i = s.makeIterator() let i = p.makeIterator()
while count > 0 { while count > 0 {
XCTAssertNotNil(i.next()) XCTAssertNotNil(i.next())
count -= 1 count -= 1
} }
XCTAssertNil(i.next()) XCTAssertNil(i.next())
} }
func testUntil() { func testUntil() {
let until = Date() + 10.seconds let until = Date() + 10.seconds
let s = Plan.every(1.second).until(until).dates let p = Plan.every(1.second).until(until).dates
let i = s.makeIterator() let i = p.makeIterator()
while let date = i.next() { while let date = i.next() {
XCTAssertLessThan(date, until) XCTAssertLessThan(date, until)
} }
} }
func testNow() { func testNow() {
let s0 = Plan.now let p0 = Plan.now
let s1 = Plan.of(Date()) let p1 = Plan.of(Date())
XCTAssertTrue(s0.isAlmostEqual(to: s1, leeway: leeway)) 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() { func testAfterAndRepeating() {
let s0 = Plan.after(1.day, repeating: 1.hour).first(3) let p0 = Plan.after(1.day, repeating: 1.hour).first(3)
let s1 = Plan.of(1.day, 1.hour, 1.hour) let p1 = Plan.of(1.day, 1.hour, 1.hour)
XCTAssertTrue(s0.isAlmostEqual(to: s1, leeway: leeway)) XCTAssertTrue(p0.isAlmostEqual(to: p1, leeway: leeway))
} }
func testEveryPeriod() { func testEveryPeriod() {
let s = Plan.every("1 year").first(10) let p = Plan.every("1 year").first(10)
var date = Date() var date = Date()
for i in s.dates { for i in p.dates {
XCTAssertEqual(i.dateComponents.year!, date.dateComponents.year! + 1) XCTAssertEqual(i.dateComponents.year!, date.dateComponents.year! + 1)
XCTAssertEqual(i.dateComponents.month!, date.dateComponents.month!) XCTAssertEqual(i.dateComponents.month!, date.dateComponents.month!)
XCTAssertEqual(i.dateComponents.day!, date.dateComponents.day!) XCTAssertEqual(i.dateComponents.day!, date.dateComponents.day!)
@ -104,121 +117,47 @@ final class PlanTests: XCTestCase {
} }
func testEveryWeekday() { func testEveryWeekday() {
let s = Plan.every(.friday, .monday).at("11:11:00").first(5) let p = Plan.every(.friday, .monday).at("11:11:00").first(5)
for i in s.dates { for i in p.dates {
XCTAssertTrue(i.dateComponents.weekday == 6 || i.dateComponents.weekday == 2) XCTAssertTrue(i.dateComponents.weekday == 6 || i.dateComponents.weekday == 2)
XCTAssertEqual(i.dateComponents.hour, 11) XCTAssertEqual(i.dateComponents.hour, 11)
} }
} }
func testEveryMonthday() { func testEveryMonthday() {
let s = Plan.every(.april(1), .october(1)).at(11, 11).first(5) let p = Plan.every(.april(1), .october(1)).at(11, 11).first(5)
for i in s.dates { for i in p.dates {
XCTAssertTrue(i.dateComponents.month == 4 || i.dateComponents.month == 10) XCTAssertTrue(i.dateComponents.month == 4 || i.dateComponents.month == 10)
XCTAssertEqual(i.dateComponents.day, 1) XCTAssertEqual(i.dateComponents.day, 1)
XCTAssertEqual(i.dateComponents.hour, 11) XCTAssertEqual(i.dateComponents.hour, 11)
} }
} }
func testPassingEmptyArrays() { func testOffset() {
XCTAssertTrue(Plan.of([Interval]()).isNever()) let p1 = Plan.after(1.second).first(100)
XCTAssertTrue(Plan.of([Date]()).isNever()) let p2 = p1.offset(by: 1.second).first(100)
XCTAssertTrue(Plan.every([Weekday]()).at(11, 11).isNever()) for (d1, d2) in zip(p1.dates, p2.dates) {
XCTAssertTrue(Plan.every([Monthday]()).at(11, 11).isNever()) XCTAssertTrue(d2.interval(since: d1).isAlmostEqual(to: 1.second, leeway: leeway))
}
XCTAssertTrue(Plan.every(.monday).at([]).isNever())
XCTAssertTrue(Plan.every([Weekday]()).at("11:11:00").isNever())
}
func testIntervalOffset() {
// Non-offset plan
let e1 = expectation(description: "testIntervalOffset_1")
let plan1 = Plan.after(1.second)
var date1: Date?
// Offset plan
let e2 = expectation(description: "testIntervalOffset_2")
let plan2 = plan1.offset(by: 1.second)
var date2: Date?
let task1 = plan1.do { date1 = Date(); e1.fulfill() }
let task2 = plan2.do { date2 = Date(); e2.fulfill() }
_ = task1
_ = task2
waitForExpectations(timeout: 3.5)
XCTAssertNotNil(date1)
XCTAssertNotNil(date2)
XCTAssertTrue(date2!.interval(since: date1!).isAlmostEqual(to: 1.second, leeway: 0.1.seconds))
}
func testNegativeIntervalOffset() {
// Non-offset plan
let e1 = expectation(description: "testIntervalOffset_1")
let plan1 = Plan.after(2.seconds)
var date1: Date?
// Offset plan
let e2 = expectation(description: "testIntervalOffset_2")
let plan2 = plan1.offset(by: -1.second)
var date2: Date?
let task1 = plan1.do { date1 = Date(); e1.fulfill() }
let task2 = plan2.do { date2 = Date(); e2.fulfill() }
_ = task1
_ = task2
waitForExpectations(timeout: 2.5)
XCTAssertNotNil(date1)
XCTAssertNotNil(date2)
XCTAssertTrue(date2!.interval(since: date1!).isAlmostEqual(to: -1.second, leeway: 0.1.seconds))
}
func testNilIntervalOffset() {
// Non-offset plan
let e1 = expectation(description: "testIntervalOffset_1")
let plan1 = Plan.after(1.second)
var date1: Date?
// Offset plan
let e2 = expectation(description: "testIntervalOffset_2")
let plan2 = plan1.offset(by: nil)
var date2: Date?
let task1 = plan1.do { date1 = Date(); e1.fulfill() }
let task2 = plan2.do { date2 = Date(); e2.fulfill() }
_ = task1
_ = task2
waitForExpectations(timeout: 1.5)
XCTAssertNotNil(date1)
XCTAssertNotNil(date2)
XCTAssertTrue(date2!.interval(since: date1!).isAlmostEqual(to: 0.seconds, leeway: 0.1.seconds))
} }
static var allTests = [ static var allTests = [
("testMake", testMake), ("testOfIntervals", testOfIntervals),
("testOfDates", testOfDates),
("testDates", testDates), ("testDates", testDates),
("testDistant", testDistant),
("testNever", testNever), ("testNever", testNever),
("testConcat", testConcat), ("testConcat", testConcat),
("testMerge", testMerge), ("testMerge", testMerge),
("testAt", testAt),
("testFirst", testFirst), ("testFirst", testFirst),
("testUntil", testUntil), ("testUntil", testUntil),
("testNow", testNow), ("testNow", testNow),
("testAt", testAt),
("testAfterAndRepeating", testAfterAndRepeating), ("testAfterAndRepeating", testAfterAndRepeating),
("testEveryPeriod", testEveryPeriod), ("testEveryPeriod", testEveryPeriod),
("testEveryWeekday", testEveryWeekday), ("testEveryWeekday", testEveryWeekday),
("testEveryMonthday", testEveryMonthday), ("testEveryMonthday", testEveryMonthday),
("testPassingEmptyArrays", testPassingEmptyArrays), ("testOffset", testOffset)
("testIntervalOffset", testIntervalOffset),
("testNegativeIntervalOffset", testNegativeIntervalOffset),
("testNilIntervalOffset", testNilIntervalOffset)
] ]
} }

View File

@ -19,50 +19,58 @@ final class TaskCenterTests: XCTestCase {
} }
func testAdd() { func testAdd() {
let c = TaskCenter()
let task = makeTask() let task = makeTask()
XCTAssertEqual(center.allTasks.count, 1) XCTAssertEqual(center.allTasks.count, 1)
let c = TaskCenter()
c.add(task) c.add(task)
XCTAssertEqual(center.allTasks.count, 0) XCTAssertEqual(center.allTasks.count, 0)
XCTAssertEqual(c.allTasks.count, 1) XCTAssertEqual(c.allTasks.count, 1)
c.add(task) center.add(task)
XCTAssertEqual(c.allTasks.count, 1) XCTAssertEqual(center.allTasks.count, 1)
XCTAssertEqual(c.allTasks.count, 0)
center.removeAll() center.removeAll()
} }
func testRemove() { func testRemove() {
let task = makeTask() let task = makeTask()
let tag = UUID().uuidString
center.addTag(tag, to: task)
center.remove(task) center.remove(task)
XCTAssertFalse(center.allTasks.contains(task)) XCTAssertFalse(center.allTasks.contains(task))
XCTAssertFalse(center.allTags.contains(tag))
center.removeAll()
} }
func testTag() { func testTag() {
let task = makeTask() let task = makeTask()
let tag = UUID().uuidString let tag = UUID().uuidString
center.addTag(tag, to: task) center.addTag(tag, to: task)
XCTAssertTrue(center.tasksForTag(tag).contains(task)) XCTAssertTrue(center.tasks(forTag: tag).contains(task))
XCTAssertTrue(center.tagsForTask(task).contains(tag)) XCTAssertTrue(center.tags(forTask: task).contains(tag))
center.removeTag(tag, from: task) center.removeTag(tag, from: task)
XCTAssertFalse(center.tasksForTag(tag).contains(task)) XCTAssertFalse(center.tasks(forTag: tag).contains(task))
XCTAssertFalse(center.tagsForTask(task).contains(tag)) XCTAssertFalse(center.tags(forTask: task).contains(tag))
center.removeAll() center.removeAll()
} }
func testAll() { func testAll() {
let task = makeTask() 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.sorted(), [tag1, tag2].sorted())
XCTAssertEqual(center.allTags, [tag])
XCTAssertEqual(center.allTasks, [task]) XCTAssertEqual(center.allTasks, [task])
center.removeAll() center.removeAll()
@ -70,18 +78,17 @@ final class TaskCenterTests: XCTestCase {
func testOperation() { func testOperation() {
let task = makeTask() let task = makeTask()
let tag = UUID().uuidString let tag = UUID().uuidString
center.addTag(tag, to: task) center.addTag(tag, to: task)
center.suspendByTag(tag) center.suspend(byTag: tag)
XCTAssertEqual(task.suspensions, 1) XCTAssertEqual(task.suspensionCount, 1)
center.resumeByTag(tag) center.resume(byTag: tag)
XCTAssertEqual(task.suspensions, 0) XCTAssertEqual(task.suspensionCount, 0)
center.cancelByTag(tag) center.cancel(byTag: tag)
XCTAssertTrue(task.isCancelled) XCTAssertTrue(task.isCancelled)
center.removeAll() center.removeAll()
@ -89,7 +96,9 @@ final class TaskCenterTests: XCTestCase {
func testWeak() { func testWeak() {
let block = { let block = {
_ = self.makeTask() let task = self.makeTask()
XCTAssertEqual(self.center.allTasks.count, 1)
_ = task
} }
block() block()

View File

@ -2,46 +2,84 @@ import XCTest
@testable import Schedule @testable import Schedule
final class TaskTests: XCTestCase { 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() { func testAfter() {
let e = expectation(description: "testSchedule") let e = expectation(description: "testSchedule")
let date = Date() let date = Date()
let task = Plan.after(0.1.second).do { let task = Plan.after(0.01.second).do(queue: .global()) {
XCTAssertTrue(Date().timeIntervalSince(date).isAlmostEqual(to: 0.1, leeway: 0.1)) XCTAssertTrue(Date().interval(since: date).isAlmostEqual(to: 0.01.second, leeway: self.leeway))
e.fulfill() e.fulfill()
} }
waitForExpectations(timeout: 0.5) waitForExpectations(timeout: 0.1)
task.cancel()
_ = task
} }
func testRepeat() { func testRepeat() {
let e = expectation(description: "testRepeat") let e = expectation(description: "testRepeat")
var t = 0 var count = 0
let task = Plan.every(0.1.second).first(3).do { let task = Plan.every(0.01.second).first(3).do(queue: .global()) {
t += 1 count += 1
if t == 3 { e.fulfill() } if count == 3 { e.fulfill() }
} }
waitForExpectations(timeout: 1) waitForExpectations(timeout: 0.1)
task.cancel()
_ = 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() { func testDispatchQueue() {
let e = expectation(description: "testQueue") 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) { let task = Plan.after(0.01.second).do(queue: q) {
XCTAssertTrue(DispatchQueue.is(queue)) XCTAssertTrue(DispatchQueue.is(q))
e.fulfill() e.fulfill()
} }
waitForExpectations(timeout: 0.5) waitForExpectations(timeout: 0.1)
task.cancel()
_ = task
} }
func testThread() { func testThread() {
let e = expectation(description: "testThread") let e = expectation(description: "testThread")
DispatchQueue.global().async { DispatchQueue.global().async {
let thread = Thread.current 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) XCTAssertTrue(thread === Thread.current)
e.fulfill() e.fulfill()
task.cancel() task.cancel()
@ -49,20 +87,70 @@ final class TaskTests: XCTestCase {
_ = task _ = task
RunLoop.current.run() RunLoop.current.run()
} }
waitForExpectations(timeout: 0.5) waitForExpectations(timeout: 0.1)
} }
func testSuspendResume() { func testSuspendResume() {
let task1 = Plan.distantFuture.do { } let task = Plan.never.do { }
XCTAssertEqual(task1.suspensions, 0) XCTAssertEqual(task.suspensionCount, 0)
task1.suspend() task.suspend()
task1.suspend() task.suspend()
task1.suspend() task.suspend()
XCTAssertEqual(task1.suspensions, 3) XCTAssertEqual(task.suspensionCount, 3)
task1.resume() task.resume()
XCTAssertEqual(task1.suspensions, 2) 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() { func testAddAndRemoveActions() {
let e = expectation(description: "testAddAndRemoveActions") let e = expectation(description: "testAddAndRemoveActions")
let task = Plan.after(0.1.second).do { } let task = Plan.after(0.1.second).do { }
@ -83,69 +171,17 @@ final class TaskTests: XCTestCase {
XCTAssertEqual(task.countOfActions, 0) 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 = [ static var allTests = [
("testAfter", testAfter), ("testAfter", testAfter),
("testRepeat", testRepeat), ("testRepeat", testRepeat),
("testTaskCenter", testTaskCenter),
("testDispatchQueue", testDispatchQueue), ("testDispatchQueue", testDispatchQueue),
("testThread", testThread), ("testThread", testThread),
("testAddAndRemoveActions", testAddAndRemoveActions), ("testSuspendResume", testSuspendResume),
("testReschedule", testReschedule), ("testCancel", testCancel),
("testExecuteNow", testExecuteNow),
("testHost", testHost), ("testHost", testHost),
("testLifetime", testLifetime) ("testReschedule", testReschedule),
("testAddAndRemoveActions", testAddAndRemoveActions)
] ]
} }

View File

@ -2,7 +2,7 @@ import XCTest
@testable import Schedule @testable import Schedule
final class TimeTests: XCTestCase { final class TimeTests: XCTestCase {
func testTime() { func testTime() {
let t1 = Time("11:12:13.456") let t1 = Time("11:12:13.456")
XCTAssertNotNil(t1) XCTAssertNotNil(t1)
@ -24,11 +24,11 @@ final class TimeTests: XCTestCase {
let t4 = Time("schedule") let t4 = Time("schedule")
XCTAssertNil(t4) XCTAssertNil(t4)
} }
func testIntervalSinceStartOfDay() { func testIntervalSinceStartOfDay() {
XCTAssertEqual(Time(hour: 1)!.intervalSinceStartOfDay, 1.hour) XCTAssertEqual(Time(hour: 1)!.intervalSinceStartOfDay, 1.hour)
} }
func testAsDateComponents() { func testAsDateComponents() {
let time = Time(hour: 11, minute: 12, second: 13, nanosecond: 456) let time = Time(hour: 11, minute: 12, second: 13, nanosecond: 456)
let components = time?.asDateComponents() let components = time?.asDateComponents()
@ -37,7 +37,7 @@ final class TimeTests: XCTestCase {
XCTAssertEqual(components?.second, 13) XCTAssertEqual(components?.second, 13)
XCTAssertEqual(components?.nanosecond, 456) XCTAssertEqual(components?.nanosecond, 456)
} }
func testDescription() { func testDescription() {
let time = Time("11:12:13.456") let time = Time("11:12:13.456")
XCTAssertEqual(time!.debugDescription, "Time: 11:12:13.456") XCTAssertEqual(time!.debugDescription, "Time: 11:12:13.456")

View File

@ -2,22 +2,22 @@ import XCTest
@testable import Schedule @testable import Schedule
final class WeekdayTests: XCTestCase { final class WeekdayTests: XCTestCase {
func testIs() { func testIs() {
// ! Be careful the time zone problem. // ! Be careful the time zone problem.
let d = Date(year: 2019, month: 1, day: 1) let d = Date(year: 2019, month: 1, day: 1)
XCTAssertTrue(d.is(.tuesday)) XCTAssertTrue(d.is(.tuesday, in: TimeZone.shanghai))
} }
func testAsDateComponents() { func testAsDateComponents() {
XCTAssertEqual(Weekday.monday.asDateComponents().weekday!, 2) XCTAssertEqual(Weekday.monday.asDateComponents().weekday!, 2)
} }
func testDescription() { func testDescription() {
let wd = Weekday.tuesday let wd = Weekday.tuesday
XCTAssertEqual(wd.debugDescription, "Weekday: Tuesday") XCTAssertEqual(wd.debugDescription, "Weekday: Tuesday")
} }
static var allTests = [ static var allTests = [
("testIs", testIs), ("testIs", testIs),
("testAsDateComponents", testAsDateComponents), ("testAsDateComponents", testAsDateComponents),