Remove Objc dependency, and improve documents.

This commit is contained in:
QuentinJin 2018-07-25 23:50:21 +08:00
parent 6ac74e786c
commit f48758d4f7
16 changed files with 580 additions and 218 deletions

View File

@ -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 = "<group>"; };
62EF93F821085309001F7A47 /* WeakSetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakSetTests.swift; sourceTree = "<group>"; };
62EF93FD21086411001F7A47 /* TaskCenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskCenterTests.swift; sourceTree = "<group>"; };
62EF940721086F63001F7A47 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = "<group>"; };
662410492104A42C00013B00 /* Bucket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bucket.swift; sourceTree = "<group>"; };
6624104D2104AF2100013B00 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
66241051210617CD00013B00 /* Schedule.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = Schedule.podspec; sourceTree = "<group>"; };
6624105221075F8A00013B00 /* ExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionsTests.swift; sourceTree = "<group>"; };
66241055210761F000013B00 /* BucketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BucketTests.swift; sourceTree = "<group>"; };
662410582107804400013B00 /* Lock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lock.swift; sourceTree = "<group>"; };
669D214F20FE1B7300AFFDF7 /* Interval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interval.swift; sourceTree = "<group>"; };
669D215120FE1B8A00AFFDF7 /* Time.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Time.swift; sourceTree = "<group>"; };
669D215320FE1BA600AFFDF7 /* Period.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Period.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
};
62EF93FC210863FA001F7A47 /* DateTime */ = {
isa = PBXGroup;
children = (
OBJ_15 /* DateTimeTests.swift */,
);
name = DateTime;
sourceTree = "<group>";
};
662410462104A0EC00013B00 /* Utils */ = {
isa = PBXGroup;
children = (
662410492104A42C00013B00 /* Bucket.swift */,
6624104D2104AF2100013B00 /* Extensions.swift */,
662410582107804400013B00 /* Lock.swift */,
62661F402108433300055501 /* WeakSet.swift */,
);
name = Utils;
sourceTree = "<group>";
@ -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;
};

View File

@ -24,36 +24,50 @@ struct Bucket<Element> {
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..<entries.count {
if entries[i].key == key {
let element = entries[i].element
entries.remove(at: i)
return element
}
}
return nil
}
mutating func removeAll() {
elements.removeAll()
entries.removeAll()
}
static var empty: Bucket {
return Bucket()
var count: Int {
return entries.count
}
}
extension Bucket: Sequence {
func makeIterator() -> AnyIterator<Element> {
var iterator = elements.makeIterator()
return AnyIterator {
iterator.next()?.value
}
return AnyIterator(entries.map({ $0.element }).makeIterator())
}
}

View File

@ -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..<match.numberOfRanges).reduce(into: [String]()) { (r, i) in
r.append((self as NSString).substring(with: match.range(at: i)))
}
func clampedToInt() -> Int {
if self > Double(Int.max) { return Int.max }
if self < Double(Int.min) { return Int.min }
return Int(self)
}
}

View File

@ -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
}
}
/// Returns a boolean value indicating whether this interval is longer than the given value.
extension Interval {
/// 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 }

View File

@ -0,0 +1,44 @@
//
// Lock.swift
// Schedule
//
// Created by Quentin Jin on 2018/7/24.
//
import Foundation
final class Lock {
private let mutex = UnsafeMutablePointer<pthread_mutex_t>.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<T>(_ body: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try body()
}
}

View File

@ -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
}

View File

@ -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<Interval>
/// All tags associate with this task.
public var tags: Set<String> {
lock.lock()
defer { lock.unlock() }
return _tags
}
private var _tags: Set<String> = []
private lazy var lock = NSRecursiveLock()
private var iterator: AnyIterator<Interval>
private var deadline: Date!
private var _timer: DispatchSourceTimer?
private typealias Action = (Task) -> Void
private var actions = Bucket<Action>.empty
private lazy var _actions = Bucket<Action>()
private var timer: DispatchSourceTimer!
private var suspensions: UInt64 = 0
private lazy var _suspensions: UInt64 = 0
private lazy var _timeline = Timeline()
private lazy var _tags: Set<String> = []
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<String> {
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
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
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()
guard let timer = _timer else { return }
_lock.withLock {
timer.cancel()
lock.unlock()
_timeline.cancel = Date()
}
TaskCenter.shared.remove(self)
}
deinit {
lock.lock()
while suspensions > 0 {
suspensions -= 1
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)
}
}

View File

@ -13,56 +13,53 @@ final class TaskCenter {
private init() { }
private var lock = NSLock()
private var lock = Lock()
private var tasks: Set<Task> = []
private var tags: [String: NSHashTable<Task>] = [:]
private var registry: [String: WeakSet<Task>] = [:]
func add(_ task: Task, withTag tag: String? = nil) {
lock.lock()
defer { lock.unlock() }
lock.withLock {
tasks.insert(task)
if let tag = tag {
if tags[tag] == nil {
tags[tag] = NSHashTable<Task>(options: .weakMemory)
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<Task>(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)
}
}
}

View File

@ -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.

View File

@ -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() { }
}

View File

@ -0,0 +1,60 @@
//
// WeakSet.swift
// Schedule
//
// Created by Quentin Jin on 2018/7/25.
//
import Foundation
struct WeakBox<T: AnyObject> {
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<T>, rhs: WeakBox<T>) -> Bool {
return lhs.hashValue == rhs.hashValue
}
}
struct WeakSet<T: AnyObject> {
private var set = Set<WeakBox<T>>()
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<T> {
return AnyIterator(objects.makeIterator())
}
}

View File

@ -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<Fn>()
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<Fn>()
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<Fn>()
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)
]
}

View File

@ -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),

View File

@ -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)
]
}

View File

@ -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))
}
}

View File

@ -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<Object>()
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<Object>()
let obj = Object()
set.add(obj)
set.remove(obj)
XCTAssertEqual(set.count, 0)
}
static var allTests = [
("testAdd", testAdd),
("testRemove", testRemove)
]
}