From f48758d4f75e076f4f6bbde555041aa8c99a99b5 Mon Sep 17 00:00:00 2001 From: QuentinJin Date: Wed, 25 Jul 2018 23:50:21 +0800 Subject: [PATCH] Remove Objc dependency, and improve documents. --- Schedule.xcodeproj/project.pbxproj | 46 ++++- Sources/Schedule/Bucket.swift | 38 ++-- Sources/Schedule/Extensions.swift | 24 +-- Sources/Schedule/Interval.swift | 78 ++++---- Sources/Schedule/Lock.swift | 44 +++++ Sources/Schedule/Period.swift | 10 +- Sources/Schedule/Task.swift | 212 +++++++++++----------- Sources/Schedule/TaskCenter.swift | 61 +++---- Sources/Schedule/Time.swift | 6 +- Sources/Schedule/Timeline.swift | 29 +++ Sources/Schedule/WeakSet.swift | 60 ++++++ Tests/ScheduleTests/BucketTests.swift | 61 +++++++ Tests/ScheduleTests/DateTimeTests.swift | 9 - Tests/ScheduleTests/ExtensionsTests.swift | 29 +++ Tests/ScheduleTests/TaskCenterTests.swift | 47 +++++ Tests/ScheduleTests/WeakSetTests.swift | 44 +++++ 16 files changed, 580 insertions(+), 218 deletions(-) create mode 100644 Sources/Schedule/Lock.swift create mode 100644 Sources/Schedule/Timeline.swift create mode 100644 Sources/Schedule/WeakSet.swift create mode 100644 Tests/ScheduleTests/BucketTests.swift create mode 100644 Tests/ScheduleTests/ExtensionsTests.swift create mode 100644 Tests/ScheduleTests/TaskCenterTests.swift create mode 100644 Tests/ScheduleTests/WeakSetTests.swift diff --git a/Schedule.xcodeproj/project.pbxproj b/Schedule.xcodeproj/project.pbxproj index bb3f6eb..3abe034 100644 --- a/Schedule.xcodeproj/project.pbxproj +++ b/Schedule.xcodeproj/project.pbxproj @@ -21,8 +21,15 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 62661F412108433400055501 /* WeakSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62661F402108433300055501 /* WeakSet.swift */; }; + 62EF93FA2108530D001F7A47 /* WeakSetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EF93F821085309001F7A47 /* WeakSetTests.swift */; }; + 62EF93FF21086420001F7A47 /* TaskCenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EF93FD21086411001F7A47 /* TaskCenterTests.swift */; }; + 62EF940821086F63001F7A47 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EF940721086F63001F7A47 /* Timeline.swift */; }; 6624104A2104A42C00013B00 /* Bucket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662410492104A42C00013B00 /* Bucket.swift */; }; 6624104E2104AF2100013B00 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6624104D2104AF2100013B00 /* Extensions.swift */; }; + 6624105421075FDC00013B00 /* ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6624105221075F8A00013B00 /* ExtensionsTests.swift */; }; + 66241056210761F000013B00 /* BucketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66241055210761F000013B00 /* BucketTests.swift */; }; + 662410592107804400013B00 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662410582107804400013B00 /* Lock.swift */; }; 669D215020FE1B7300AFFDF7 /* Interval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669D214F20FE1B7300AFFDF7 /* Interval.swift */; }; 669D215220FE1B8A00AFFDF7 /* Time.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669D215120FE1B8A00AFFDF7 /* Time.swift */; }; 669D215420FE1BA600AFFDF7 /* Period.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669D215320FE1BA600AFFDF7 /* Period.swift */; }; @@ -58,9 +65,16 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 62661F402108433300055501 /* WeakSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakSet.swift; sourceTree = ""; }; + 62EF93F821085309001F7A47 /* WeakSetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakSetTests.swift; sourceTree = ""; }; + 62EF93FD21086411001F7A47 /* TaskCenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskCenterTests.swift; sourceTree = ""; }; + 62EF940721086F63001F7A47 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = ""; }; 662410492104A42C00013B00 /* Bucket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bucket.swift; sourceTree = ""; }; 6624104D2104AF2100013B00 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 66241051210617CD00013B00 /* Schedule.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = Schedule.podspec; sourceTree = ""; }; + 6624105221075F8A00013B00 /* ExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionsTests.swift; sourceTree = ""; }; + 66241055210761F000013B00 /* BucketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BucketTests.swift; sourceTree = ""; }; + 662410582107804400013B00 /* Lock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lock.swift; sourceTree = ""; }; 669D214F20FE1B7300AFFDF7 /* Interval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interval.swift; sourceTree = ""; }; 669D215120FE1B8A00AFFDF7 /* Time.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Time.swift; sourceTree = ""; }; 669D215320FE1BA600AFFDF7 /* Period.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Period.swift; sourceTree = ""; }; @@ -99,11 +113,31 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 62EF93FB21085312001F7A47 /* Utils */ = { + isa = PBXGroup; + children = ( + 6624105221075F8A00013B00 /* ExtensionsTests.swift */, + 66241055210761F000013B00 /* BucketTests.swift */, + 62EF93F821085309001F7A47 /* WeakSetTests.swift */, + ); + name = Utils; + sourceTree = ""; + }; + 62EF93FC210863FA001F7A47 /* DateTime */ = { + isa = PBXGroup; + children = ( + OBJ_15 /* DateTimeTests.swift */, + ); + name = DateTime; + sourceTree = ""; + }; 662410462104A0EC00013B00 /* Utils */ = { isa = PBXGroup; children = ( 662410492104A42C00013B00 /* Bucket.swift */, 6624104D2104AF2100013B00 /* Extensions.swift */, + 662410582107804400013B00 /* Lock.swift */, + 62661F402108433300055501 /* WeakSet.swift */, ); name = Utils; sourceTree = ""; @@ -140,10 +174,12 @@ OBJ_14 /* ScheduleTests */ = { isa = PBXGroup; children = ( - OBJ_15 /* DateTimeTests.swift */, + 62EF93FC210863FA001F7A47 /* DateTime */, + 62EF93FB21085312001F7A47 /* Utils */, OBJ_16 /* ScheduleTests.swift */, OBJ_17 /* Util.swift */, OBJ_18 /* XCTestManifests.swift */, + 62EF93FD21086411001F7A47 /* TaskCenterTests.swift */, ); name = ScheduleTests; path = Tests/ScheduleTests; @@ -185,6 +221,7 @@ OBJ_10 /* Task.swift */, 669D215A20FE1C0E00AFFDF7 /* TaskCenter.swift */, OBJ_11 /* Schedule.swift */, + 62EF940721086F63001F7A47 /* Timeline.swift */, 662410462104A0EC00013B00 /* Utils */, ); name = Schedule; @@ -277,13 +314,16 @@ 669D215220FE1B8A00AFFDF7 /* Time.swift in Sources */, 669D215020FE1B7300AFFDF7 /* Interval.swift in Sources */, 669D215620FE1BB400AFFDF7 /* Weekday.swift in Sources */, + 62EF940821086F63001F7A47 /* Timeline.swift in Sources */, OBJ_28 /* Task.swift in Sources */, OBJ_29 /* Schedule.swift in Sources */, + 62661F412108433400055501 /* WeakSet.swift in Sources */, 669D215820FE1BC000AFFDF7 /* Monthday.swift in Sources */, 669D215B20FE1C0E00AFFDF7 /* TaskCenter.swift in Sources */, 669D215D20FE1D1800AFFDF7 /* ParasiticTask.swift in Sources */, 6624104E2104AF2100013B00 /* Extensions.swift in Sources */, 6624104A2104A42C00013B00 /* Bucket.swift in Sources */, + 662410592107804400013B00 /* Lock.swift in Sources */, 669D215420FE1BA600AFFDF7 /* Period.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -300,10 +340,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 0; files = ( + 6624105421075FDC00013B00 /* ExtensionsTests.swift in Sources */, + 62EF93FA2108530D001F7A47 /* WeakSetTests.swift in Sources */, OBJ_48 /* DateTimeTests.swift in Sources */, OBJ_49 /* ScheduleTests.swift in Sources */, OBJ_50 /* Util.swift in Sources */, OBJ_51 /* XCTestManifests.swift in Sources */, + 66241056210761F000013B00 /* BucketTests.swift in Sources */, + 62EF93FF21086420001F7A47 /* TaskCenterTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/Schedule/Bucket.swift b/Sources/Schedule/Bucket.swift index aefe4a4..f8b5ef7 100644 --- a/Sources/Schedule/Bucket.swift +++ b/Sources/Schedule/Bucket.swift @@ -24,36 +24,50 @@ struct Bucket { private var nextKey = BucketKey(rawValue: 0) - private var elements: [BucketKey: Element] = [:] + typealias Entry = (key: BucketKey, element: Element) + private var entries: [Entry] = [] @discardableResult - mutating func insert(_ new: Element) -> BucketKey { + mutating func add(_ new: Element) -> BucketKey { let key = nextKey nextKey = BucketKey(rawValue: nextKey.rawValue &+ 1) - elements[key] = new + entries.append((key: key, element: new)) return key } + func element(for key: BucketKey) -> Element? { + for entry in entries { + if entry.key == key { + return entry.element + } + } + return nil + } + @discardableResult - mutating func removeElement(byKey key: BucketKey) -> Element? { - return elements.removeValue(forKey: key) + mutating func removeElement(for key: BucketKey) -> Element? { + for i in 0.. AnyIterator { - var iterator = elements.makeIterator() - return AnyIterator { - iterator.next()?.value - } + return AnyIterator(entries.map({ $0.element }).makeIterator()) } } diff --git a/Sources/Schedule/Extensions.swift b/Sources/Schedule/Extensions.swift index d04d800..656144c 100644 --- a/Sources/Schedule/Extensions.swift +++ b/Sources/Schedule/Extensions.swift @@ -7,30 +7,24 @@ import Foundation -extension FixedWidthInteger { +extension Int { - func clampedAdding(_ other: Self) -> Self { + func clampedAdding(_ other: Int) -> Int { let r = addingReportingOverflow(other) return r.overflow ? (other > 0 ? .max : .min) : r.partialValue } - func clampedSubtracting(_ other: Self) -> Self { + func clampedSubtracting(_ other: Int) -> Int { let r = subtractingReportingOverflow(other) - return r.overflow ? (other > 0 ? .max : .min) : r.partialValue + return r.overflow ? (other > 0 ? .min : .max) : r.partialValue } } -extension String { +extension Double { - func matches(pattern: String) -> [String] { - guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { - return [] - } - let matches = regex.matches(in: self, options: [], range: NSRange(location: 0, length: count)) - guard let match = matches.first else { return [] } - - return (0.. Int { + if self > Double(Int.max) { return Int.max } + if self < Double(Int.min) { return Int.min } + return Int(self) } } diff --git a/Sources/Schedule/Interval.swift b/Sources/Schedule/Interval.swift index e49083e..f7c1b82 100644 --- a/Sources/Schedule/Interval.swift +++ b/Sources/Schedule/Interval.swift @@ -7,15 +7,22 @@ import Foundation -/// `Interval` represents a duration of time. +/// `Interval` represents a length of time. +/// +/// The value range of interval is [Int.min.nanoseconds...Int.max.nanoseconds], +/// that is, about -292.years ~ 292.years, enough for us! public struct Interval { - /// The length of this interval, measured in nanoseconds. - public let nanoseconds: Double + let ns: Int + + /// The length of this interval in nanoseconds. + public var nanoseconds: Double { + return Double(ns) + } /// Creates an interval from the given number of nanoseconds. public init(nanoseconds: Double) { - self.nanoseconds = nanoseconds + self.ns = nanoseconds.clampedToInt() } /// A boolean value indicating whether this interval is negative. @@ -26,27 +33,31 @@ public struct Interval { /// but the interval between 7:00 and 6:00 is `-1.hour`. /// In this case, `-1.hour` means **one hour ago**. /// - /// - The interval comparing `3.hour` and `1.hour` is `2.hour`, - /// but the interval comparing `1.hour` and `3.hour` is `-2.hour`. + /// - The interval comparing `3.hour` to `1.hour` is `2.hour`, + /// but the interval comparing `1.hour` to `3.hour` is `-2.hour`. /// In this case, `-2.hour` means **two hours shorter** public var isNegative: Bool { - return nanoseconds.isLess(than: 0) + return ns < 0 } - /// The magnitude of this interval. - /// - /// It's the absolute value of the length of this interval, + /// The absolute value of the length of this interval, /// measured in nanoseconds, but disregarding its sign. - public var magnitude: Double { - return nanoseconds.magnitude + public var magnitude: UInt { + return ns.magnitude } +} + + +extension Interval { - /// Returns a boolean value indicating whether this interval is longer than the given value. + /// Returns a boolean value indicating whether this interval is longer + /// than the given value. public func isLonger(than other: Interval) -> Bool { return magnitude > other.magnitude } - /// Returns a boolean value indicating whether this interval is shorter than the given value. + /// Returns a boolean value indicating whether this interval is shorter + /// than the given value. public func isShorter(than other: Interval) -> Bool { return magnitude < other.magnitude } @@ -87,30 +98,30 @@ extension Interval { /// Creates an interval from the given number of seconds. public init(seconds: Double) { - self.nanoseconds = seconds * pow(10, 9) + self.init(nanoseconds: seconds * pow(10, 9)) } - /// The length of this interval, measured in seconds. + /// The length of this interval in seconds. public var seconds: Double { return nanoseconds / pow(10, 9) } - /// The length of this interval, measured in minutes. + /// The length of this interval in minutes. public var minutes: Double { return seconds / 60 } - /// The length of this interval, measured in hours. + /// The length of this interval in hours. public var hours: Double { return minutes / 60 } - /// The length of this interval, measured in days. + /// The length of this interval in days. public var days: Double { return hours / 24 } - /// The length of this interval, measured in weeks. + /// The length of this interval in weeks. public var weeks: Double { return days / 7 } @@ -131,53 +142,48 @@ extension Interval: Hashable { extension Date { - /// The interval between this date and now. + /// The interval between this date and the current date and time. /// - /// If the date is earlier than now, the interval is negative. + /// If this date is earlier than now, the interval will be negative. public var intervalSinceNow: Interval { return timeIntervalSinceNow.seconds } /// Returns the interval between this date and the given date. /// - /// If the date is earlier than the given date, the interval is negative. + /// If this date is earlier than the given date, the interval will be negative. public func interval(since date: Date) -> Interval { return timeIntervalSince(date).seconds } - /// Returns a new date by adding an interval to the date. + /// Returns a new date by adding an interval to this date. public func addingInterval(_ interval: Interval) -> Date { return addingTimeInterval(interval.seconds) } - /// Returns a new date by adding an interval to the date. + /// Returns a date with an interval added to it. public static func +(lhs: Date, rhs: Interval) -> Date { return lhs.addingInterval(rhs) } - /// Adds an interval to the date. + /// Adds a interval to the date. public static func +=(lhs: inout Date, rhs: Interval) { lhs = lhs + rhs } } -extension Interval { - - var ns: Int { - if nanoseconds > Double(Int.max) { return .max } - if nanoseconds < Double(Int.min) { return .min } - return Int(nanoseconds) - } -} - extension DispatchSourceTimer { func schedule(after interval: Interval) { + guard !interval.isNegative else { + schedule(wallDeadline: .distantFuture) + return + } schedule(wallDeadline: .now() + DispatchTimeInterval.nanoseconds(interval.ns)) } } -/// `IntervalConvertible` provides a set of intuitive apis for creating interval. +/// `IntervalConvertible` provides a set of intuitive api for creating interval. public protocol IntervalConvertible { var nanoseconds: Interval { get } diff --git a/Sources/Schedule/Lock.swift b/Sources/Schedule/Lock.swift new file mode 100644 index 0000000..124fb89 --- /dev/null +++ b/Sources/Schedule/Lock.swift @@ -0,0 +1,44 @@ +// +// Lock.swift +// Schedule +// +// Created by Quentin Jin on 2018/7/24. +// + +import Foundation + +final class Lock { + + private let mutex = UnsafeMutablePointer.allocate(capacity: 1) + + init() { + let err = pthread_mutex_init(mutex, nil) + precondition(err == 0) + } + + deinit { + let err = pthread_mutex_destroy(mutex) + precondition(err == 0) + mutex.deallocate() + } + + func lock() { + let err = pthread_mutex_lock(mutex) + precondition(err == 0) + } + + func unlock() { + let err = pthread_mutex_unlock(mutex) + precondition(err == 0) + } +} + +extension Lock { + + @inline(__always) + func withLock(_ body: () throws -> T) rethrows -> T { + lock() + defer { unlock() } + return try body() + } +} diff --git a/Sources/Schedule/Period.swift b/Sources/Schedule/Period.swift index 42dc248..a8228f0 100644 --- a/Sources/Schedule/Period.swift +++ b/Sources/Schedule/Period.swift @@ -16,8 +16,10 @@ import Foundation /// /// If you add a period `1.month` to the 1st January, /// you will get the 1st February. +/// /// If you add the same period to the 1st February, /// you will get the 1st March. +/// /// But the intervals(`31.days` in case 1, `28.days` or `29.days` in case 2) /// in these two cases are quite different. public struct Period { @@ -59,12 +61,12 @@ public struct Period { nanoseconds: lhs.nanoseconds.clampedAdding(rhs.nanoseconds)) } - /// Returns a new date by adding the right period to the left date. + /// Returns a date with a period added to it. public static func +(lhs: Date, rhs: Period) -> Date { return Calendar.autoupdatingCurrent.date(byAdding: rhs.asDateComponents(), to: lhs) ?? .distantFuture } - /// Returns a new period by adding the right interval to the left period. + /// Return a period with a interval added to it. public static func +(lhs: Period, rhs: Interval) -> Period { return Period(years: lhs.years, months: lhs.months, days: lhs.days, hours: lhs.hours, minutes: lhs.minutes, seconds: lhs.seconds, @@ -80,22 +82,18 @@ public struct Period { extension Int { - /// Period by setting years to this value. public var years: Period { return Period(years: self) } - /// Period by setting years to this value. public var year: Period { return years } - /// Period by setting months to this value. public var months: Period { return Period(months: self) } - /// Period by setting months to this value. public var month: Period { return months } diff --git a/Sources/Schedule/Task.swift b/Sources/Schedule/Task.swift index 698f085..0118e57 100644 --- a/Sources/Schedule/Task.swift +++ b/Sources/Schedule/Task.swift @@ -7,6 +7,7 @@ import Foundation +/// `ActionKey` represents a token that can be used to operate action. public protocol ActionKey { var underlying: UInt64 { get } } @@ -17,127 +18,133 @@ extension BucketKey: ActionKey { } } -/// `Task` represents a series of actions to be scheduled. +/// `Task` represents a job to be scheduled. public class Task { - /// The timstamp last time this task was scheduled at. - public var lastSchedule: Date? { - lock.lock() - defer { lock.unlock() } - return _lastSchedule - } - private var _lastSchedule: Date? + private let _lock = Lock() - /// The timestamp next time this task will be scheduled at. - public var nextSchedule: Date? { - lock.lock() - defer { lock.unlock() } - return deadline - } + private var _iterator: AnyIterator - /// All tags associate with this task. - public var tags: Set { - lock.lock() - defer { lock.unlock() } - return _tags - } - private var _tags: Set = [] - - private lazy var lock = NSRecursiveLock() - - private var iterator: AnyIterator - - private var deadline: Date! + private var _timer: DispatchSourceTimer? private typealias Action = (Task) -> Void - private var actions = Bucket.empty - - private var timer: DispatchSourceTimer! + private lazy var _actions = Bucket() - private var suspensions: UInt64 = 0 + private lazy var _suspensions: UInt64 = 0 + private lazy var _timeline = Timeline() + private lazy var _tags: Set = [] init(schedule: Schedule, queue: DispatchQueue? = nil, tag: String? = nil, onElapse: @escaping (Task) -> Void) { - self.iterator = schedule.makeIterator() - guard let interval = self.iterator.next() else { - return - } - self.deadline = Date() + interval - self.actions.insert(onElapse) - self.timer = DispatchSource.makeTimerSource(queue: queue) - self.timer.setEventHandler { [weak self] in + self._iterator = schedule.makeIterator() + + guard let interval = self._iterator.next() else { return } + + self._timer = DispatchSource.makeTimerSource(queue: queue) + + self._actions.add(onElapse) + self._timer?.setEventHandler { [weak self] in self?.elapse() } - self.timer.schedule(after: interval) - self.timer.resume() + self._timer?.schedule(after: interval) + + let now = Date() + self._timeline.activate = now + self._timeline.nextSchedule = now.addingInterval(interval) TaskCenter.shared.add(self, withTag: tag) + + self._timer?.resume() } private func elapse() { + _lock.lock() let now = Date() + if _timeline.firstSchedule == nil { + _timeline.firstSchedule = now + } + _timeline.lastSchedule = now - lock.lock() - _lastSchedule = now - guard let interval = iterator.next(), !interval.isNegative else { - deadline = nil - let _actions = actions - lock.unlock() - _actions.forEach { $0(self) } + guard let interval = _iterator.next() else { + _timeline.nextSchedule = nil + let actions = _actions + _lock.unlock() + actions.forEach { $0(self) } return } - deadline = deadline.addingInterval(interval) - let _actions = actions - timer.schedule(after: interval) - lock.unlock() - _actions.forEach { $0(self) } + _timeline.nextSchedule = _timeline.nextSchedule?.addingInterval(interval) + + _timer?.schedule(after: (_timeline.nextSchedule ?? Date.distantFuture).interval(since: now)) + let actions = _actions + _lock.unlock() + + actions.forEach { $0(self) } + } + + /// The timeline of this task. + public var timeline: Timeline { + return _lock.withLock { + _timeline + } + } + + /// All tags associated with this task. + public var tags: Set { + return _lock.withLock { + _tags + } } /// Reschedules this task with the new schedule. public func reschedule(_ new: Schedule) { - lock.lock() - iterator = new.makeIterator() - lock.unlock() + _lock.withLock { + _iterator = new.makeIterator() + } } /// Suspends this task. public func suspend() { - lock.lock() - if suspensions < UInt64.max { - suspensions += 1 - timer.suspend() + guard let timer = _timer else { return } + _lock.withLock { + if _suspensions < UInt64.max { + timer.suspend() + _suspensions += 1 + } } - lock.unlock() } /// Resumes this task. public func resume() { - lock.lock() - if suspensions > 0 { - suspensions -= 1 - timer.resume() + guard let timer = _timer else { return } + _lock.withLock { + if _suspensions > 0 { + timer.resume() + _suspensions -= 1 + } } - lock.unlock() } /// Cancels this task. public func cancel() { - lock.lock() - timer.cancel() - lock.unlock() + guard let timer = _timer else { return } + _lock.withLock { + timer.cancel() + _timeline.cancel = Date() + } TaskCenter.shared.remove(self) } deinit { - lock.lock() - while suspensions > 0 { - suspensions -= 1 - timer.resume() + guard let timer = _timer else { return } + _lock.withLock { + while _suspensions > 0 { + timer.resume() + _suspensions -= 1 + } } - lock.unlock() cancel() } } @@ -145,27 +152,26 @@ public class Task { extension Task { - /// Adds an action to this task. + /// Adds the action to this task. @discardableResult public func addAction(_ action: @escaping (Task) -> Void) -> ActionKey { - lock.lock() - let key = actions.insert(action) - lock.unlock() - return key + return _lock.withLock { + _actions.add(action) + } } - /// Removes the key's corresponding action from this task. + /// Removes action by key from this task. public func removeAction(byKey key: ActionKey) { - lock.lock() - actions.removeElement(byKey: BucketKey(rawValue: key.underlying)) - lock.unlock() + _lock.withLock { + _ = _actions.removeElement(for: BucketKey(rawValue: key.underlying)) + } } /// Removes all actions from this task. public func removeAllActions() { - lock.lock() - actions.removeAll() - lock.unlock() + _lock.withLock { + _actions.removeAll() + } } } @@ -173,18 +179,18 @@ extension Task { /// Adds tags to this task. public func addTags(_ tags: [String]) { - let set = Set(tags) + var set = Set(tags) - lock.lock() - let intersection = set.intersection(self._tags) - guard intersection.count > 0 else { - lock.unlock() + _lock.lock() + set.subtract(_tags) + guard set.count > 0 else { + _lock.unlock() return } - self._tags.formUnion(intersection) - lock.unlock() + _tags.formUnion(set) + _lock.unlock() - for tag in intersection { + for tag in set { TaskCenter.shared.add(tag: tag, to: self) } } @@ -201,19 +207,17 @@ extension Task { /// Removes tags from this task. public func removeTags(_ tags: [String]) { - let set = Set(tags) - lock.lock() - let intersection = set.intersection(self._tags) - guard intersection.count > 0 else { - lock.unlock() + var set = Set(tags) + _lock.lock() + set.formIntersection(_tags) + guard set.count > 0 else { + _lock.unlock() return } - for tag in intersection { - self._tags.insert(tag) - } - lock.unlock() + _tags.subtract(set) + _lock.unlock() - for tag in intersection { + for tag in set { TaskCenter.shared.remove(tag: tag, from: self) } } diff --git a/Sources/Schedule/TaskCenter.swift b/Sources/Schedule/TaskCenter.swift index 17c55ba..c9efe03 100644 --- a/Sources/Schedule/TaskCenter.swift +++ b/Sources/Schedule/TaskCenter.swift @@ -13,56 +13,53 @@ final class TaskCenter { private init() { } - private var lock = NSLock() + private var lock = Lock() private var tasks: Set = [] - private var tags: [String: NSHashTable] = [:] + private var registry: [String: WeakSet] = [:] func add(_ task: Task, withTag tag: String? = nil) { - lock.lock() - defer { lock.unlock() } - - tasks.insert(task) - - if let tag = tag { - if tags[tag] == nil { - tags[tag] = NSHashTable(options: .weakMemory) + lock.withLock { + tasks.insert(task) + if let tag = tag { + if registry[tag] == nil { + registry[tag] = WeakSet() + } + registry[tag]?.add(task) } - weak var t = task - tags[tag]?.add(t) } } func remove(_ task: Task) { - lock.lock() - defer { lock.unlock() } - - tasks.remove(task) + lock.withLock { + _ = tasks.remove(task) + } } func add(tag: String, to task: Task) { - lock.lock() - defer { lock.unlock() } - - if tags[tag] == nil { - tags[tag] = NSHashTable(options: .weakMemory) + lock.withLock { + if registry[tag] == nil { + registry[tag] = WeakSet() + } + registry[tag]?.add(task) } - weak var t = task - tags[tag]?.add(t) } func remove(tag: String, from task: Task) { - lock.lock() - defer { lock.unlock() } - - tags[tag]?.remove(task) + lock.withLock { + _ = registry[tag]?.remove(task) + } } func tasks(forTag tag: String) -> [Task] { - lock.lock() - defer { lock.unlock() } - - return tags[tag]?.allObjects ?? [] + return lock.withLock { + registry[tag]?.objects ?? [] + } + } + + func contains(_ task: Task) -> Bool { + return lock.withLock { + tasks.contains(task) + } } } - diff --git a/Sources/Schedule/Time.swift b/Sources/Schedule/Time.swift index dd7b760..3d9a26a 100644 --- a/Sources/Schedule/Time.swift +++ b/Sources/Schedule/Time.swift @@ -10,13 +10,13 @@ import Foundation /// `Time` represents a time without a date. public struct Time { - /// Hour of this time, max is 23, min is 0. + /// Hour of this time. public let hour: Int - /// Minute of this time, max is 59, min is 0. + /// Minute of this time. public let minute: Int - /// Second of this time, max is 59, min is 0. + /// Second of this time. public let second: Int /// Nanosecond of this time. diff --git a/Sources/Schedule/Timeline.swift b/Sources/Schedule/Timeline.swift new file mode 100644 index 0000000..d66ef37 --- /dev/null +++ b/Sources/Schedule/Timeline.swift @@ -0,0 +1,29 @@ +// +// Timeline.swift +// Schedule +// +// Created by Quentin Jin on 2018/7/25. +// + +import Foundation + +/// `Timeline` records a task's schedule. +public struct Timeline { + + /// The time when the first time task was scheduled. + public internal(set) var firstSchedule: Date? + + /// The time when the last time task was scheduled. + public internal(set) var lastSchedule: Date? + + /// The time when the next time task will be scheduled. + public internal(set) var nextSchedule: Date? + + /// The time when task was activated. + public internal(set) var activate: Date? + + /// The time when task was canceled. + public internal(set) var cancel: Date? + + init() { } +} diff --git a/Sources/Schedule/WeakSet.swift b/Sources/Schedule/WeakSet.swift new file mode 100644 index 0000000..2faf842 --- /dev/null +++ b/Sources/Schedule/WeakSet.swift @@ -0,0 +1,60 @@ +// +// WeakSet.swift +// Schedule +// +// Created by Quentin Jin on 2018/7/25. +// + +import Foundation + +struct WeakBox { + weak var underlying: T? + init(_ value: T) { + self.underlying = value + } +} + +extension WeakBox: Hashable { + + var hashValue: Int { + guard let value = self.underlying else { return 0 } + return ObjectIdentifier(value).hashValue + } + + static func == (lhs: WeakBox, rhs: WeakBox) -> Bool { + return lhs.hashValue == rhs.hashValue + } +} + +struct WeakSet { + + private var set = Set>() + + mutating func add(_ object: T) { + self.set.insert(WeakBox(object)) + } + + @discardableResult + mutating func remove(_ object: T) -> T? { + return self.set.remove(WeakBox(object))?.underlying + } + + func contains(_ object: T) -> Bool { + return set.contains(WeakBox(object)) + } + + var objects: [T] { + return self.set.map { $0.underlying }.compactMap { $0 } + } + + var count: Int { + return self.objects.count + } +} + +extension WeakSet: Sequence { + + func makeIterator() -> AnyIterator { + return AnyIterator(objects.makeIterator()) + } +} diff --git a/Tests/ScheduleTests/BucketTests.swift b/Tests/ScheduleTests/BucketTests.swift new file mode 100644 index 0000000..26b7107 --- /dev/null +++ b/Tests/ScheduleTests/BucketTests.swift @@ -0,0 +1,61 @@ +// +// BucketTests.swift +// ScheduleTests +// +// Created by Quentin Jin on 2018/7/24. +// + +import XCTest +@testable import Schedule + +final class BucketTests: XCTestCase { + + typealias Fn = () -> Int + + func testAdd() { + var bucket = Bucket() + let key = bucket.add({ 1 }) + let element = bucket.element(for: key) + XCTAssertNotNil(element) + guard let fn = element else { return } + XCTAssertEqual(fn(), 1) + } + + func testRemove() { + var bucket = Bucket() + let k0 = bucket.add { 0 } + bucket.add { 1 } + bucket.add { 2 } + XCTAssertEqual(bucket.count, 3) + + let e0 = bucket.removeElement(for: k0) + XCTAssertNotNil(e0) + + guard let fn0 = e0 else { return } + XCTAssertEqual(fn0(), 0) + + XCTAssertEqual(bucket.count, 2) + + bucket.removeAll() + XCTAssertEqual(bucket.count, 0) + } + + func testSequence() { + var bucket = Bucket() + bucket.add { 0 } + bucket.add { 1 } + bucket.add { 2 } + + var i = 0 + for fn in bucket { + XCTAssertEqual(fn(), i) + i += 1 + } + } + + static var allTests = [ + ("testAdd", testAdd), + ("testRemove", testRemove), + ("testSequence", testSequence) + ] +} diff --git a/Tests/ScheduleTests/DateTimeTests.swift b/Tests/ScheduleTests/DateTimeTests.swift index 04b9189..c6cd7a1 100644 --- a/Tests/ScheduleTests/DateTimeTests.swift +++ b/Tests/ScheduleTests/DateTimeTests.swift @@ -10,14 +10,6 @@ import XCTest final class DateTimeTests: XCTestCase { - func testInterval2DispatchInterval() { - let i0 = 1.23.seconds - XCTAssertEqual(i0.asDispatchTimeInterval(), DispatchTimeInterval.nanoseconds(Int(i0.nanoseconds))) - - let i1 = 4.56.minutes + 7.89.hours - XCTAssertEqual(i1.asDispatchTimeInterval(), DispatchTimeInterval.nanoseconds(Int(i1.nanoseconds))) - } - func testIntervalConvertible() { XCTAssertEqual(1.nanoseconds, Interval(nanoseconds: 1)) XCTAssertEqual(2.microseconds, Interval(nanoseconds: 2 * K.ns_per_us)) @@ -53,7 +45,6 @@ final class DateTimeTests: XCTestCase { } static var allTests = [ - ("testInterval2DispatchInterval", testInterval2DispatchInterval), ("testInterval", testIntervalConvertible), ("testTimeConstructor", testTimeConstructor), ("testPeriodAnd", testPeriodAnd), diff --git a/Tests/ScheduleTests/ExtensionsTests.swift b/Tests/ScheduleTests/ExtensionsTests.swift new file mode 100644 index 0000000..d9d0f1f --- /dev/null +++ b/Tests/ScheduleTests/ExtensionsTests.swift @@ -0,0 +1,29 @@ +// +// ExtensionsTests.swift +// Schedule +// +// Created by Quentin Jin on 2018/7/24. +// + +import XCTest +@testable import Schedule + +final class ExtensionsTests: XCTestCase { + + func testClampedAdding() { + let a: Int = 1 + let b: Int = .max + XCTAssertEqual(a.clampedAdding(b), Int.max) + } + + func testClampedSubtracting() { + let a: Int = .min + let b: Int = 1 + XCTAssertEqual(a.clampedSubtracting(b), Int.min) + } + + static var allTests = [ + ("testClampedAdding", testClampedAdding), + ("testClampedSubtracting", testClampedSubtracting) + ] +} diff --git a/Tests/ScheduleTests/TaskCenterTests.swift b/Tests/ScheduleTests/TaskCenterTests.swift new file mode 100644 index 0000000..18d774b --- /dev/null +++ b/Tests/ScheduleTests/TaskCenterTests.swift @@ -0,0 +1,47 @@ +// +// TaskCenterTests.swift +// Schedule +// +// Created by Quentin Jin on 2018/7/25. +// + +import XCTest +@testable import Schedule + +final class TaskCenterTests: XCTestCase { + + func makeTask() -> Task { + return Schedule.never.do { } + } + + var center: TaskCenter { + return TaskCenter.shared + } + + func testAdd() { + let task = makeTask() + center.add(task) + XCTAssertTrue(center.contains(task)) + } + + func testRemove() { + let task = makeTask() + center.add(task) + center.remove(task) + XCTAssertFalse(center.contains(task)) + } + + func testTag() { + let task = makeTask() + let tag0 = UUID().uuidString + center.add(task, withTag: tag0) + XCTAssertTrue(center.tasks(forTag: tag0).contains(task)) + + let tag1 = UUID().uuidString + center.add(tag: tag1, to: task) + XCTAssertTrue(center.tasks(forTag: tag1).contains(task)) + + center.remove(tag: tag0, from: task) + XCTAssertFalse(center.tasks(forTag: tag0).contains(task)) + } +} diff --git a/Tests/ScheduleTests/WeakSetTests.swift b/Tests/ScheduleTests/WeakSetTests.swift new file mode 100644 index 0000000..8777577 --- /dev/null +++ b/Tests/ScheduleTests/WeakSetTests.swift @@ -0,0 +1,44 @@ +// +// WeakSetTests.swift +// Schedule +// +// Created by Quentin Jin on 2018/7/25. +// + +import XCTest +@testable import Schedule + +private class Object { } + +final class WeakSetTests: XCTestCase { + + func testAdd() { + var set = WeakSet() + + let block = { + let obj = Object() + set.add(obj) + } + + block() + XCTAssertEqual(set.count, 0) + + let obj = Object() + set.add(obj) + set.add(obj) + XCTAssertEqual(set.count, 1) + } + + func testRemove() { + var set = WeakSet() + let obj = Object() + set.add(obj) + set.remove(obj) + XCTAssertEqual(set.count, 0) + } + + static var allTests = [ + ("testAdd", testAdd), + ("testRemove", testRemove) + ] +}