diff --git a/.swiftlint.yml b/.swiftlint.yml index 07b97ed..c1dfb8c 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -7,3 +7,4 @@ disabled_rules: - file_length - function_body_length - identifier_name + - type_name \ No newline at end of file diff --git a/Schedule.xcodeproj/project.pbxproj b/Schedule.xcodeproj/project.pbxproj index 7843043..ad1887e 100644 --- a/Schedule.xcodeproj/project.pbxproj +++ b/Schedule.xcodeproj/project.pbxproj @@ -37,7 +37,6 @@ OBJ_59 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_19 /* Task.swift */; }; OBJ_60 /* TaskCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_20 /* TaskCenter.swift */; }; OBJ_61 /* Time.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_21 /* Time.swift */; }; - OBJ_62 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_22 /* Timeline.swift */; }; OBJ_63 /* Weekday.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_23 /* Weekday.swift */; }; OBJ_70 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; }; OBJ_81 /* AtomicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_26 /* AtomicTests.swift */; }; @@ -86,7 +85,6 @@ OBJ_19 /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; }; OBJ_20 /* TaskCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskCenter.swift; sourceTree = ""; }; OBJ_21 /* Time.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Time.swift; sourceTree = ""; }; - OBJ_22 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = ""; }; OBJ_23 /* Weekday.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weekday.swift; sourceTree = ""; }; OBJ_26 /* AtomicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicTests.swift; sourceTree = ""; }; OBJ_27 /* BagTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BagTests.swift; sourceTree = ""; }; @@ -205,7 +203,6 @@ OBJ_19 /* Task.swift */, OBJ_20 /* TaskCenter.swift */, OBJ_21 /* Time.swift */, - OBJ_22 /* Timeline.swift */, OBJ_23 /* Weekday.swift */, ); name = Schedule; @@ -309,7 +306,6 @@ OBJ_59 /* Task.swift in Sources */, OBJ_60 /* TaskCenter.swift in Sources */, OBJ_61 /* Time.swift in Sources */, - OBJ_62 /* Timeline.swift in Sources */, OBJ_63 /* Weekday.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Sources/Schedule/Atomic.swift b/Sources/Schedule/Atomic.swift index 43437af..7565199 100644 --- a/Sources/Schedule/Atomic.swift +++ b/Sources/Schedule/Atomic.swift @@ -17,7 +17,7 @@ final class Atomic { func read(_ body: (T) -> U) -> U { return lock.withLock { body(val) } } - + /// Reads the current value atomically. @inline(__always) func readVoid(_ body: (T) -> Void) { @@ -29,7 +29,7 @@ final class Atomic { func write(_ body: (inout T) -> U) -> U { return lock.withLock { body(&val) } } - + /// Writes the current value atomically. @inline(__always) func writeVoid(_ body: (inout T) -> Void) { diff --git a/Sources/Schedule/Bag.swift b/Sources/Schedule/Bag.swift index 5b6c0c4..3afac57 100644 --- a/Sources/Schedule/Bag.swift +++ b/Sources/Schedule/Bag.swift @@ -28,9 +28,7 @@ struct BagKeyGenerator: Sequence, IteratorProtocol { /// Advances to the next key and returns it, or nil if no next key exists. mutating func next() -> BagKey? { - if k.i == UInt64.max { - return nil - } + if k.i == UInt64.max { return nil } defer { k = BagKey(underlying: k.i + 1) } return k } diff --git a/Sources/Schedule/DeinitObserver.swift b/Sources/Schedule/DeinitObserver.swift index ae36aeb..2821c57 100644 --- a/Sources/Schedule/DeinitObserver.swift +++ b/Sources/Schedule/DeinitObserver.swift @@ -10,9 +10,9 @@ import Foundation /// /// observer.cancel() class DeinitObserver { - + private var associateKey: Void = () - + private(set) weak var observed: AnyObject? private var block: (() -> Void)? diff --git a/Sources/Schedule/Extensions.swift b/Sources/Schedule/Extensions.swift index 114d8fa..b99b1a8 100644 --- a/Sources/Schedule/Extensions.swift +++ b/Sources/Schedule/Extensions.swift @@ -20,7 +20,7 @@ extension Int { } extension Locale { - + static let posix = Locale(identifier: "en_US_POSIX") } diff --git a/Sources/Schedule/Interval.swift b/Sources/Schedule/Interval.swift index 23764c4..417b331 100644 --- a/Sources/Schedule/Interval.swift +++ b/Sources/Schedule/Interval.swift @@ -13,7 +13,7 @@ public struct Interval { } extension Interval: Hashable { - + /// Returns a boolean value indicating whether two intervals are equal. public static func == (lhs: Interval, rhs: Interval) -> Bool { return lhs.nanoseconds == rhs.nanoseconds @@ -72,7 +72,7 @@ extension Interval: Comparable { /// A positive interval is always ordered ascending to a negative interval. public func compare(_ other: Interval) -> ComparisonResult { let d = nanoseconds - other.nanoseconds - + if d < 0 { return .orderedAscending } if d > 0 { return .orderedDescending } return .orderedSame @@ -102,7 +102,7 @@ extension Interval: Comparable { // MARK: - Adding & Subtracting extension Interval { - + /// Returns a new interval by multipling this interval by the given number. /// /// 1.hour * 2 == 2.hours @@ -331,7 +331,7 @@ extension Date { extension DispatchSourceTimer { - /// Schedule this timer later. + /// Schedule this timer after the given interval. func schedule(after timeout: Interval) { if timeout.isNegative { return } let ns = timeout.nanoseconds.clampedToInt() diff --git a/Sources/Schedule/Monthday.swift b/Sources/Schedule/Monthday.swift index 5a1b22a..5bc29d5 100644 --- a/Sources/Schedule/Monthday.swift +++ b/Sources/Schedule/Monthday.swift @@ -29,7 +29,7 @@ public enum Monthday { /// Returns a dateComponenets of this monthday, using gregorian calender and /// current time zone. - public func asDateComponents() -> DateComponents { + public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents { var month, day: Int switch self { case .january(let n): month = 1; day = n @@ -47,7 +47,7 @@ public enum Monthday { } return DateComponents( calendar: Calendar.gregorian, - timeZone: TimeZone.current, + timeZone: timeZone, month: month, day: day) } @@ -56,8 +56,8 @@ public enum Monthday { extension Date { /// Returns a Boolean value indicating whether this date is the monthday in current time zone.. - public func `is`(_ monthday: Monthday) -> Bool { - let components = monthday.asDateComponents() + public func `is`(_ monthday: Monthday, in timeZone: TimeZone = .current) -> Bool { + let components = monthday.asDateComponents(timeZone) let m = Calendar.gregorian.component(.month, from: self) let d = Calendar.gregorian.component(.day, from: self) diff --git a/Sources/Schedule/Period.swift b/Sources/Schedule/Period.swift index 79aae79..f2836e3 100644 --- a/Sources/Schedule/Period.swift +++ b/Sources/Schedule/Period.swift @@ -1,6 +1,6 @@ import Foundation -/// Type used to represents a date-based amount of time in the ISO-8601 calendar system, +/// Type used to represent a date-based amount of time in the ISO-8601 calendar system, /// such as '2 years, 3 months and 4 days'. /// /// It's a little different from `Interval`: @@ -65,18 +65,26 @@ public struct Period { for (word, number) in Period.quantifiers.read({ $0 }) { str = str.replacingOccurrences(of: word, with: "\(number)") } + + // swiftlint:disable force_try let regexp = try! NSRegularExpression(pattern: "( and |, )") - str = regexp.stringByReplacingMatches(in: str, range: NSRange(location: 0, length: str.count), withTemplate: "$") + + let mark: Character = "秋" + str = regexp.stringByReplacingMatches( + in: str, + range: NSRange(location: 0, length: str.count), + withTemplate: String(mark) + ) var period = 0.year - for pair in str.split(separator: "$").map({ $0.split(separator: " ") }) { + for pair in str.split(separator: mark).map({ $0.split(separator: " ") }) { guard pair.count == 2, let number = Int(pair[0]) else { return nil } - + var unit = String(pair[1]) if unit.last == "s" { unit.removeLast() } switch unit { @@ -141,27 +149,27 @@ public struct Period { /// Period(hours: 25).tidied(to .day) => Period(days: 1, hours: 1) public func tidied(to level: TideLevel) -> Period { var period = self - + if case .nanosecond = level { return period } - + if period.nanoseconds.magnitude >= UInt(1.second.nanoseconds) { period.seconds += period.nanoseconds / Int(1.second.nanoseconds) period.nanoseconds %= Int(1.second.nanoseconds) } if case .second = level { return period } - + if period.seconds.magnitude >= 60 { period.minutes += period.seconds / 60 period.seconds %= 60 } if case .minute = level { return period } - + if period.minutes.magnitude >= 60 { period.hours += period.minutes / 60 period.minutes %= 60 } if case .hour = level { return period } - + if period.hours.magnitude >= 24 { period.days += period.hours / 24 period.hours %= 24 @@ -171,10 +179,18 @@ public struct Period { /// Returns a dateComponenets of this period, using gregorian calender and /// current time zone. - public func asDateComponents() -> DateComponents { - return DateComponents(year: years, month: months, day: days, - hour: hours, minute: minutes, second: seconds, - nanosecond: nanoseconds) + public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents { + return DateComponents( + calendar: Calendar.gregorian, + timeZone: timeZone, + year: years, + month: months, + day: days, + hour: hours, + minute: minutes, + second: seconds, + nanosecond: nanoseconds + ) } } diff --git a/Sources/Schedule/Plan.swift b/Sources/Schedule/Plan.swift index 2a12fc0..e01c162 100644 --- a/Sources/Schedule/Plan.swift +++ b/Sources/Schedule/Plan.swift @@ -1,50 +1,54 @@ import Foundation -/// `Plan` represents a plan that gives time at which a task should be +/// `Plan` represents a sequence of times at which a task should be /// executed. /// /// `Plan` is `Interval` based. -public struct Plan { +public struct Plan: Sequence { - private var iSeq: AnySequence + private var seq: AnySequence private init(_ sequence: S) where S: Sequence, S.Element == Interval { - iSeq = AnySequence(sequence) + seq = AnySequence(sequence) } - func makeIterator() -> AnyIterator { - return iSeq.makeIterator() + /// Returns an iterator over the interval of this sequence. + public func makeIterator() -> AnyIterator { + return seq.makeIterator() } /// Schedules a task with this plan. /// /// - Parameters: - /// - queue: The queue to which the task will be dispatched. - /// - onElapse: The action to do when time is out. + /// - queue: The dispatch queue to which the block should be dispatched. + /// - block: A block to be executed when time is up. /// - Returns: The task just created. - public func `do`(queue: DispatchQueue, - onElapse: @escaping (Task) -> Void) -> Task { - return Task(plan: self, queue: queue, onElapse: onElapse) + public func `do`( + queue: DispatchQueue, + block: @escaping (Task) -> Void + ) -> Task { + return Task(plan: self, queue: queue, block: block) } /// Schedules a task with this plan. /// /// - Parameters: - /// - queue: The queue to which the task will be dispatched. - /// - onElapse: The action to do when time is out. + /// - queue: The dispatch queue to which the block should be dispatched. + /// - block: A block to be executed when time is up. /// - Returns: The task just created. - public func `do`(queue: DispatchQueue, - onElapse: @escaping () -> Void) -> Task { - return self.do(queue: queue, onElapse: { (_) in onElapse() }) + public func `do`( + queue: DispatchQueue, + block: @escaping () -> Void + ) -> Task { + return self.do(queue: queue, block: { (_) in block() }) } } extension Plan { - /// Creates a plan from a `makeUnderlyingIterator()` method. + /// Creates a plan whose `makeIterator()` method forwards to makeUnderlyingIterator. /// - /// The task will be executed after each interval produced by the iterator - /// that `makeUnderlyingIterator` returns. + /// The task will be executed after each interval. /// /// For example: /// @@ -52,11 +56,11 @@ extension Plan { /// var i = 0 /// return AnyIterator { /// i += 1 - /// return i + /// return i // 1, 2, 3, ... /// } /// } /// plan.do { - /// print(Date()) + /// logTimestamp() /// } /// /// > "2001-01-01 00:00:00" @@ -71,15 +75,15 @@ extension Plan { } /// Creates a plan from a list of intervals. + /// /// The task will be executed after each interval in the array. - /// - Note: Returns `Plan.never` if given no parameters. public static func of(_ intervals: Interval...) -> Plan { return Plan.of(intervals) } /// Creates a plan from a list of intervals. + /// /// The task will be executed after each interval in the array. - /// - Note: Returns `Plan.never` if given an empty array. public static func of(_ intervals: S) -> Plan where S: Sequence, S.Element == Interval { return Plan(intervals) } @@ -87,10 +91,9 @@ extension Plan { extension Plan { - /// Creates a plan from a `makeUnderlyingIterator()` method. + /// Creates a plan whose `makeIterator()` method forwards to makeUnderlyingIterator. /// - /// The task will be executed at each date - /// produced by the iterator that `makeUnderlyingIterator` returns. + /// The task will be executed at each date. /// /// For example: /// @@ -99,42 +102,44 @@ extension Plan { /// return Date().addingTimeInterval(3) /// } /// } - /// print("now:", Date()) + /// /// plan.do { - /// print("task", Date()) + /// logTimestamp() /// } /// - /// > "now: 2001-01-01 00:00:00" - /// > "task: 2001-01-01 00:00:03" + /// > "2001-01-01 00:00:00" + /// > "2001-01-01 00:00:03" + /// > "2001-01-01 00:00:06" + /// > "2001-01-01 00:00:09" /// ... /// - /// You are not supposed to return `Date()` in making interator. - /// If you want to execute a task immediately, - /// use `Plan.now` then `concat` another plan instead. + /// You should not return `Date()` in making iterator. + /// If you want to execute a task immediately, use `Plan.now`. public static func make( _ makeUnderlyingIterator: @escaping () -> I ) -> Plan where I: IteratorProtocol, I.Element == Date { return Plan.make { () -> AnyIterator in var iterator = makeUnderlyingIterator() - var last: Date! + var prev: Date! return AnyIterator { - last = last ?? Date() + prev = prev ?? Date() guard let next = iterator.next() else { return nil } - defer { last = next } - return next.interval(since: last) + defer { prev = next } + return next.interval(since: prev) } } } /// Creates a plan from a list of dates. + /// /// The task will be executed at each date in the array. public static func of(_ dates: Date...) -> Plan { return Plan.of(dates) } /// Creates a plan from a list of dates. + /// /// The task will be executed at each date in the array. - /// - Note: Returns `Plan.never` if given no parameters. public static func of(_ sequence: S) -> Plan where S: Sequence, S.Element == Date { return Plan.make(sequence.makeIterator) } @@ -143,13 +148,13 @@ extension Plan { public var dates: AnySequence { return AnySequence { () -> AnyIterator in let iterator = self.makeIterator() - var last: Date! + var prev: Date! return AnyIterator { - last = last ?? Date() + prev = prev ?? Date() guard let interval = iterator.next() else { return nil } // swiftlint:disable shorthand_operator - last = last + interval - return last + prev = prev + interval + return prev } } } @@ -157,27 +162,27 @@ extension Plan { extension Plan { - /// A plan with a distant past date. + /// A plan of a distant past date. public static var distantPast: Plan { return Plan.of(Date.distantPast) } - /// A plan with a distant future date. + /// A plan of a distant future date. public static var distantFuture: Plan { return Plan.of(Date.distantFuture) } - /// A plan that is never going to happen. + /// A plan that will never happen. public static var never: Plan { return Plan.make { - AnyIterator { nil } + AnyIterator { nil } } } } extension Plan { - /// Returns a new plan by concatenating a plan to this plan. + /// Returns a new plan by concatenating the given plan to this plan. /// /// For example: /// @@ -198,40 +203,44 @@ extension Plan { } } - /// Returns a new plan by merging a plan to this plan. + /// Returns a new plan by merging the given plan to this plan. /// /// For example: /// /// let s0 = Plan.of(1.second, 3.seconds, 5.seconds) /// let s1 = Plan.of(2.seconds, 4.seconds, 6.seconds) - /// let s2 = s0.concat(s1) + /// let s2 = s0.merge(s1) /// > s2 /// > 1.second, 1.seconds, 2.seconds, 2.seconds, 3.seconds, 3.seconds public func merge(_ plan: Plan) -> Plan { return Plan.make { () -> AnyIterator in let i0 = self.dates.makeIterator() let i1 = plan.dates.makeIterator() - var buffer0: Date! - var buffer1: Date! + + var buf0: Date! + var buf1: Date! + return AnyIterator { - if buffer0 == nil { buffer0 = i0.next() } - if buffer1 == nil { buffer1 = i1.next() } + if buf0 == nil { buf0 = i0.next() } + if buf1 == nil { buf1 = i1.next() } var d: Date! - if let d0 = buffer0, let d1 = buffer1 { + if let d0 = buf0, let d1 = buf1 { d = Swift.min(d0, d1) } else { - d = buffer0 ?? buffer1 + d = buf0 ?? buf1 } - if d == buffer0 { buffer0 = nil } - if d == buffer1 { buffer1 = nil } + if d == nil { return d } + + if d == buf0 { buf0 = nil; return d } + if d == buf1 { buf1 = nil } return d } } } - /// Returns a new plan by only taking the first specific number of this plan. + /// Returns a new plan by taking the first specific number of intervals from this plan. /// /// For example: /// @@ -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 { return Plan.make { () -> AnyIterator in let iterator = self.dates.makeIterator() return AnyIterator { guard let next = iterator.next(), next < date else { - return nil + return nil } return next } } } -} - -extension Plan { /// Creates a plan that executes the task immediately. public static var now: Plan { return Plan.of(0.nanosecond) } - /// Creates a plan that executes the task after delay. + /// Creates a plan that executes the task after the given interval. public static func after(_ delay: Interval) -> Plan { return Plan.of(delay) } + + /// Creates a plan that executes the task 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 { return Plan.make { AnyIterator { interval } } } - /// Creates a plan that executes the task after delay then repeat - /// every interval. - public static func after(_ delay: Interval, repeating interval: Interval) -> Plan { - return Plan.after(delay).concat(Plan.every(interval)) - } - - /// Creates a plan that executes the task at the specific date. - public static func at(_ date: Date) -> Plan { - return Plan.of(date) - } - - /// Creates a plan that executes the task every period. + /// Creates a plan that executes the task every given period. public static func every(_ period: Period) -> Plan { return Plan.make { () -> AnyIterator in let calendar = Calendar.gregorian - var last: Date! + var prev: Date! return AnyIterator { - last = last ?? Date() - guard let next = calendar.date(byAdding: period.asDateComponents(), - to: last) else { + prev = prev ?? Date() + guard + let next = calendar.date( + byAdding: period.asDateComponents(), + to: prev) + else { return nil } - defer { last = next } - return next.interval(since: last) + defer { prev = next } + return next.interval(since: prev) } } } /// Creates a plan that executes the task every period. /// - /// See Period's constructor + /// See Period's constructor: `init?(_ string: String)`. public static func every(_ period: String) -> Plan { guard let p = Period(period) else { return Plan.never @@ -326,16 +334,16 @@ extension Plan { extension Plan { /// `DateMiddleware` represents a middleware that wraps a plan - /// which was only specified date without time. + /// which was only specified with date without time. /// - /// You should call `at` method to get the plan with time specified. + /// You should call `at` method to specified time of the plan. public struct DateMiddleware { fileprivate let plan: Plan - /// Returns a plan at the specific time. + /// Creates a plan with time specified. public func at(_ time: Time) -> Plan { - guard !self.plan.isNever() else { return .never } + if plan.isNever() { return .never } var interval = time.intervalSinceStartOfDay return Plan.make { () -> AnyIterator in @@ -350,56 +358,54 @@ extension Plan { } } - /// Returns a plan at the specific time. + /// Creates a plan with time specified. /// - /// See Time's constructor + /// See Time's constructor: `init?(_ string: String)`. public func at(_ time: String) -> Plan { - guard - !self.plan.isNever(), - let time = Time(time) - else { + if plan.isNever() { return .never } + guard let time = Time(time) else { return .never } - return at(time) } - /// Returns a plan at the specific time. + /// Creates a plan with time specified. /// /// .at(1) => 01 /// .at(1, 2) => 01:02 /// .at(1, 2, 3) => 01:02:03 /// .at(1, 2, 3, 456) => 01:02:03.456 - /// - /// - Note: Returns `Plan.never` if given no parameters. public func at(_ time: Int...) -> Plan { return self.at(time) } - /// Returns a plan at the specific time. + /// Creates a plan with time specified. /// /// .at([1]) => 01 /// .at([1, 2]) => 01:02 /// .at([1, 2, 3]) => 01:02:03 /// .at([1, 2, 3, 456]) => 01:02:03.456 - /// - /// - Note: Returns `Plan.never` if given an empty array. public func at(_ time: [Int]) -> Plan { - guard !time.isEmpty, !self.plan.isNever() else { return .never } + if plan.isNever() || time.isEmpty { return .never } let hour = time[0] let minute = time.count > 1 ? time[1] : 0 let second = time.count > 2 ? time[2] : 0 let nanosecond = time.count > 3 ? time[3]: 0 - guard let time = Time(hour: hour, minute: minute, second: second, nanosecond: nanosecond) else { + guard let time = Time( + hour: hour, + minute: minute, + second: second, + nanosecond: nanosecond + ) else { return Plan.never } return at(time) } } - /// Creates a plan that executes the task every specific weekday. + /// Creates a date middleware that executes the task on every specific week day. public static func every(_ weekday: Weekday) -> DateMiddleware { let plan = Plan.make { () -> AnyIterator in let calendar = Calendar.gregorian @@ -419,14 +425,12 @@ extension Plan { return DateMiddleware(plan: plan) } - /// Creates a plan that executes the task every specific weekdays. - /// - Note: Returns initialized with `Plan.never` if given no parameters. + /// Creates a date middleware that executes the task on every specific week day. public static func every(_ weekdays: Weekday...) -> DateMiddleware { return Plan.every(weekdays) } - /// Creates a plan that executes the task every specific weekdays. - /// - Note: Returns initialized with `Plan.never` if given an empty array. + /// Creates a date middleware that executes the task on every specific week day. public static func every(_ weekdays: [Weekday]) -> DateMiddleware { guard !weekdays.isEmpty else { return .init(plan: .never) } @@ -439,7 +443,7 @@ extension Plan { return DateMiddleware(plan: plan) } - /// Creates a plan that executes the task every specific day in the month. + /// Creates a date middleware that executes the task on every specific month day. public static func every(_ monthday: Monthday) -> DateMiddleware { let plan = Plan.make { () -> AnyIterator in let calendar = Calendar.gregorian @@ -459,14 +463,12 @@ extension Plan { return DateMiddleware(plan: plan) } - /// Creates a plan that executes the task every specific days in the months. - /// - Note: Returns initialized with `Plan.never` if given no parameters. + /// Creates a date middleware that executes the task on every specific month day. public static func every(_ mondays: Monthday...) -> DateMiddleware { return Plan.every(mondays) } - /// Creates a plan that executes the task every specific days in the months. - /// - Note: Returns initialized with `Plan.never` if given an empty array. + /// Creates a date middleware that executes the task on every specific month day. public static func every(_ mondays: [Monthday]) -> DateMiddleware { guard !mondays.isEmpty else { return .init(plan: .never) } @@ -484,11 +486,12 @@ extension Plan { /// Returns a Boolean value indicating whether this plan is empty. public func isNever() -> Bool { - return iSeq.makeIterator().next() == nil + return seq.makeIterator().next() == nil } } extension Plan { + /// Creates a new plan that is offset by the specified interval in the /// closure body. /// @@ -497,23 +500,15 @@ extension Plan { /// /// If the returned interval offset is `nil`, then no offset is added /// to that next-run date. - public func offset(by intervalOffset: @escaping () -> Interval?) -> Plan { + public func offset(by interval: @autoclosure @escaping () -> Interval?) -> Plan { return Plan.make { () -> AnyIterator in let it = self.makeIterator() return AnyIterator { if let next = it.next() { - return next + (intervalOffset() ?? 0.second) + return next + (interval() ?? 0.second) } return nil } } } - - /// Creates a new plan that is offset by the specified interval. - /// - /// If the specified interval offset is `nil`, then no offset is - /// added to the plan (ie. it stays the same). - public func offset(by intervalOffset: Interval?) -> Plan { - return self.offset(by: { intervalOffset }) - } } diff --git a/Sources/Schedule/RunLoopTask.swift b/Sources/Schedule/RunLoopTask.swift index d9c407a..108d024 100644 --- a/Sources/Schedule/RunLoopTask.swift +++ b/Sources/Schedule/RunLoopTask.swift @@ -5,38 +5,42 @@ extension Plan { /// Schedules a task with this plan. /// /// When time is up, the task will be executed on current thread. It behaves - /// like a `Timer`, so you have to make sure the current thread has a - /// runloop available. + /// like a `Timer`, so you need to make sure that the current thread has a + /// available runloop. /// - /// Since this method relies on run loop, it is recommended to use + /// Since this method relies on run loop, it is remove recommended to use /// `do(queue: _, onElapse: _)`. /// /// - Parameters: - /// - mode: The mode in which to add the task. - /// - onElapse: The action to do when time is out. + /// - mode: The mode to which the block should be added. + /// - block: A block to be executed when time is up. /// - Returns: The task just created. - public func `do`(mode: RunLoop.Mode = .common, - onElapse: @escaping (Task) -> Void) -> Task { - return RunLoopTask(plan: self, mode: mode, onElapse: onElapse) + public func `do`( + mode: RunLoop.Mode = .common, + block: @escaping (Task) -> Void + ) -> Task { + return RunLoopTask(plan: self, mode: mode, block: block) } /// Schedules a task with this plan. /// /// When time is up, the task will be executed on current thread. It behaves - /// like a `Timer`, so you have to make sure the current thread has a - /// runloop available. + /// like a `Timer`, so you need to make sure that the current thread has a + /// available runloop. /// - /// Since this method relies on run loop, it is recommended to use + /// Since this method relies on run loop, it is remove recommended to use /// `do(queue: _, onElapse: _)`. /// /// - Parameters: - /// - mode: The mode in which to add the task. - /// - onElapse: The action to do when time is out. + /// - mode: The mode to which the block should be added. + /// - block: A block to be executed when time is up. /// - Returns: The task just created. - public func `do`(mode: RunLoop.Mode = .common, - onElapse: @escaping () -> Void) -> Task { - return self.do(mode: mode) { (_) in - onElapse() + public func `do`( + mode: RunLoop.Mode = .common, + block: @escaping () -> Void + ) -> Task { + return self.do(mode: mode) { _ in + block() } } } @@ -45,24 +49,26 @@ private final class RunLoopTask: Task { var timer: Timer! - init(plan: Plan, mode: RunLoop.Mode, onElapse: @escaping (Task) -> Void) { + init( + plan: Plan, + mode: RunLoop.Mode, + block: @escaping (Task) -> Void + ) { + super.init(plan: plan, queue: nil) { (task) in + guard let task = task as? RunLoopTask, let timer = task.timer else { return } + timer.fireDate = Date() + } - weak var this: Task? - - let distant = Date.distantFuture.timeIntervalSinceReferenceDate - timer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, distant, distant, 0, 0) { _ in - guard let task = this else { return } - onElapse(task) + timer = Timer( + fire: Date.distantFuture, + interval: .greatestFiniteMagnitude, + repeats: false + ) { [weak self] _ in + guard let self = self else { return } + block(self) } RunLoop.current.add(timer, forMode: mode) - - super.init(plan: plan, queue: nil) { (task) in - guard let task = task as? RunLoopTask else { return } - task.timer.fireDate = Date() - } - - this = self } deinit { diff --git a/Sources/Schedule/Task.swift b/Sources/Schedule/Task.swift index c6df4fc..a803459 100644 --- a/Sources/Schedule/Task.swift +++ b/Sources/Schedule/Task.swift @@ -7,13 +7,13 @@ public struct ActionKey { } extension BagKey { - + fileprivate func asActionKey() -> ActionKey { return ActionKey(bagKey: self) } } -/// `Task` represents a timed task. +/// `Task` represents a timing task. open class Task { /// The unique id of this task. @@ -21,77 +21,86 @@ open class Task { public typealias Action = (Task) -> Void - private let _mutex = NSRecursiveLock() + private let _lock = NSRecursiveLock() private var _iterator: AnyIterator private var _timer: DispatchSourceTimer - private lazy var _onElapseActions = Bag() + private lazy var _actions = Bag() - private lazy var _suspensions: UInt64 = 0 - private lazy var _timeline = Timeline() - - 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? + private lazy var _suspensionCount: UInt64 = 0 + private lazy var _executionCount: Int = 0 - /// The task center which this task currently in. - open var taskCenter: TaskCenter? { - return _taskCenter + private var _firstExecutionDate: Date? + private var _lastExecutionDate: Date? + 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. - private let _taskCenterLock = NSRecursiveLock() + /// The date of last execution. + open var lastExecutionDate: Date? { + return _lock.withLock { _lastExecutionDate } + } + /// The date of estimated next execution. + open var estimatedNextExecutionDate: Date? { + return _lock.withLock { _estimatedNextExecutionDate } + } + + 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. - /// - /// If this task is already in a task center, it will be removed from that center first. func addToTaskCenter(_ center: TaskCenter) { _taskCenterLock.lock() defer { _taskCenterLock.unlock() } - + if _taskCenter === center { return } - - _taskCenter?.remove(self) + + let c = _taskCenter _taskCenter = center + + c?.remove(self) } - + /// Removes this task from the given task center. func removeFromTaskCenter(_ center: TaskCenter) { _taskCenterLock.lock() defer { _taskCenterLock.unlock() } - + if _taskCenter !== center { return } - - _taskCenter?.remove(self) + _taskCenter = nil + center.remove(self) } - /// Initializes a normal task with specified plan and dispatch queue. + /// Initializes a timing task. /// /// - Parameters: /// - plan: The plan. - /// - queue: The dispatch queue to which all actions should be added. - /// - onElapse: The action to do when time is out. - init(plan: Plan, - queue: DispatchQueue?, - onElapse: @escaping (Task) -> Void) { - + /// - queue: The dispatch queue to which the block should be dispatched. + /// - block: A block to be executed when time is up. + init( + plan: Plan, + queue: DispatchQueue?, + block: @escaping (Task) -> Void + ) { _iterator = plan.makeIterator() _timer = DispatchSource.makeTimerSource(queue: queue) - _onElapseActions.append(onElapse) + _actions.append(block) _timer.setEventHandler { [weak self] in guard let self = self else { return } @@ -100,244 +109,163 @@ open class Task { if let interval = _iterator.next(), !interval.isNegative { _timer.schedule(after: interval) - _timeline.estimatedNextExecution = Date().adding(interval) + _estimatedNextExecutionDate = Date().adding(interval) } - + _timer.resume() + TaskCenter.default.add(self) } deinit { - while _suspensions > 0 { + while _suspensionCount > 0 { _timer.resume() - _suspensions -= 1 + _suspensionCount -= 1 } cancel() - + taskCenter?.remove(self) } + + private func elapse() { + scheduleNextExecution() + execute() + } - private func scheduleNext() { - _mutex.withLockVoid { + private func scheduleNextExecution() { + _lock.withLockVoid { let now = Date() - var estimated = _timeline.estimatedNextExecution ?? now + var estimated = _estimatedNextExecutionDate ?? now repeat { guard let interval = _iterator.next(), !interval.isNegative else { - _timeline.estimatedNextExecution = nil + _estimatedNextExecutionDate = nil return } estimated = estimated.adding(interval) } while (estimated < now) - _timeline.estimatedNextExecution = estimated - _timer.schedule(after: _timeline.estimatedNextExecution!.interval(since: now)) + _estimatedNextExecutionDate = estimated + _timer.schedule(after: _estimatedNextExecutionDate!.interval(since: now)) } } - /// Execute this task now, without disrupting its plan. - public func execute() { - let actions = _mutex.withLock { () -> Bag in + /// Execute this task now, without interrupting its plan. + open func execute() { + let actions = _lock.withLock { () -> Bag in let now = Date() - if _timeline.firstExecution == nil { - _timeline.firstExecution = now + if _firstExecutionDate == nil { + _firstExecutionDate = now } - _timeline.lastExecution = now - _countOfExecutions += 1 - return _onElapseActions + _lastExecutionDate = now + _executionCount += 1 + return _actions } actions.forEach { $0(self) } } - private func elapse() { - scheduleNext() - execute() - } - + /// Host this task to an object, that is, when the object deallocates, this task will be cancelled. #if canImport(ObjectiveC) - open func host(on target: AnyObject) { + open func host(to target: AnyObject) { DeinitObserver.observe(target) { [weak self] in self?.cancel() } } #endif - /// The number of times the task has been executed. - public var countOfExecutions: Int { - return _mutex.withLock { - _countOfExecutions + /// The number of task executions. + public var executionCount: Int { + return _lock.withLock { + _executionCount } } /// A Boolean indicating whether the task was canceled. public var isCancelled: Bool { - return _mutex.withLock { + return _lock.withLock { _timer.isCancelled } } - // MARK: - Manage - /// Reschedules this task with the new plan. public func reschedule(_ new: Plan) { - _mutex.withLockVoid { + _lock.withLockVoid { _iterator = new.makeIterator() } - scheduleNext() + scheduleNextExecution() } - /// Suspensions of this task. - public var suspensions: UInt64 { - return _mutex.withLock { - _suspensions + /// The number of task suspensions. + public var suspensionCount: UInt64 { + return _lock.withLock { + _suspensionCount } } /// Suspends this task. public func suspend() { - _mutex.withLockVoid { - if _suspensions < UInt64.max { + _lock.withLockVoid { + if _suspensionCount < UInt64.max { _timer.suspend() - _suspensions += 1 + _suspensionCount += 1 } } } /// Resumes this task. public func resume() { - _mutex.withLockVoid { - if _suspensions > 0 { + _lock.withLockVoid { + if _suspensionCount > 0 { _timer.resume() - _suspensions -= 1 + _suspensionCount -= 1 } } } /// Cancels this task. public func cancel() { - _mutex.withLockVoid { + _lock.withLockVoid { _timer.cancel() } TaskCenter.default.remove(self) } - // MARK: - Lifecycle - - /// The snapshot timeline of this task. - public var timeline: Timeline { - return _mutex.withLock { - _timeline - } - } - - /// The lifetime of this task. - public var lifetime: Interval { - return _mutex.withLock { - _lifetime - } - } - - /// The rest of lifetime. - public var restOfLifetime: Interval { - return _mutex.withLock { - _lifetime - Date().interval(since: _timeline.initialization) - } - } - - /// Set a new lifetime for this task. - /// - /// If this task has already ended its lifetime, setting will fail, - /// if new lifetime is shorter than its age, setting will fail, too. - /// - /// - Returns: `true` if set successfully, `false` if not. - @discardableResult - public func setLifetime(_ interval: Interval) -> Bool { - if restOfLifetime.isNegative { - return false - } - - _mutex.lock() - let age = Date().interval(since: _timeline.initialization) - guard age.isShorter(than: interval) else { - _mutex.unlock() - return false - } - - _lifetime = interval - _lifetimeTimer.schedule(after: interval - age) - _mutex.unlock() - return true - } - - /// Add an interval to this task's lifetime. - /// - /// If this task has already ended its lifetime, adding will fail, - /// if new lifetime is shorter than its age, adding will fail, too. - /// - /// - Returns: `true` if set successfully, `false` if not. - @discardableResult - public func addLifetime(_ interval: Interval) -> Bool { - var rest = restOfLifetime - if rest.isNegative { return false } - rest += interval - if rest.isNegative { return false } - _mutex.withLockVoid { - _lifetime += interval - _lifetimeTimer.schedule(after: rest) - } - return true - } - - /// Subtract an interval to this task's lifetime. - /// - /// If this task has already ended its lifetime, subtracting will fail, - /// if new lifetime is shorter than its age, subtracting will fail, too. - /// - /// - Returns: `true` if set successfully, `false` if not. - @discardableResult - public func subtractLifetime(_ interval: Interval) -> Bool { - return addLifetime(interval.negated) - } // MARK: - Action /// The number of actions in this task. public var countOfActions: Int { - return _mutex.withLock { - _onElapseActions.count + return _lock.withLock { + _actions.count } } /// Adds action to this task. @discardableResult public func addAction(_ action: @escaping (Task) -> Void) -> ActionKey { - return _mutex.withLock { - return _onElapseActions.append(action).asActionKey() + return _lock.withLock { + return _actions.append(action).asActionKey() } } /// Removes action by key from this task. public func removeAction(byKey key: ActionKey) { - _mutex.withLockVoid { - _ = _onElapseActions.removeValue(for: key.bagKey) + _lock.withLockVoid { + _ = _actions.removeValue(for: key.bagKey) } } /// Removes all actions from this task. public func removeAllActions() { - _mutex.withLockVoid { - _onElapseActions.removeAll() + _lock.withLockVoid { + _actions.removeAll() } } - - // MARK: - Tag - open func add(to: TaskCenter) { - _mutex.lock() - } } extension Task: Hashable { + /// Hashes the essential components of this value by feeding them into the given hasher. public func hash(into hasher: inout Hasher) { hasher.combine(id) } @@ -347,25 +275,3 @@ extension Task: Hashable { return lhs === rhs } } - -extension Task: CustomStringConvertible { - - /// A textual representation of this task. - public var description: String { - return "Task: { " + - "\"isCancelled\": \(_timer.isCancelled), " + - "\"countOfElapseActions\": \(_onElapseActions.count), " + - "\"countOfExecutions\": \(_countOfExecutions), " + - "\"lifeTime\": \(_lifetime), " + - "\"timeline\": \(_timeline)" + - " }" - } -} - -extension Task: CustomDebugStringConvertible { - - /// A textual representation of this task for debugging. - public var debugDescription: String { - return description - } -} diff --git a/Sources/Schedule/TaskCenter.swift b/Sources/Schedule/TaskCenter.swift index 9ffc85a..7983684 100644 --- a/Sources/Schedule/TaskCenter.swift +++ b/Sources/Schedule/TaskCenter.swift @@ -30,8 +30,8 @@ open class TaskCenter { private let lock = NSLock() - private var tasksOfTag: [String: Set] = [:] - private var tagsOfTask: [TaskBox: Set] = [:] + private var tags: [String: Set] = [:] + private var tasks: [TaskBox: Set] = [:] /// Default task center. open class var `default`: TaskCenter { @@ -39,14 +39,12 @@ open class TaskCenter { } /// Adds the given task to this center. - /// - /// Center won't retain the task. open func add(_ task: Task) { task.addToTaskCenter(self) lock.withLockVoid { let box = TaskBox(task) - tagsOfTask[box] = [] + self.tasks[box] = [] } } @@ -56,15 +54,14 @@ open class TaskCenter { lock.withLockVoid { let box = TaskBox(task) - if let tags = self.tagsOfTask[box] { + if let tags = self.tasks[box] { for tag in tags { - self.tasksOfTag[tag]?.remove(box) - - if self.tasksOfTag[tag]?.count == 0 { - self.tasksOfTag[tag] = nil + self.tags[tag]?.remove(box) + if self.tags[tag]?.count == 0 { + self.tags[tag] = nil } } - self.tagsOfTask[box] = nil + self.tasks[box] = nil } } } @@ -84,15 +81,12 @@ open class TaskCenter { lock.withLockVoid { let box = TaskBox(task) - if tagsOfTask[box] == nil { - tagsOfTask[box] = [] - } for tag in tags { - tagsOfTask[box]?.insert(tag) - if tasksOfTag[tag] == nil { - tasksOfTag[tag] = [] + tasks[box]?.insert(tag) + if self.tags[tag] == nil { + self.tags[tag] = [] } - tasksOfTag[tag]?.insert(box) + self.tags[tag]?.insert(box) } } } @@ -113,64 +107,67 @@ open class TaskCenter { lock.withLockVoid { let box = TaskBox(task) for tag in tags { - tagsOfTask[box]?.remove(tag) - tasksOfTag[tag]?.remove(box) + self.tasks[box]?.remove(tag) + self.tags[tag]?.remove(box) + if self.tags[tag]?.count == 0 { + self.tags[tag] = nil + } } } } - /// Returns all tags on the task. + /// Returns all tags for the task. /// /// If the task is not in this center, return an empty array. - open func tagsForTask(_ task: Task) -> [String] { + open func tags(forTask task: Task) -> [String] { guard task.taskCenter === self else { return [] } return lock.withLock { - Array(tagsOfTask[TaskBox(task)] ?? []) + Array(tasks[TaskBox(task)] ?? []) } } - /// Returns all tasks that have the tag. - open func tasksForTag(_ tag: String) -> [Task] { + /// Returns all tasks for the tag. + open func tasks(forTag tag: String) -> [Task] { return lock.withLock { - tasksOfTag[tag]?.compactMap { $0.task } ?? [] + tags[tag]?.compactMap { $0.task } ?? [] } } /// Returns all tasks in this center. open var allTasks: [Task] { return lock.withLock { - tagsOfTask.compactMap { $0.key.task } + tasks.compactMap { $0.key.task } } } - /// Returns all existing tags in this center. + /// Returns all tags in this center. open var allTags: [String] { return lock.withLock { - tasksOfTag.map { $0.key } + tags.map { $0.key } } } /// Removes all tasks from this center. open func removeAll() { lock.withLockVoid { - tagsOfTask = [:] - tasksOfTag = [:] + tasks = [:] + tags = [:] } } - /// Suspends all tasks that have the tag. - open func suspendByTag(_ tag: String) { - tasksForTag(tag).forEach { $0.suspend() } + /// Suspends all tasks by tag. + open func suspend(byTag tag: String) { + tasks(forTag: tag).forEach { $0.suspend() } } - /// Resumes all tasks that have the tag. - open func resumeByTag(_ tag: String) { - tasksForTag(tag).forEach { $0.resume() } + /// Resumes all tasks by tag. + open func resume(byTag tag: String) { + tasks(forTag: tag).forEach { $0.resume() } } - /// Cancels all tasks that have the tag. - open func cancelByTag(_ tag: String) { - tasksForTag(tag).forEach { $0.cancel() } + /// Cancels all tasks by tag. + open func cancel(byTag tag: String) { + tasks(forTag: tag).forEach { $0.cancel() } } } diff --git a/Sources/Schedule/Time.swift b/Sources/Schedule/Time.swift index 40bb9c2..2268d55 100644 --- a/Sources/Schedule/Time.swift +++ b/Sources/Schedule/Time.swift @@ -48,8 +48,15 @@ public struct Time { public init?(_ string: String) { let pattern = "^(\\d{1,2})(:(\\d{1,2})(:(\\d{1,2})(.(\\d{1,3}))?)?)?( (am|AM|pm|PM))?$" + // swiftlint:disable force_try let regexp = try! NSRegularExpression(pattern: pattern, options: []) - guard let matches = regexp.matches(in: string, options: [], range: NSRange(location: 0, length: string.count)).first else { return nil } + guard let matches = regexp.matches( + in: string, + options: [], + range: NSRange(location: 0, length: string.count)).first + else { + return nil + } var hasAM = false var hasPM = false @@ -87,9 +94,9 @@ public struct Time { /// Returns a dateComponenets of the time, using gregorian calender and /// current time zone. - public func asDateComponents() -> DateComponents { + public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents { return DateComponents(calendar: Calendar.gregorian, - timeZone: TimeZone.current, + timeZone: timeZone, hour: hour, minute: minute, second: second, diff --git a/Sources/Schedule/Timeline.swift b/Sources/Schedule/Timeline.swift deleted file mode 100644 index 52be369..0000000 --- a/Sources/Schedule/Timeline.swift +++ /dev/null @@ -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 - } -} diff --git a/Sources/Schedule/Weekday.swift b/Sources/Schedule/Weekday.swift index 669202b..a0e5d82 100644 --- a/Sources/Schedule/Weekday.swift +++ b/Sources/Schedule/Weekday.swift @@ -7,10 +7,10 @@ public enum Weekday: Int { /// Returns dateComponenets of the weekday, using gregorian calender and /// current time zone. - public func asDateComponents() -> DateComponents { + public func asDateComponents(_ timeZone: TimeZone = .current) -> DateComponents { return DateComponents( calendar: Calendar.gregorian, - timeZone: TimeZone.current, + timeZone: timeZone, weekday: rawValue) } } @@ -18,8 +18,10 @@ public enum Weekday: Int { extension Date { /// Returns a Boolean value indicating whether this date is the weekday in current time zone. - public func `is`(_ weekday: Weekday) -> Bool { - return Calendar.gregorian.component(.weekday, from: self) == weekday.rawValue + public func `is`(_ weekday: Weekday, in timeZone: TimeZone = .current) -> Bool { + var cal = Calendar.gregorian + cal.timeZone = timeZone + return cal.component(.weekday, from: self) == weekday.rawValue } } diff --git a/Tests/ScheduleTests/AtomicTests.swift b/Tests/ScheduleTests/AtomicTests.swift index 151032a..d7f300e 100644 --- a/Tests/ScheduleTests/AtomicTests.swift +++ b/Tests/ScheduleTests/AtomicTests.swift @@ -8,7 +8,7 @@ final class AtomicTests: XCTestCase { let val = i.read { $0 } XCTAssertEqual(val, 1) } - + func testReadVoid() { let i = Atomic(1) var val = 0 @@ -24,7 +24,7 @@ final class AtomicTests: XCTestCase { } XCTAssertEqual(i.read { $0 }, val) } - + func testWriteVoid() { let i = Atomic(1) var val = 0 @@ -39,6 +39,6 @@ final class AtomicTests: XCTestCase { ("testRead", testRead), ("testReadVoid", testReadVoid), ("testWrite", testWrite), - ("testWriteVoid", testWriteVoid), + ("testWriteVoid", testWriteVoid) ] } diff --git a/Tests/ScheduleTests/BagTests.swift b/Tests/ScheduleTests/BagTests.swift index 78dd5ba..97be43e 100644 --- a/Tests/ScheduleTests/BagTests.swift +++ b/Tests/ScheduleTests/BagTests.swift @@ -26,15 +26,15 @@ final class BagTests: XCTestCase { var bag = Bag() let k1 = bag.append { 1 } let k2 = bag.append { 2 } - + let fn1 = bag.value(for: k1) XCTAssertNotNil(fn1) - + let fn2 = bag.value(for: k2) XCTAssertNotNil(fn2) - + guard let _fn1 = fn1, let _fn2 = fn2 else { return } - + XCTAssertEqual(_fn1(), 1) XCTAssertEqual(_fn2(), 2) } @@ -52,24 +52,24 @@ final class BagTests: XCTestCase { let fn2 = bag.removeValue(for: k2) XCTAssertNotNil(fn2) XCTAssertNil(bag.removeValue(for: k2)) - + guard let _fn1 = fn1, let _fn2 = fn2 else { return } - + XCTAssertEqual(_fn1(), 1) XCTAssertEqual(_fn2(), 2) } - + func testCount() { var bag = Bag() - + let k1 = bag.append { 1 } let k2 = bag.append { 2 } - + XCTAssertEqual(bag.count, 2) - + bag.removeValue(for: k1) bag.removeValue(for: k2) - + XCTAssertEqual(bag.count, 0) } diff --git a/Tests/ScheduleTests/DeinitObserverTests.swift b/Tests/ScheduleTests/DeinitObserverTests.swift index 7148bdf..def129e 100644 --- a/Tests/ScheduleTests/DeinitObserverTests.swift +++ b/Tests/ScheduleTests/DeinitObserverTests.swift @@ -16,7 +16,7 @@ final class DeinitObserverTests: XCTestCase { fn() XCTAssertEqual(i, 1) } - + func testCancel() { var i = 0 let fn = { diff --git a/Tests/ScheduleTests/ExtensionsTests.swift b/Tests/ScheduleTests/ExtensionsTests.swift index ad4a3fc..507ae92 100644 --- a/Tests/ScheduleTests/ExtensionsTests.swift +++ b/Tests/ScheduleTests/ExtensionsTests.swift @@ -18,14 +18,14 @@ final class ExtensionsTests: XCTestCase { func testStartOfToday() { let components = Date().startOfToday.dateComponents - guard - let h = components.hour, - let m = components.minute, - let s = components.second - else { - XCTFail() - return - } + + let h = components.hour + let m = components.minute + let s = components.second + XCTAssertNotNil(h) + XCTAssertNotNil(m) + XCTAssertNotNil(s) + XCTAssertEqual(h, 0) XCTAssertEqual(m, 0) XCTAssertEqual(s, 0) diff --git a/Tests/ScheduleTests/Helpers.swift b/Tests/ScheduleTests/Helpers.swift index 79705ab..76fa587 100644 --- a/Tests/ScheduleTests/Helpers.swift +++ b/Tests/ScheduleTests/Helpers.swift @@ -64,7 +64,7 @@ extension Plan { extension DispatchQueue { func async(after interval: Interval, execute body: @escaping () -> Void) { - asyncAfter(deadline: .now() + interval.asSeconds(), execute: body) + asyncAfter(wallDeadline: .now() + interval.asSeconds(), execute: body) } static func `is`(_ queue: DispatchQueue) -> Bool { @@ -76,3 +76,8 @@ extension DispatchQueue { return DispatchQueue.getSpecific(key: key) != nil } } + +extension TimeZone { + + static let shanghai = TimeZone(identifier: "Asia/Shanghai")! +} diff --git a/Tests/ScheduleTests/IntervalTests.swift b/Tests/ScheduleTests/IntervalTests.swift index ef836e2..1bff1ed 100644 --- a/Tests/ScheduleTests/IntervalTests.swift +++ b/Tests/ScheduleTests/IntervalTests.swift @@ -9,64 +9,65 @@ import XCTest @testable import Schedule final class IntervalTests: XCTestCase { - + private let leeway = 0.01.second - + func testEquatable() { XCTAssertEqual(1.second, 1.second) XCTAssertEqual(1.week, 7.days) } - + func testIsNegative() { XCTAssertFalse(1.second.isNegative) XCTAssertTrue((-1).second.isNegative) } - + func testAbs() { XCTAssertEqual(1.second, (-1).second.abs) } - + func testNegated() { XCTAssertEqual(1.second.negated, (-1).second) XCTAssertEqual(1.second.negated.negated, 1.second) } - + func testCompare() { XCTAssertEqual((-1).second.compare(1.second), ComparisonResult.orderedAscending) XCTAssertEqual(8.days.compare(1.week), ComparisonResult.orderedDescending) - + XCTAssertEqual(1.day.compare(24.hours), ComparisonResult.orderedSame) + XCTAssertTrue(23.hours < 1.day) XCTAssertTrue(25.hours > 1.day) } - + func testLongerShorter() { XCTAssertTrue((-25).hour.isLonger(than: 1.day)) XCTAssertTrue(1.week.isShorter(than: 8.days)) } - + func testMultiplying() { XCTAssertEqual(7.days * 2, 2.week) } - + func testAdding() { XCTAssertEqual(6.days + 1.day, 1.week) - + XCTAssertEqual(1.1.weeks, 1.week + 0.1.weeks) } - + func testOperators() { XCTAssertEqual(1.week - 6.days, 1.day) - + var i = 6.days i += 1.day XCTAssertEqual(i, 1.week) - + XCTAssertEqual(-(7.days), (-1).week) } - + func testAs() { XCTAssertEqual(1.millisecond.asNanoseconds(), 1.microsecond.asNanoseconds() * pow(10, 3)) - + XCTAssertEqual(1.second.asNanoseconds(), pow(10, 9)) XCTAssertEqual(1.second.asMicroseconds(), pow(10, 6)) XCTAssertEqual(1.second.asMilliseconds(), pow(10, 3)) @@ -77,23 +78,23 @@ final class IntervalTests: XCTestCase { XCTAssertEqual(1.week.asDays(), 7) XCTAssertEqual(7.days.asWeeks(), 1) } - + func testDate() { let date0 = Date() let date1 = date0.addingTimeInterval(100) - + XCTAssertTrue(date1.intervalSinceNow.isAlmostEqual(to: 100.seconds, leeway: leeway)) - + XCTAssertEqual(date0.interval(since: date1), date0.timeIntervalSince(date1).seconds) XCTAssertEqual(date0.adding(1.seconds), date0.addingTimeInterval(1)) XCTAssertEqual(date0 + 1.seconds, date0.addingTimeInterval(1)) } - + func testDescription() { XCTAssertEqual(1.nanosecond.debugDescription, "Interval: 1 nanosecond(s)") } - + static var allTests = [ ("testEquatable", testEquatable), ("testIsNegative", testIsNegative), @@ -105,6 +106,6 @@ final class IntervalTests: XCTestCase { ("testAdding", testAdding), ("testOperators", testOperators), ("testAs", testAs), - ("testDate", testDate), + ("testDate", testDate) ] } diff --git a/Tests/ScheduleTests/MonthdayTests.swift b/Tests/ScheduleTests/MonthdayTests.swift index 9afedec..3a196ac 100644 --- a/Tests/ScheduleTests/MonthdayTests.swift +++ b/Tests/ScheduleTests/MonthdayTests.swift @@ -2,24 +2,24 @@ import XCTest @testable import Schedule final class MonthdayTests: XCTestCase { - + func testIs() { // ! Be careful the time zone problem. let d = Date(year: 2019, month: 1, day: 1) - XCTAssertTrue(d.is(.january(1))) + XCTAssertTrue(d.is(.january(1), in: TimeZone.shanghai)) } - + func testAsDateComponents() { let comps = Monthday.april(1).asDateComponents() XCTAssertEqual(comps.month, 4) XCTAssertEqual(comps.day, 1) } - + func testDescription() { let md = Monthday.april(1) XCTAssertEqual(md.debugDescription, "Monthday: April 1st") } - + static var allTests = [ ("testIs", testIs), ("testAsDateComponents", testAsDateComponents), diff --git a/Tests/ScheduleTests/PeriodTests.swift b/Tests/ScheduleTests/PeriodTests.swift index 03f00f2..ca5bb73 100644 --- a/Tests/ScheduleTests/PeriodTests.swift +++ b/Tests/ScheduleTests/PeriodTests.swift @@ -2,45 +2,48 @@ import XCTest @testable import Schedule final class PeriodTests: XCTestCase { - + func testPeriod() { let period = (1.year + 2.years + 1.month + 2.months + 3.days) XCTAssertEqual(period.years, 3) XCTAssertEqual(period.months, 3) XCTAssertEqual(period.days, 3) } - + func testInitWithString() { let p1 = Period("one second") XCTAssertNotNil(p1) XCTAssertEqual(p1!.seconds, 1) - + let p2 = Period("two hours and ten minutes") XCTAssertNotNil(p2) XCTAssertEqual(p2!.hours, 2) XCTAssertEqual(p2!.minutes, 10) - + let p3 = Period("1 year, 2 months and 3 days") XCTAssertNotNil(p3) XCTAssertEqual(p3!.years, 1) XCTAssertEqual(p3!.months, 2) XCTAssertEqual(p3!.days, 3) - + Period.registerQuantifier("many", for: 100 * 1000) let p4 = Period("many days") XCTAssertEqual(p4!.days, 100 * 1000) + + let p5 = Period("hi, 😈") + XCTAssertNil(p5) } - + func testAdd() { XCTAssertEqual(1.month.adding(1.month).months, 2) XCTAssertEqual(Period(days: 1).adding(1.day).days, 2) } - + func testTidy() { let period = 1.month.adding(25.hour).tidied(to: .day) XCTAssertEqual(period.days, 1) } - + func testAsDateComponents() { let period = Period(years: 1, months: 2, days: 3, hours: 4, minutes: 5, seconds: 6, nanoseconds: 7) let comps = period.asDateComponents() @@ -52,13 +55,13 @@ final class PeriodTests: XCTestCase { XCTAssertEqual(comps.second, 6) XCTAssertEqual(comps.nanosecond, 7) } - + func testDate() { let d = Date(year: 1989, month: 6, day: 4) + 1.year let year = d.dateComponents.year XCTAssertEqual(year, 1990) } - + func testDescription() { let period = Period(years: 1, nanoseconds: 1) XCTAssertEqual(period.debugDescription, "Period: 1 year(s) 1 nanosecond(s)") diff --git a/Tests/ScheduleTests/PlanTests.swift b/Tests/ScheduleTests/PlanTests.swift index 41d7d6f..916b6e0 100644 --- a/Tests/ScheduleTests/PlanTests.swift +++ b/Tests/ScheduleTests/PlanTests.swift @@ -3,34 +3,47 @@ import XCTest final class PlanTests: XCTestCase { - let leeway = 0.01.seconds - - func testMake() { - let intervals = [1.second, 2.hours, 3.days, 4.weeks] - let s0 = Plan.of(intervals[0], intervals[1], intervals[2], intervals[3]) - XCTAssertTrue(s0.makeIterator().isAlmostEqual(to: intervals, leeway: leeway)) - - let d0 = Date() + intervals[0] - let d1 = d0 + intervals[1] - let d2 = d1 + intervals[2] - let d3 = d2 + intervals[3] - - let s2 = Plan.of(d0, d1, d2, d3) - XCTAssertTrue(s2.makeIterator().isAlmostEqual(to: intervals, leeway: leeway)) - - let longTime = (100 * 365).days - XCTAssertTrue(Plan.distantPast.makeIterator().next()!.isLonger(than: longTime)) - XCTAssertTrue(Plan.distantFuture.makeIterator().next()!.isLonger(than: longTime)) + private let leeway = 0.01.seconds + + func testOfIntervals() { + let ints = [1.second, 2.hours, 3.days, 4.weeks] + let p = Plan.of(ints) + XCTAssertTrue(p.makeIterator().isAlmostEqual(to: ints, leeway: leeway)) + } + + func testOfDates() { + let ints = [1.second, 2.hours, 3.days, 4.weeks] + + let d0 = Date() + ints[0] + let d1 = d0 + ints[1] + let d2 = d1 + ints[2] + let d3 = d2 + ints[3] + + let p = Plan.of(d0, d1, d2, d3) + 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() - var next = iterator.next() - XCTAssertNotNil(next) - XCTAssertTrue(next!.intervalSinceNow.isAlmostEqual(to: 1.days, leeway: leeway)) - next = iterator.next() - XCTAssertNotNil(next) - XCTAssertTrue(next!.intervalSinceNow.isAlmostEqual(to: 2.weeks + 1.days, leeway: leeway)) + + func testDistant() { + let distantPast = Plan.distantPast.makeIterator().next() + XCTAssertNotNil(distantPast) + XCTAssertTrue(distantPast!.isAlmostEqual(to: Date.distantPast.intervalSinceNow, leeway: leeway)) + + let distantFuture = Plan.distantFuture.makeIterator().next() + XCTAssertNotNil(distantFuture) + XCTAssertTrue(distantFuture!.isAlmostEqual(to: Date.distantFuture.intervalSinceNow, leeway: leeway)) } func testNever() { @@ -38,64 +51,64 @@ final class PlanTests: XCTestCase { } func testConcat() { - let s0: [Interval] = [1.second, 2.minutes, 3.hours] - let s1: [Interval] = [4.days, 5.weeks] - let s3 = Plan.of(s0).concat(Plan.of(s1)) - let s4 = Plan.of(s0 + s1) - XCTAssertTrue(s3.isAlmostEqual(to: s4, leeway: leeway)) + let p0: [Interval] = [1.second, 2.minutes, 3.hours] + let p1: [Interval] = [4.days, 5.weeks] + let p2 = Plan.of(p0).concat(Plan.of(p1)) + let p3 = Plan.of(p0 + p1) + XCTAssertTrue(p2.isAlmostEqual(to: p3, leeway: leeway)) } func testMerge() { - let intervals0: [Interval] = [1.second, 2.minutes, 1.hour] - let intervals1: [Interval] = [2.seconds, 1.minutes, 1.seconds] - let scheudle0 = Plan.of(intervals0).merge(Plan.of(intervals1)) - let scheudle1 = Plan.of(1.second, 1.second, 1.minutes, 1.seconds, 58.seconds, 1.hour) - XCTAssertTrue(scheudle0.isAlmostEqual(to: scheudle1, leeway: leeway)) + let ints0: [Interval] = [1.second, 2.minutes, 1.hour] + let ints1: [Interval] = [2.seconds, 1.minutes, 1.seconds] + let p0 = Plan.of(ints0).merge(Plan.of(ints1)) + let p1 = Plan.of(1.second, 1.second, 1.minutes, 1.seconds, 58.seconds, 1.hour) + XCTAssertTrue(p0.isAlmostEqual(to: p1, leeway: leeway)) } - - func 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() { var count = 10 - let s = Plan.every(1.second).first(count) - let i = s.makeIterator() + let p = Plan.every(1.second).first(count) + let i = p.makeIterator() while count > 0 { XCTAssertNotNil(i.next()) count -= 1 } XCTAssertNil(i.next()) } - + func testUntil() { let until = Date() + 10.seconds - let s = Plan.every(1.second).until(until).dates - let i = s.makeIterator() + let p = Plan.every(1.second).until(until).dates + let i = p.makeIterator() while let date = i.next() { XCTAssertLessThan(date, until) } } func testNow() { - let s0 = Plan.now - let s1 = Plan.of(Date()) - XCTAssertTrue(s0.isAlmostEqual(to: s1, leeway: leeway)) + let p0 = Plan.now + let p1 = Plan.of(Date()) + XCTAssertTrue(p0.isAlmostEqual(to: p1, leeway: leeway)) + } + + func testAt() { + let p = Plan.at(Date() + 1.second) + let next = p.makeIterator().next() + XCTAssertNotNil(next) + XCTAssertTrue(next!.isAlmostEqual(to: 1.second, leeway: leeway)) } func testAfterAndRepeating() { - let s0 = Plan.after(1.day, repeating: 1.hour).first(3) - let s1 = Plan.of(1.day, 1.hour, 1.hour) - XCTAssertTrue(s0.isAlmostEqual(to: s1, leeway: leeway)) + let p0 = Plan.after(1.day, repeating: 1.hour).first(3) + let p1 = Plan.of(1.day, 1.hour, 1.hour) + XCTAssertTrue(p0.isAlmostEqual(to: p1, leeway: leeway)) } func testEveryPeriod() { - let s = Plan.every("1 year").first(10) + let p = Plan.every("1 year").first(10) var date = Date() - for i in s.dates { + for i in p.dates { XCTAssertEqual(i.dateComponents.year!, date.dateComponents.year! + 1) XCTAssertEqual(i.dateComponents.month!, date.dateComponents.month!) XCTAssertEqual(i.dateComponents.day!, date.dateComponents.day!) @@ -104,121 +117,47 @@ final class PlanTests: XCTestCase { } func testEveryWeekday() { - let s = Plan.every(.friday, .monday).at("11:11:00").first(5) - for i in s.dates { + let p = Plan.every(.friday, .monday).at("11:11:00").first(5) + for i in p.dates { XCTAssertTrue(i.dateComponents.weekday == 6 || i.dateComponents.weekday == 2) XCTAssertEqual(i.dateComponents.hour, 11) } } func testEveryMonthday() { - let s = Plan.every(.april(1), .october(1)).at(11, 11).first(5) - for i in s.dates { + let p = Plan.every(.april(1), .october(1)).at(11, 11).first(5) + for i in p.dates { XCTAssertTrue(i.dateComponents.month == 4 || i.dateComponents.month == 10) XCTAssertEqual(i.dateComponents.day, 1) XCTAssertEqual(i.dateComponents.hour, 11) } } - func testPassingEmptyArrays() { - XCTAssertTrue(Plan.of([Interval]()).isNever()) - XCTAssertTrue(Plan.of([Date]()).isNever()) - - XCTAssertTrue(Plan.every([Weekday]()).at(11, 11).isNever()) - XCTAssertTrue(Plan.every([Monthday]()).at(11, 11).isNever()) - - XCTAssertTrue(Plan.every(.monday).at([]).isNever()) - - XCTAssertTrue(Plan.every([Weekday]()).at("11:11:00").isNever()) - } - - func testIntervalOffset() { - // Non-offset plan - let e1 = expectation(description: "testIntervalOffset_1") - let plan1 = Plan.after(1.second) - var date1: Date? - - // Offset plan - let e2 = expectation(description: "testIntervalOffset_2") - let plan2 = plan1.offset(by: 1.second) - var date2: Date? - - let task1 = plan1.do { date1 = Date(); e1.fulfill() } - let task2 = plan2.do { date2 = Date(); e2.fulfill() } - _ = task1 - _ = task2 - - waitForExpectations(timeout: 3.5) - - XCTAssertNotNil(date1) - XCTAssertNotNil(date2) - XCTAssertTrue(date2!.interval(since: date1!).isAlmostEqual(to: 1.second, leeway: 0.1.seconds)) - } - - func testNegativeIntervalOffset() { - // Non-offset plan - let e1 = expectation(description: "testIntervalOffset_1") - let plan1 = Plan.after(2.seconds) - var date1: Date? - - // Offset plan - let e2 = expectation(description: "testIntervalOffset_2") - let plan2 = plan1.offset(by: -1.second) - var date2: Date? - - let task1 = plan1.do { date1 = Date(); e1.fulfill() } - let task2 = plan2.do { date2 = Date(); e2.fulfill() } - _ = task1 - _ = task2 - - waitForExpectations(timeout: 2.5) - - XCTAssertNotNil(date1) - XCTAssertNotNil(date2) - XCTAssertTrue(date2!.interval(since: date1!).isAlmostEqual(to: -1.second, leeway: 0.1.seconds)) - } - - func testNilIntervalOffset() { - // Non-offset plan - let e1 = expectation(description: "testIntervalOffset_1") - let plan1 = Plan.after(1.second) - var date1: Date? - - // Offset plan - let e2 = expectation(description: "testIntervalOffset_2") - let plan2 = plan1.offset(by: nil) - var date2: Date? - - let task1 = plan1.do { date1 = Date(); e1.fulfill() } - let task2 = plan2.do { date2 = Date(); e2.fulfill() } - - _ = task1 - _ = task2 - - waitForExpectations(timeout: 1.5) - - XCTAssertNotNil(date1) - XCTAssertNotNil(date2) - XCTAssertTrue(date2!.interval(since: date1!).isAlmostEqual(to: 0.seconds, leeway: 0.1.seconds)) + func testOffset() { + let p1 = Plan.after(1.second).first(100) + let p2 = p1.offset(by: 1.second).first(100) + + for (d1, d2) in zip(p1.dates, p2.dates) { + XCTAssertTrue(d2.interval(since: d1).isAlmostEqual(to: 1.second, leeway: leeway)) + } } static var allTests = [ - ("testMake", testMake), + ("testOfIntervals", testOfIntervals), + ("testOfDates", testOfDates), ("testDates", testDates), + ("testDistant", testDistant), ("testNever", testNever), ("testConcat", testConcat), ("testMerge", testMerge), - ("testAt", testAt), ("testFirst", testFirst), ("testUntil", testUntil), ("testNow", testNow), + ("testAt", testAt), ("testAfterAndRepeating", testAfterAndRepeating), ("testEveryPeriod", testEveryPeriod), ("testEveryWeekday", testEveryWeekday), ("testEveryMonthday", testEveryMonthday), - ("testPassingEmptyArrays", testPassingEmptyArrays), - ("testIntervalOffset", testIntervalOffset), - ("testNegativeIntervalOffset", testNegativeIntervalOffset), - ("testNilIntervalOffset", testNilIntervalOffset) + ("testOffset", testOffset) ] } diff --git a/Tests/ScheduleTests/TaskCenterTests.swift b/Tests/ScheduleTests/TaskCenterTests.swift index 68b7dff..7474067 100644 --- a/Tests/ScheduleTests/TaskCenterTests.swift +++ b/Tests/ScheduleTests/TaskCenterTests.swift @@ -19,50 +19,58 @@ final class TaskCenterTests: XCTestCase { } func testAdd() { - let c = TaskCenter() - let task = makeTask() XCTAssertEqual(center.allTasks.count, 1) + let c = TaskCenter() c.add(task) + XCTAssertEqual(center.allTasks.count, 0) XCTAssertEqual(c.allTasks.count, 1) - c.add(task) - XCTAssertEqual(c.allTasks.count, 1) + center.add(task) + XCTAssertEqual(center.allTasks.count, 1) + XCTAssertEqual(c.allTasks.count, 0) center.removeAll() } func testRemove() { let task = makeTask() + let tag = UUID().uuidString + center.addTag(tag, to: task) + center.remove(task) + XCTAssertFalse(center.allTasks.contains(task)) + XCTAssertFalse(center.allTags.contains(tag)) + + center.removeAll() } func testTag() { let task = makeTask() - let tag = UUID().uuidString center.addTag(tag, to: task) - XCTAssertTrue(center.tasksForTag(tag).contains(task)) - XCTAssertTrue(center.tagsForTask(task).contains(tag)) + XCTAssertTrue(center.tasks(forTag: tag).contains(task)) + XCTAssertTrue(center.tags(forTask: task).contains(tag)) center.removeTag(tag, from: task) - XCTAssertFalse(center.tasksForTag(tag).contains(task)) - XCTAssertFalse(center.tagsForTask(task).contains(tag)) + XCTAssertFalse(center.tasks(forTag: tag).contains(task)) + XCTAssertFalse(center.tags(forTask: task).contains(tag)) center.removeAll() } func testAll() { let task = makeTask() + let tag1 = UUID().uuidString + let tag2 = UUID().uuidString - let tag = UUID().uuidString + center.addTags([tag1, tag2], to: task) - center.addTag(tag, to: task) - XCTAssertEqual(center.allTags, [tag]) + XCTAssertEqual(center.allTags.sorted(), [tag1, tag2].sorted()) XCTAssertEqual(center.allTasks, [task]) center.removeAll() @@ -70,18 +78,17 @@ final class TaskCenterTests: XCTestCase { func testOperation() { let task = makeTask() - let tag = UUID().uuidString center.addTag(tag, to: task) - center.suspendByTag(tag) - XCTAssertEqual(task.suspensions, 1) + center.suspend(byTag: tag) + XCTAssertEqual(task.suspensionCount, 1) - center.resumeByTag(tag) - XCTAssertEqual(task.suspensions, 0) + center.resume(byTag: tag) + XCTAssertEqual(task.suspensionCount, 0) - center.cancelByTag(tag) + center.cancel(byTag: tag) XCTAssertTrue(task.isCancelled) center.removeAll() @@ -89,7 +96,9 @@ final class TaskCenterTests: XCTestCase { func testWeak() { let block = { - _ = self.makeTask() + let task = self.makeTask() + XCTAssertEqual(self.center.allTasks.count, 1) + _ = task } block() diff --git a/Tests/ScheduleTests/TaskTests.swift b/Tests/ScheduleTests/TaskTests.swift index 6b919f0..e712819 100644 --- a/Tests/ScheduleTests/TaskTests.swift +++ b/Tests/ScheduleTests/TaskTests.swift @@ -2,46 +2,84 @@ import XCTest @testable import Schedule final class TaskTests: XCTestCase { + + let leeway = 0.01.second + + func testMetrics() { + let e = expectation(description: "testMetrics") + let date = Date() + + let task = Plan.after(0.01.second, repeating: 0.01.second).do(queue: .global()) { + e.fulfill() + } + XCTAssertTrue(task.creationDate.interval(since: date).isAlmostEqual(to: 0.second, leeway: leeway)) + + waitForExpectations(timeout: 0.1) + + XCTAssertNotNil(task.firstExecutionDate) + XCTAssertTrue(task.firstExecutionDate!.interval(since: date).isAlmostEqual(to: 0.01.second, leeway: leeway)) + + XCTAssertNotNil(task.lastExecutionDate) + XCTAssertTrue(task.lastExecutionDate!.interval(since: date).isAlmostEqual(to: 0.01.second, leeway: leeway)) + } func testAfter() { let e = expectation(description: "testSchedule") let date = Date() - let task = Plan.after(0.1.second).do { - XCTAssertTrue(Date().timeIntervalSince(date).isAlmostEqual(to: 0.1, leeway: 0.1)) + let task = Plan.after(0.01.second).do(queue: .global()) { + XCTAssertTrue(Date().interval(since: date).isAlmostEqual(to: 0.01.second, leeway: self.leeway)) e.fulfill() } - waitForExpectations(timeout: 0.5) - task.cancel() + waitForExpectations(timeout: 0.1) + + _ = task } func testRepeat() { let e = expectation(description: "testRepeat") - var t = 0 - let task = Plan.every(0.1.second).first(3).do { - t += 1 - if t == 3 { e.fulfill() } + var count = 0 + let task = Plan.every(0.01.second).first(3).do(queue: .global()) { + count += 1 + if count == 3 { e.fulfill() } } - waitForExpectations(timeout: 1) - task.cancel() + waitForExpectations(timeout: 0.1) + + _ = task + } + + func testTaskCenter() { + let task = Plan.never.do { } + XCTAssertTrue(task.taskCenter === TaskCenter.default) + + task.removeFromTaskCenter(TaskCenter()) + XCTAssertNotNil(task.taskCenter) + + task.removeFromTaskCenter(task.taskCenter!) + XCTAssertNil(task.taskCenter) + + let center = TaskCenter() + task.addToTaskCenter(center) + XCTAssertTrue(task.taskCenter === center) } func testDispatchQueue() { let e = expectation(description: "testQueue") - let queue = DispatchQueue(label: "testQueue") + let q = DispatchQueue(label: UUID().uuidString) - let task = Plan.after(0.1.second).do(queue: queue) { - XCTAssertTrue(DispatchQueue.is(queue)) + let task = Plan.after(0.01.second).do(queue: q) { + XCTAssertTrue(DispatchQueue.is(q)) e.fulfill() } - waitForExpectations(timeout: 0.5) - task.cancel() + waitForExpectations(timeout: 0.1) + + _ = task } func testThread() { let e = expectation(description: "testThread") DispatchQueue.global().async { let thread = Thread.current - let task = Plan.after(0.1.second).do { task in + let task = Plan.after(0.01.second).do { task in XCTAssertTrue(thread === Thread.current) e.fulfill() task.cancel() @@ -49,20 +87,70 @@ final class TaskTests: XCTestCase { _ = task RunLoop.current.run() } - waitForExpectations(timeout: 0.5) + waitForExpectations(timeout: 0.1) } func testSuspendResume() { - let task1 = Plan.distantFuture.do { } - XCTAssertEqual(task1.suspensions, 0) - task1.suspend() - task1.suspend() - task1.suspend() - XCTAssertEqual(task1.suspensions, 3) - task1.resume() - XCTAssertEqual(task1.suspensions, 2) + let task = Plan.never.do { } + XCTAssertEqual(task.suspensionCount, 0) + task.suspend() + task.suspend() + task.suspend() + XCTAssertEqual(task.suspensionCount, 3) + task.resume() + XCTAssertEqual(task.suspensionCount, 2) + } + + func testCancel() { + let task = Plan.never.do { } + XCTAssertFalse(task.isCancelled) + task.cancel() + XCTAssertTrue(task.isCancelled) + } + + func testExecuteNow() { + let e = expectation(description: "testExecuteNow") + let task = Plan.never.do { + e.fulfill() + } + task.execute() + waitForExpectations(timeout: 0.1) } + func testHost() { + let e = expectation(description: "testHost") + let fn = { + let obj = NSObject() + let task = Plan.after(0.1.second).do(queue: .main, block: { + XCTFail("should never come here") + }) + task.host(to: obj) + } + fn() + DispatchQueue.main.async(after: 0.2.seconds) { + e.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testReschedule() { + let e = expectation(description: "testReschedule") + var i = 0 + let task = Plan.after(0.01.second).do(queue: .global()) { (task) in + i += 1 + if task.executionCount == 3 && task.estimatedNextExecutionDate == nil { + e.fulfill() + } + if task.executionCount > 3 { + XCTFail("should never come here") + } + } + DispatchQueue.global().async(after: 0.02.second) { + task.reschedule(Plan.every(0.01.second).first(2)) + } + waitForExpectations(timeout: 1) + } + func testAddAndRemoveActions() { let e = expectation(description: "testAddAndRemoveActions") let task = Plan.after(0.1.second).do { } @@ -83,69 +171,17 @@ final class TaskTests: XCTestCase { XCTAssertEqual(task.countOfActions, 0) } - func testReschedule() { - let e = expectation(description: "testReschedule") - var i = 0 - let task = Plan.after(0.1.second).do { (task) in - i += 1 - if task.countOfExecutions == 6 && task.timeline.estimatedNextExecution == nil { - e.fulfill() - } - if task.countOfExecutions > 6 { - XCTFail("should never come here") - } - } - DispatchQueue.global().async(after: 0.5.second) { - task.reschedule(Plan.every(0.1.second).first(5)) - } - waitForExpectations(timeout: 2) - task.cancel() - } - - func testHost() { - let e = expectation(description: "testHost") - let fn = { - let obj = NSObject() - let task = Plan.after(0.1.second).do(queue: .main, onElapse: { - XCTFail("should never come here") - }) - task.host(on: obj) - } - fn() - DispatchQueue.main.async(after: 0.2.seconds) { - e.fulfill() - } - waitForExpectations(timeout: 1) - } - - func testLifetime() { - let e = expectation(description: "testLifetime") - let task = Plan.after(1.hour).do { } - task.setLifetime(1.second) - XCTAssertEqual(task.lifetime, 1.second) - - DispatchQueue.global().async(after: 0.5.second) { - XCTAssertTrue(task.restOfLifetime.isAlmostEqual(to: 0.5.second, leeway: 0.1.second)) - task.subtractLifetime(-0.5.second) - } - DispatchQueue.global().async(after: 1.second) { - XCTAssertFalse(task.isCancelled) - } - DispatchQueue.global().async(after: 2.second) { - XCTAssertTrue(task.isCancelled) - e.fulfill() - } - waitForExpectations(timeout: 3) - } - static var allTests = [ ("testAfter", testAfter), ("testRepeat", testRepeat), + ("testTaskCenter", testTaskCenter), ("testDispatchQueue", testDispatchQueue), ("testThread", testThread), - ("testAddAndRemoveActions", testAddAndRemoveActions), - ("testReschedule", testReschedule), + ("testSuspendResume", testSuspendResume), + ("testCancel", testCancel), + ("testExecuteNow", testExecuteNow), ("testHost", testHost), - ("testLifetime", testLifetime) + ("testReschedule", testReschedule), + ("testAddAndRemoveActions", testAddAndRemoveActions) ] } diff --git a/Tests/ScheduleTests/TimeTests.swift b/Tests/ScheduleTests/TimeTests.swift index 4c436f4..f8518f0 100644 --- a/Tests/ScheduleTests/TimeTests.swift +++ b/Tests/ScheduleTests/TimeTests.swift @@ -2,7 +2,7 @@ import XCTest @testable import Schedule final class TimeTests: XCTestCase { - + func testTime() { let t1 = Time("11:12:13.456") XCTAssertNotNil(t1) @@ -24,11 +24,11 @@ final class TimeTests: XCTestCase { let t4 = Time("schedule") XCTAssertNil(t4) } - + func testIntervalSinceStartOfDay() { XCTAssertEqual(Time(hour: 1)!.intervalSinceStartOfDay, 1.hour) } - + func testAsDateComponents() { let time = Time(hour: 11, minute: 12, second: 13, nanosecond: 456) let components = time?.asDateComponents() @@ -37,7 +37,7 @@ final class TimeTests: XCTestCase { XCTAssertEqual(components?.second, 13) XCTAssertEqual(components?.nanosecond, 456) } - + func testDescription() { let time = Time("11:12:13.456") XCTAssertEqual(time!.debugDescription, "Time: 11:12:13.456") diff --git a/Tests/ScheduleTests/WeekdayTests.swift b/Tests/ScheduleTests/WeekdayTests.swift index cfeb6cd..1f9bfab 100644 --- a/Tests/ScheduleTests/WeekdayTests.swift +++ b/Tests/ScheduleTests/WeekdayTests.swift @@ -2,22 +2,22 @@ import XCTest @testable import Schedule final class WeekdayTests: XCTestCase { - + func testIs() { // ! Be careful the time zone problem. let d = Date(year: 2019, month: 1, day: 1) - XCTAssertTrue(d.is(.tuesday)) + XCTAssertTrue(d.is(.tuesday, in: TimeZone.shanghai)) } - + func testAsDateComponents() { XCTAssertEqual(Weekday.monday.asDateComponents().weekday!, 2) } - + func testDescription() { let wd = Weekday.tuesday XCTAssertEqual(wd.debugDescription, "Weekday: Tuesday") } - + static var allTests = [ ("testIs", testIs), ("testAsDateComponents", testAsDateComponents),