diff --git a/.gitignore b/.gitignore index 02c0875..e1ae197 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,67 @@ -.DS_Store -/.build -/Packages -/*.xcodeproj +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output \ No newline at end of file diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..8012ebb --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +4.2 \ No newline at end of file diff --git a/README.md b/README.md index d339ff3..97d7372 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,29 @@ # Schedule -Swift job scheduler. +⏰ A interval-based and date-based job scheduler for swift. +## Features + +- πŸ“†Β Date-based scheduling +- ⏳ Interval-based scheduling +- πŸ“ Mixture rules scheduling +- 🚦 Suspend, resume, cancel +- 🏷 Tag related management +- 🍻 No need to concern about runloop +- πŸ‘» No need to concern about circular reference +- 🍭 **Sweet apis** + ## Usage -### Getting Start - Scheduling a job can not be simplier. ```swift -func job() { } -Schedule.every(1.minute).do(job) +func heartBeat() { } +Schedule.every(0.5.seconds).do(heartBeat) ``` - - ### Interval-based Scheduling ```swift @@ -37,7 +44,7 @@ Schedule.from([1.second, 2.minutes, 3.hours]).do { } -## Date-based Scheduling +### Date-based Scheduling ```swift import Schedule @@ -62,7 +69,7 @@ import Schedule /// concat let s0 = Schedule.at(birthdate) -let s1 = Schedule.every(.year(1)) +let s1 = Schedule.every(1.year) let birthdaySchedule = s0.concat.s1 birthdaySchedule.do { print("Happy birthday") @@ -73,32 +80,65 @@ let s3 = Schedule.every(.january(1)).at(8, 30) let s4 = Schedule.every(.october(1)).at(8, 30) let holiday = s3.merge(s3) holidaySchedule.do { - print("Holiday~") + print("Happy holiday") } -``` +/// count +let s5 = Schedule.after(5.seconds).concat(Schedule.every(1.day)) +let s6 = s5.count(10) + +/// until +let s7 = Schedule.every(.monday).at(11, 12) +let s8 = s7.until(date) +``` ### Job management -Normally, you don't need to care about the reference management of job. All jobs will be held by a inner instance `jobCenter`, so they won't be released. +In genera, you don't need to care about the reference management of job. All jobs will be held by a inner shared instance `jobCenter`, so they won't be released, unless you do that yourself. -If you want everything in your control: ```swift let job = Schedule.every(1.day).do { } -job.suspend() // will suspend job, it won't change job's schedule -job.resume() // will resume the suspended job, it won't change job's schedule -job.cancel() // will cancel job, then job will be released after variable `job` is gone +job.suspend() +job.resume() +job.cancel() // will release this job ``` -You also can use `tag` to organize jobs, and use `queue` to define which queue the job should be dispatched to: +You also can use `tag` to organize jobs, and use `queue` to define which queue the job should be dispatched to: ```swift -Schedule.every(1.day).do(queue: myJobQueue, tag: "remind me") { } +let s = Schedule.every(1.day) +s.do(queue: myJobQueue, tag: "log") { } +s.do(queue: myJobQueue, tag: "log") { } + +Job.suspend("log") +Job.resume("log") +Job.cancel("log") +``` + +## Install + +Schedul supports all popular dependency managers. + +### Cocoapods + +```ruby +pod 'Schedule' +``` + +### Carthage + +```swift +github "jianstm/Schedule" +``` + +### Swift Package Manager + +```swift +dependencies: [ + .package(url: "https://github.com/jianstm/Schedule", from: "0.0.2") +] +``` -Job.suspend("remind me") // will suspend all jobs with this tag -Job.resume("remind me") // will resume all jobs with this tag -Job.cancel("remind me") // will cancel all jobs with this tag -``` \ No newline at end of file diff --git a/Schedule.podspec b/Schedule.podspec index 73def7f..5a7f46c 100644 --- a/Schedule.podspec +++ b/Schedule.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Schedule" - s.version = "0.0.2" + s.version = "0.0.3" s.summary = "Swift Job Schedule." s.homepage = "https://github.com/jianstm/Schedule" s.license = { :type => "MIT", :file => "LICENSE" } @@ -9,10 +9,10 @@ Pod::Spec.new do |s| :tag => "#{s.version}" } s.source_files = "Sources/Schedule/*.swift" s.requires_arc = true - s.swift_version = "4.0" + s.swift_version = "4.2" s.ios.deployment_target = "8.0" - s.osx.deployment_target = "10.9" + s.osx.deployment_target = "10.10" s.tvos.deployment_target = "9.0" s.watchos.deployment_target = "2.0" end \ No newline at end of file diff --git a/Schedule.xcodeproj/ScheduleTests_Info.plist b/Schedule.xcodeproj/ScheduleTests_Info.plist new file mode 100644 index 0000000..7c23420 --- /dev/null +++ b/Schedule.xcodeproj/ScheduleTests_Info.plist @@ -0,0 +1,25 @@ + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/Schedule.xcodeproj/Schedule_Info.plist b/Schedule.xcodeproj/Schedule_Info.plist new file mode 100644 index 0000000..57ada9f --- /dev/null +++ b/Schedule.xcodeproj/Schedule_Info.plist @@ -0,0 +1,25 @@ + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/Schedule.xcodeproj/project.pbxproj b/Schedule.xcodeproj/project.pbxproj new file mode 100644 index 0000000..3c318a2 --- /dev/null +++ b/Schedule.xcodeproj/project.pbxproj @@ -0,0 +1,478 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXAggregateTarget section */ + "Schedule::SchedulePackageTests::ProductTarget" /* SchedulePackageTests */ = { + isa = PBXAggregateTarget; + buildConfigurationList = OBJ_39 /* Build configuration list for PBXAggregateTarget "SchedulePackageTests" */; + buildPhases = ( + ); + dependencies = ( + OBJ_42 /* PBXTargetDependency */, + ); + name = SchedulePackageTests; + productName = SchedulePackageTests; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + OBJ_27 /* DateTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* DateTime.swift */; }; + OBJ_28 /* Job.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* Job.swift */; }; + OBJ_29 /* Schedule.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* Schedule.swift */; }; + OBJ_30 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* Util.swift */; }; + OBJ_37 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; }; + OBJ_48 /* DateTimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* DateTimeTests.swift */; }; + OBJ_49 /* ScheduleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_16 /* ScheduleTests.swift */; }; + OBJ_50 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* Util.swift */; }; + OBJ_51 /* XCTestManifests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_18 /* XCTestManifests.swift */; }; + OBJ_53 /* Schedule.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "Schedule::Schedule::Product" /* Schedule.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 6266004220EF028200D5E606 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = OBJ_1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = "Schedule::Schedule"; + remoteInfo = Schedule; + }; + 6266004320EF028200D5E606 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = OBJ_1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = "Schedule::ScheduleTests"; + remoteInfo = ScheduleTests; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + OBJ_10 /* Job.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Job.swift; sourceTree = ""; }; + OBJ_11 /* Schedule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Schedule.swift; sourceTree = ""; }; + OBJ_12 /* Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Util.swift; sourceTree = ""; }; + OBJ_15 /* DateTimeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeTests.swift; sourceTree = ""; }; + OBJ_16 /* ScheduleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleTests.swift; sourceTree = ""; }; + OBJ_17 /* Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Util.swift; sourceTree = ""; }; + OBJ_18 /* XCTestManifests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestManifests.swift; sourceTree = ""; }; + OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; + OBJ_9 /* DateTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTime.swift; sourceTree = ""; }; + "Schedule::Schedule::Product" /* Schedule.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Schedule.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + "Schedule::ScheduleTests::Product" /* ScheduleTests.xctest */ = {isa = PBXFileReference; lastKnownFileType = file; path = ScheduleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + OBJ_31 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + OBJ_52 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 0; + files = ( + OBJ_53 /* Schedule.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + OBJ_13 /* Tests */ = { + isa = PBXGroup; + children = ( + OBJ_14 /* ScheduleTests */, + ); + name = Tests; + sourceTree = SOURCE_ROOT; + }; + OBJ_14 /* ScheduleTests */ = { + isa = PBXGroup; + children = ( + OBJ_15 /* DateTimeTests.swift */, + OBJ_16 /* ScheduleTests.swift */, + OBJ_17 /* Util.swift */, + OBJ_18 /* XCTestManifests.swift */, + ); + name = ScheduleTests; + path = Tests/ScheduleTests; + sourceTree = SOURCE_ROOT; + }; + OBJ_19 /* Products */ = { + isa = PBXGroup; + children = ( + "Schedule::Schedule::Product" /* Schedule.framework */, + "Schedule::ScheduleTests::Product" /* ScheduleTests.xctest */, + ); + name = Products; + sourceTree = BUILT_PRODUCTS_DIR; + }; + OBJ_5 /* */ = { + isa = PBXGroup; + children = ( + OBJ_6 /* Package.swift */, + OBJ_7 /* Sources */, + OBJ_13 /* Tests */, + OBJ_19 /* Products */, + ); + name = ""; + sourceTree = ""; + }; + OBJ_7 /* Sources */ = { + isa = PBXGroup; + children = ( + OBJ_8 /* Schedule */, + ); + name = Sources; + sourceTree = SOURCE_ROOT; + }; + OBJ_8 /* Schedule */ = { + isa = PBXGroup; + children = ( + OBJ_9 /* DateTime.swift */, + OBJ_10 /* Job.swift */, + OBJ_11 /* Schedule.swift */, + OBJ_12 /* Util.swift */, + ); + name = Schedule; + path = Sources/Schedule; + sourceTree = SOURCE_ROOT; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + "Schedule::Schedule" /* Schedule */ = { + isa = PBXNativeTarget; + buildConfigurationList = OBJ_23 /* Build configuration list for PBXNativeTarget "Schedule" */; + buildPhases = ( + OBJ_26 /* Sources */, + OBJ_31 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Schedule; + productName = Schedule; + productReference = "Schedule::Schedule::Product" /* Schedule.framework */; + productType = "com.apple.product-type.framework"; + }; + "Schedule::ScheduleTests" /* ScheduleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = OBJ_44 /* Build configuration list for PBXNativeTarget "ScheduleTests" */; + buildPhases = ( + OBJ_47 /* Sources */, + OBJ_52 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + OBJ_54 /* PBXTargetDependency */, + ); + name = ScheduleTests; + productName = ScheduleTests; + productReference = "Schedule::ScheduleTests::Product" /* ScheduleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + "Schedule::SwiftPMPackageDescription" /* SchedulePackageDescription */ = { + isa = PBXNativeTarget; + buildConfigurationList = OBJ_33 /* Build configuration list for PBXNativeTarget "SchedulePackageDescription" */; + buildPhases = ( + OBJ_36 /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SchedulePackageDescription; + productName = SchedulePackageDescription; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + OBJ_1 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 9999; + }; + buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "Schedule" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = OBJ_5 /* */; + productRefGroup = OBJ_19 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + "Schedule::Schedule" /* Schedule */, + "Schedule::SwiftPMPackageDescription" /* SchedulePackageDescription */, + "Schedule::SchedulePackageTests::ProductTarget" /* SchedulePackageTests */, + "Schedule::ScheduleTests" /* ScheduleTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + OBJ_26 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 0; + files = ( + OBJ_27 /* DateTime.swift in Sources */, + OBJ_28 /* Job.swift in Sources */, + OBJ_29 /* Schedule.swift in Sources */, + OBJ_30 /* Util.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + OBJ_36 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 0; + files = ( + OBJ_37 /* Package.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + OBJ_47 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 0; + files = ( + OBJ_48 /* DateTimeTests.swift in Sources */, + OBJ_49 /* ScheduleTests.swift in Sources */, + OBJ_50 /* Util.swift in Sources */, + OBJ_51 /* XCTestManifests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + OBJ_42 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = "Schedule::ScheduleTests" /* ScheduleTests */; + targetProxy = 6266004320EF028200D5E606 /* PBXContainerItemProxy */; + }; + OBJ_54 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = "Schedule::Schedule" /* Schedule */; + targetProxy = 6266004220EF028200D5E606 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + OBJ_24 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + HEADER_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = Schedule.xcodeproj/Schedule_Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; + OTHER_CFLAGS = "$(inherited)"; + OTHER_LDFLAGS = "$(inherited)"; + OTHER_SWIFT_FLAGS = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = Schedule; + PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.0; + TARGET_NAME = Schedule; + }; + name = Debug; + }; + OBJ_25 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + HEADER_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = Schedule.xcodeproj/Schedule_Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; + OTHER_CFLAGS = "$(inherited)"; + OTHER_LDFLAGS = "$(inherited)"; + OTHER_SWIFT_FLAGS = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = Schedule; + PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.0; + TARGET_NAME = Schedule; + }; + name = Release; + }; + OBJ_3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_ARC = YES; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_NS_ASSERTIONS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + MACOSX_DEPLOYMENT_TARGET = 10.10; + ONLY_ACTIVE_ARCH = YES; + OTHER_SWIFT_FLAGS = "-DXcode"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + USE_HEADERMAP = NO; + }; + name = Debug; + }; + OBJ_34 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + LD = /usr/bin/true; + OTHER_SWIFT_FLAGS = "-swift-version 4 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk"; + SWIFT_VERSION = 4.0; + }; + name = Debug; + }; + OBJ_35 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + LD = /usr/bin/true; + OTHER_SWIFT_FLAGS = "-swift-version 4 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk"; + SWIFT_VERSION = 4.0; + }; + name = Release; + }; + OBJ_4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_ARC = YES; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_OPTIMIZATION_LEVEL = s; + MACOSX_DEPLOYMENT_TARGET = 10.10; + OTHER_SWIFT_FLAGS = "-DXcode"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + USE_HEADERMAP = NO; + }; + name = Release; + }; + OBJ_40 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + }; + name = Debug; + }; + OBJ_41 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + }; + name = Release; + }; + OBJ_45 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + HEADER_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = Schedule.xcodeproj/ScheduleTests_Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; + OTHER_CFLAGS = "$(inherited)"; + OTHER_LDFLAGS = "$(inherited)"; + OTHER_SWIFT_FLAGS = "$(inherited)"; + SWIFT_VERSION = 4.0; + TARGET_NAME = ScheduleTests; + }; + name = Debug; + }; + OBJ_46 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + HEADER_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = Schedule.xcodeproj/ScheduleTests_Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; + OTHER_CFLAGS = "$(inherited)"; + OTHER_LDFLAGS = "$(inherited)"; + OTHER_SWIFT_FLAGS = "$(inherited)"; + SWIFT_VERSION = 4.0; + TARGET_NAME = ScheduleTests; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + OBJ_2 /* Build configuration list for PBXProject "Schedule" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + OBJ_3 /* Debug */, + OBJ_4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + OBJ_23 /* Build configuration list for PBXNativeTarget "Schedule" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + OBJ_24 /* Debug */, + OBJ_25 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + OBJ_33 /* Build configuration list for PBXNativeTarget "SchedulePackageDescription" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + OBJ_34 /* Debug */, + OBJ_35 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + OBJ_39 /* Build configuration list for PBXAggregateTarget "SchedulePackageTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + OBJ_40 /* Debug */, + OBJ_41 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + OBJ_44 /* Build configuration list for PBXNativeTarget "ScheduleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + OBJ_45 /* Debug */, + OBJ_46 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = OBJ_1 /* Project object */; +} diff --git a/Schedule.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Schedule.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Schedule.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Schedule.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Schedule.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Schedule.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Schedule.xcodeproj/xcshareddata/xcschemes/Schedule-Package.xcscheme b/Schedule.xcodeproj/xcshareddata/xcschemes/Schedule-Package.xcscheme new file mode 100644 index 0000000..d142845 --- /dev/null +++ b/Schedule.xcodeproj/xcshareddata/xcschemes/Schedule-Package.xcscheme @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Schedule.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist b/Schedule.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..12a793f --- /dev/null +++ b/Schedule.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist @@ -0,0 +1,12 @@ + + + + SchemeUserState + + Schedule-Package.xcscheme + + + SuppressBuildableAutocreation + + + diff --git a/Sources/Schedule/DateTime.swift b/Sources/Schedule/DateTime.swift index 07855ce..5b29c53 100644 --- a/Sources/Schedule/DateTime.swift +++ b/Sources/Schedule/DateTime.swift @@ -7,109 +7,191 @@ import Foundation -/// `Interval` represents a length of time, it's contextless. +/// `Interval` represents a duration of time. public struct Interval { + /// The length of this interval, measured in nanoseconds. public let nanoseconds: Double + + /// Creates an interval from the given number of nanoseconds. public init(nanoseconds: Double) { self.nanoseconds = nanoseconds } - public init(seconds: Double) { - self.nanoseconds = seconds * pow(10, 9) - } - - public var seconds: Double { - return nanoseconds / pow(10, 9) - } - + /// A boolean value indicating whether this interval is negative. + /// + /// A interval can be negative. + /// + /// - The interval between 6:00 and 7:00 is `1.hour`, + /// 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`. + /// In this case, `-2.hour` means **two hours shorter** public var isNegative: Bool { return nanoseconds.isLess(than: 0) } - public var dispatchInterval: DispatchTimeInterval { - if nanoseconds > Double(Int.max) { return .nanoseconds(.max) } - if nanoseconds < Double(Int.min) { return .nanoseconds(.min) } - return .nanoseconds(Int(nanoseconds)) + /// The magnitude of this interval. + /// + /// It's the absolute value of the length of this interval, + /// measured in nanoseconds, but disregarding its sign. + public var magnitude: Double { + return nanoseconds.magnitude } + internal var ns: Int { + if nanoseconds > Double(Int.max) { return .max } + if nanoseconds < Double(Int.min) { return .min } + return Int(nanoseconds) + } + + /// Returns a dispatchTimeInterval created from this interval. + /// + /// The returned value will be clamped to the `DispatchTimeInterval`'s + /// usable range [`.nanoseconds(.min)....nanoseconds(.max)`]. + public func asDispatchTimeInterval() -> DispatchTimeInterval { + return .nanoseconds(ns) + } + + /// 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. + public func isShorter(than other: Interval) -> Bool { + return magnitude < other.magnitude + } + + /// Returns the longest interval of the given values. + public static func longest(_ intervals: Interval...) -> Interval { + return intervals.sorted(by: { $0.magnitude > $1.magnitude })[0] + } + + /// Returns the shortest interval of the given values. + public static func shortest(_ intervals: Interval...) -> Interval { + return intervals.sorted(by: { $0.magnitude < $1.magnitude })[0] + } + + /// Returns a new interval by multipling the left interval by the right number. + /// + /// 1.hour * 2 == 2.hours public static func *(lhs: Interval, rhs: Double) -> Interval { return Interval(nanoseconds: lhs.nanoseconds * rhs) } + /// Returns a new interval by adding the right interval to the left interval. + /// + /// 1.hour + 1.hour == 2.hours public static func +(lhs: Interval, rhs: Interval) -> Interval { return Interval(nanoseconds: lhs.nanoseconds + rhs.nanoseconds) } + /// Returns a new instarval by subtracting the right interval from the left interval. + /// + /// 2.hours - 1.hour == 1.hour public static func -(lhs: Interval, rhs: Interval) -> Interval { return Interval(nanoseconds: lhs.nanoseconds - rhs.nanoseconds) } } +extension Interval { + + /// Creates an interval from the given number of seconds. + public init(seconds: Double) { + self.nanoseconds = seconds * pow(10, 9) + } + + /// The length of this interval, measured in seconds. + public var seconds: Double { + return nanoseconds / pow(10, 9) + } + + /// The length of this interval, measured in minutes. + public var minutes: Double { + return seconds / 60 + } + + /// The length of this interval, measured in hours. + public var hours: Double { + return minutes / 60 + } + + /// The length of this interval, measured in days. + public var days: Double { + return hours / 24 + } + + /// The length of this interval, measured in weeks. + public var weeks: Double { + return days / 7 + } +} + extension Interval: Hashable { + /// The hashValue of this interval. public var hashValue: Int { return nanoseconds.hashValue } + /// Returns a boolean value indicating whether the interval is equal to another interval. public static func ==(lhs: Interval, rhs: Interval) -> Bool { - return lhs.hashValue == rhs.hashValue - } -} - -extension Interval: Comparable { - - public static func <(lhs: Interval, rhs: Interval) -> Bool { - return lhs.nanoseconds.magnitude < rhs.nanoseconds.magnitude + return lhs.nanoseconds == rhs.nanoseconds } } extension Date { + /// The interval between this date and now. + /// + /// If the date is earlier than now, the interval is 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. public func interval(since date: Date) -> Interval { return timeIntervalSince(date).seconds } + /// Return a new date by adding an interval to the date. public static func +(lhs: Date, rhs: Interval) -> Date { return lhs.addingTimeInterval(rhs.seconds) } - @discardableResult - public static func +=(lhs: inout Date, rhs: Interval) -> Date { + /// Add an interval to the date. + public static func +=(lhs: inout Date, rhs: Interval) { lhs = lhs + rhs - return lhs } } +/// `IntervalConvertible` provides a set of intuitive apis for creating interval. public protocol IntervalConvertible { - func asNanoseconds() -> Double + var nanoseconds: Interval { get } } extension Int: IntervalConvertible { - public func asNanoseconds() -> Double { - return Double(self) + public var nanoseconds: Interval { + return Interval(nanoseconds: Double(self)) } } extension Double: IntervalConvertible { - public func asNanoseconds() -> Double { - return self + public var nanoseconds: Interval { + return Interval(nanoseconds: self) } } extension IntervalConvertible { - public var nanoseconds: Interval { - return Interval(nanoseconds: asNanoseconds()) - } - public var nanosecond: Interval { return nanoseconds } @@ -171,19 +253,27 @@ extension IntervalConvertible { } } -public enum Weekday: Int { - case sunday = 1, monday, tuesday, wednesday, thursday, friday, saturday -} /// `Time` represents a time without a date. +/// +/// It is a specific point in a day. public struct Time { public let hour: Int + public let minute: Int + public let second: Int + public let nanosecond: Int - public init?(hour: Int, minute: Int, second: Int, nanosecond: Int) { + /// Create a date with `hour`, `minute`, `second` and `nanosecond` fields. + /// + /// If parameter is illegal, then return nil. + /// + /// Time(hour: 25) == nil + /// Time(hour: 1, minute: 61) == nil + public init?(hour: Int, minute: Int = 0, second: Int = 0, nanosecond: Int = 0) { guard hour >= 0 && hour < 24 else { return nil } guard minute >= 0 && minute < 60 else { return nil } guard second >= 0 && second < 60 else { return nil } @@ -195,17 +285,22 @@ public struct Time { self.nanosecond = nanosecond } + /// Create a time with a timing string + /// /// For example: /// - /// "11" - /// "11:12" - /// "11:12:12" - /// "11:12:13.123" + /// Time("11") == Time(hour: 11) + /// Time("11:12") == Time(hour: 11, minute: 12) + /// Time("11:12:13") == Time(hour: 11, minute: 12, second: 13) + /// Time("11:12:13.123") == Time(hour: 11, minute: 12, second: 13, nanosecond: 123000000) + /// + /// If timing's format is illegal, then return nil. public init?(timing: String) { let args = timing.split(separator: ":") - var h = 0, m = 0, s = 0, ns = 0 if args.count > 3 { return nil } + var h = 0, m = 0, s = 0, ns = 0 + guard let _h = Int(args[0]) else { return nil } h = _h if args.count > 1 { @@ -213,22 +308,22 @@ public struct Time { m = _m } if args.count > 2 { - let components = args[2].split(separator: ".") - if components.count > 2 { return nil } - guard let _s = Int(components[0]) else { return nil } + let values = args[2].split(separator: ".") + if values.count > 2 { return nil } + guard let _s = Int(values[0]) else { return nil } s = _s - if components.count > 1 { - guard let _ns = Int(components[1]) else { return nil } - let digit = components[1].count - ns = Int(Double(_ns) * pow(10, Double(9 - digit))) + if values.count > 1 { + guard let _ns = Int(values[1]) else { return nil } + let digits = values[1].count + ns = Int(Double(_ns) * pow(10, Double(9 - digits))) } } self.init(hour: h, minute: m, second: s, nanosecond: ns) } - func asDateComponents() -> DateComponents { + internal func asDateComponents() -> DateComponents { return DateComponents(hour: hour, minute: minute, second: second, nanosecond: nanosecond) } } @@ -236,23 +331,35 @@ public struct Time { /// `Period` represents a period of time defined in terms of fields. /// -/// It's a littl different from `Interval`, for example: +/// It's a little different from `Interval`. /// -/// 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. +/// For example: +/// +/// 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 { public let years: Int + public let months: Int + public let days: Int + public let hours: Int + public let minutes: Int + public let seconds: Int + public let nanoseconds: Int - public init(years: Int = 0, months: Int = 0, days: Int = 0, hours: Int = 0, minutes: Int = 0, seconds: Int = 0, nanoseconds: Int = 0) { + public init(years: Int = 0, months: Int = 0, days: Int = 0, + hours: Int = 0, minutes: Int = 0, seconds: Int = 0, + nanoseconds: Int = 0) { self.years = years self.months = months self.days = days @@ -262,72 +369,94 @@ public struct Period { self.nanoseconds = nanoseconds } - public func and(_ period: Period) -> Period { - let years = self.years + period.years - let months = self.months + period.months - let days = self.days + period.days - let hours = self.hours + period.hours - let minutes = self.minutes + period.minutes - let seconds = self.seconds + period.seconds - let nanoseconds = self.nanoseconds + period.nanoseconds - return Period(years: years, months: months, days: days, hours: hours, minutes: minutes, seconds: seconds, nanoseconds: nanoseconds) - } - + /// Returns a new date by adding the right period to the left period. public static func +(lhs: Period, rhs: Period) -> Period { - return lhs.and(rhs) - } - - func asDateComponents() -> DateComponents { - return DateComponents(year: years, month: months, day: days, hour: hours, minute: minutes, second: seconds, nanosecond: nanoseconds) + return Period(years: lhs.years.clampedAdding(rhs.years), + months: lhs.months.clampedAdding(rhs.months), + days: lhs.days.clampedAdding(rhs.days), + hours: lhs.hours.clampedAdding(rhs.hours), + minutes: lhs.minutes.clampedAdding(rhs.minutes), + seconds: lhs.seconds.clampedAdding(rhs.seconds), + nanoseconds: lhs.nanoseconds.clampedAdding(rhs.nanoseconds)) } + /// Returns a new date by adding the right period to the left date. public static func +(lhs: Date, rhs: Period) -> Date { return Calendar.autoupdatingCurrent.date(byAdding: rhs.asDateComponents(), to: lhs) ?? .distantFuture } -} - -extension Period { - public static func years(_ n: Int) -> Period { - return Period(years: n) + /// Returns a new period by adding the right interval to the left period. + 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, + nanoseconds: lhs.nanoseconds.clampedAdding(rhs.ns)) } - public static func months(_ n: Int) -> Period { - return Period(months: n) - } - public static func days(_ n: Int) -> Period { - return Period(days: n) - } - public static func hours(_ n: Int) -> Period { - return Period(hours: n) - } - public static func minutes(_ n: Int) -> Period { - return Period(minutes: n) - } - public static func seconds(_ n: Int) -> Period { - return Period(seconds: n) - } - public static func nanoseconds(_ n: Int) -> Period { - return Period(nanoseconds: n) + internal func asDateComponents() -> DateComponents { + return DateComponents(year: years, month: months, day: days, + hour: hours, minute: minutes, second: seconds, + nanosecond: nanoseconds) } } +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 + } +} + + +/// `Weekday` represents a day of the week, without a time. +public enum Weekday: Int { + case sunday = 1, monday, tuesday, wednesday, thursday, friday, saturday +} + + +/// `MonthDay` represents a day in the month, without a time. public enum MonthDay { case january(Int) + case february(Int) + case march(Int) + case april(Int) + case may(Int) + case june(Int) + case july(Int) + case august(Int) + case september(Int) + case october(Int) + case november(Int) + case december(Int) - func asDateComponents() -> DateComponents { + internal func asDateComponents() -> DateComponents { switch self { case .january(let day): return DateComponents(month: 1, day: day) case .february(let day): return DateComponents(month: 2, day: day) diff --git a/Sources/Schedule/Job.swift b/Sources/Schedule/Job.swift index f5a7bee..d1a631d 100644 --- a/Sources/Schedule/Job.swift +++ b/Sources/Schedule/Job.swift @@ -7,26 +7,30 @@ import Foundation -public class Job { +/// `Job` represents an action that to be invoke. +public final class Job { + /// Last time this job was invoked at. public private(set) var lastTime: Date? + + /// Next time this job will be invoked at. public private(set) var nextTime: Date? - private var iterator: Atomic> + private var iterator: AtomicBox> private let onElapse: (Job) -> Void private let timer: DispatchSourceTimer - private var suspensions = Atomic(0) + private var suspensions = AtomicBox(0) init(schedule: Schedule, queue: DispatchQueue? = nil, tag: String? = nil, onElapse: @escaping (Job) -> Void) { - self.iterator = Atomic(schedule.makeIterator()) + self.iterator = AtomicBox(schedule.makeIterator()) self.onElapse = onElapse self.timer = DispatchSource.makeTimerSource(queue: queue) - let interval = self.iterator.withLock({ $0.next() })?.dispatchInterval ?? DispatchTimeInterval.never + let interval = self.iterator.withLock({ $0.next() })?.asDispatchTimeInterval() ?? DispatchTimeInterval.never self.timer.schedule(wallDeadline: .now() + interval) self.timer.setEventHandler { [weak self] in self?.elapse() @@ -49,15 +53,17 @@ public class Job { nextTime = now.addingTimeInterval(i.nanoseconds / pow(10, 9)) onElapse(self) - timer.schedule(wallDeadline: .now() + i.dispatchInterval) + timer.schedule(wallDeadline: .now() + i.asDispatchTimeInterval()) } + /// Reschedule this job with the schedule. public func reschedule(_ schedule: Schedule) { iterator.withLock { $0 = schedule.makeIterator() } } + /// Suspend this job. public func suspend() { let canSuspend = suspensions.withLock { (n) -> Bool in guard n < UInt.max else { return false } @@ -70,6 +76,7 @@ public class Job { } } + /// Resume this job. public func resume() { let canResume = suspensions.withLock { (n) -> Bool in guard n > 0 else { return false } @@ -81,23 +88,27 @@ public class Job { } } + /// Cancel this job. public func cancel() { timer.cancel() JobCenter.shared.remove(self) } - public static func cancel(_ tag: String) { - JobCenter.shared.jobs(for: tag).forEach { $0.cancel() } - } - + /// Suspend all job that attach the tag. public static func suspend(_ tag: String) { JobCenter.shared.jobs(for: tag).forEach { $0.suspend() } } + /// Resume all job that attach the tag. public static func resume(_ tag: String) { JobCenter.shared.jobs(for: tag).forEach { $0.resume() } } + /// Cancel all job that attach the tag. + public static func cancel(_ tag: String) { + JobCenter.shared.jobs(for: tag).forEach { $0.cancel() } + } + deinit { suspensions.withLock { (n) in while n > 0 { @@ -111,10 +122,12 @@ public class Job { extension Job: Hashable { + /// The hashValue of this job. public var hashValue: Int { return ObjectIdentifier(self).hashValue } + /// Returns a boolean value indicating whether the job is equal to another job. public static func ==(lhs: Job, rhs: Job) -> Bool { return lhs.hashValue == rhs.hashValue } @@ -122,6 +135,9 @@ extension Job: Hashable { extension Optional: Hashable where Wrapped: Job { + /// The hashValue of this job. + /// + /// If job is nil, then return 0. public var hashValue: Int { if case .some(let wrapped) = self { return wrapped.hashValue @@ -129,12 +145,15 @@ extension Optional: Hashable where Wrapped: Job { return 0 } + /// Returns a boolean value indicating whether the job is equal to another job. + /// + /// If both of these two are nil, then return true. public static func ==(lhs: Optional, rhs: Optional) -> Bool { return lhs.hashValue == rhs.hashValue } } -private class JobCenter { +private final class JobCenter { static let shared = JobCenter() diff --git a/Sources/Schedule/Schedule.swift b/Sources/Schedule/Schedule.swift index 67dbeff..81c5a25 100644 --- a/Sources/Schedule/Schedule.swift +++ b/Sources/Schedule/Schedule.swift @@ -7,6 +7,13 @@ import Foundation +/// `Schedule` represents a plan that gives the times +/// at which a job should be invoked. +/// +/// `Schedule` is interval based. +/// When a new job is created in the `do` method, it will ask for the first +/// interval in this schedule, then set up a timer to invoke itself +/// after the interval. public struct Schedule { private var sequence: AnySequence @@ -18,6 +25,13 @@ public struct Schedule { return sequence.makeIterator() } + /// Schedule a job with this schedule. + /// + /// - Parameters: + /// - queue: The dispatch queue to which the job will be submitted. + /// - tag: The tag to attach to the job. + /// - onElapse: The job to invoke when time is out. + /// - Returns: The job just created. @discardableResult public func `do`(queue: DispatchQueue? = nil, tag: String? = nil, @@ -25,21 +39,58 @@ public struct Schedule { return Job(schedule: self, queue: queue, tag: tag, onElapse: onElapse) } + /// Schedule a job with this schedule. + /// + /// - Parameters: + /// - queue: The dispatch queue to which the job will be submitted. + /// - tag: The tag to attach to the queue + /// - onElapse: The job to invoke when time is out. + /// - Returns: The job just created. @discardableResult public func `do`(queue: DispatchQueue? = nil, tag: String? = nil, onElapse: @escaping () -> Void) -> Job { return self.do(queue: queue, tag: tag, onElapse: { (_) in onElapse() }) } +} + +extension Schedule { - public static func make(_ makeIterator: @escaping () -> I) -> Schedule where I: IteratorProtocol, I.Element == Interval { - return Schedule(AnySequence(makeIterator)) + /// Creates a schedule from a `makeUnderlyingIterator()` method. + /// + /// The job will be invoke after each interval + /// produced by the iterator that `makeUnderlyingIterator` returns. + /// + /// For example: + /// + /// let schedule = Schedule.make { + /// var i = 0 + /// return AnyIterator { + /// i += 1 + /// return i + /// } + /// } + /// schedule.do { + /// print(Date()) + /// } + /// + /// > "2001-01-01 00:00:00" + /// > "2001-01-01 00:00:01" + /// > "2001-01-01 00:00:03" + /// > "2001-01-01 00:00:06" + /// ... + public static func make(_ makeUnderlyingIterator: @escaping () -> I) -> Schedule where I: IteratorProtocol, I.Element == Interval { + return Schedule(AnySequence(makeUnderlyingIterator)) } + /// Creates a schedule from an interval sequence. + /// The job will be invoke after each interval in the sequence. public static func from(_ sequence: S) -> Schedule where S: Sequence, S.Element == Interval { return Schedule(sequence) } + /// Creates a schedule from an interval array. + /// The job will be invoke after each interval in the array. public static func of(_ intervals: Interval...) -> Schedule { return Schedule(intervals) } @@ -47,9 +98,33 @@ public struct Schedule { extension Schedule { - public static func make(_ makeIterator: @escaping () -> I) -> Schedule where I: IteratorProtocol, I.Element == Date { + /// Creates a schedule from a `makeUnderlyingIterator()` method. + /// + /// The job will be invoke at each date + /// produced by the iterator that `makeUnderlyingIterator` returns. + /// + /// For example: + /// + /// let schedule = Schedule.make { + /// return AnyIterator { + /// return Date().addingTimeInterval(3) + /// } + /// } + /// print("now:", Date()) + /// schedule.do { + /// print("job", Date()) + /// } + /// + /// > "now: 2001-01-01 00:00:00" + /// > "job: 2001-01-01 00:00:03 + /// ... + /// + /// You should not return `Date()` in making interator + /// if you want to invoke a job immediately, + /// use `Schedule.now` then `concat` another schedule instead. + public static func make(_ makeUnderlyingIterator: @escaping () -> I) -> Schedule where I: IteratorProtocol, I.Element == Date { return Schedule.make { () -> AnyIterator in - var iterator = makeIterator() + var iterator = makeUnderlyingIterator() var previous: Date! return AnyIterator { previous = previous ?? Date() @@ -60,14 +135,19 @@ extension Schedule { } } + /// Creates a schedule from a date sequence. + /// The job will be invoke at each date in the sequence. public static func from(_ sequence: S) -> Schedule where S: Sequence, S.Element == Date { return Schedule.make(sequence.makeIterator) } + /// Creates a schedule from a date array. + /// The job will be invoke at each date in the array. public static func of(_ dates: Date...) -> Schedule { return Schedule.from(dates) } + /// A dates sequence corresponding to this schedule. public var dates: AnySequence { return AnySequence { () -> AnyIterator in let iterator = self.makeIterator() @@ -84,23 +164,35 @@ extension Schedule { extension Schedule { + /// A schedule with a distant past date. public static var distantPast: Schedule { return Schedule.of(Date.distantPast) } + /// A schedule with a distant future date. public static var distantFuture: Schedule { return Schedule.of(Date.distantFuture) } + /// A schedule with no date. public static var never: Schedule { return Schedule.make { - AnyIterator { nil } + AnyIterator { nil } } } } extension Schedule { + /// Returns a new schedule by concatenating a schedule to this schedule. + /// + /// For example: + /// + /// let s0 = Schedule.of(1.second, 2.seconds, 3.seconds) + /// let s1 = Schedule.of(4.seconds, 4.seconds, 4.seconds) + /// let s2 = s0.concat(s1) + /// > s2 + /// > 1.second, 2.seconds, 3.seconds, 4.seconds, 4.seconds, 4.seconds public func concat(_ schedule: Schedule) -> Schedule { return Schedule.make { () -> AnyIterator in let i0 = self.makeIterator() @@ -112,6 +204,15 @@ extension Schedule { } } + /// Returns a new schedule by merging a schedule to this schedule. + /// + /// For example: + /// + /// let s0 = Schedule.of(1.second, 3.seconds, 5.seconds) + /// let s1 = Schedule.of(2.seconds, 4.seconds, 6.seconds) + /// let s2 = s0.concat(s1) + /// > s2 + /// > 1.second, 1.second, 1.second, 1.second, 1.second, 1.second public func merge(_ schedule: Schedule) -> Schedule { return Schedule.make { () -> AnyIterator in let i0 = self.dates.makeIterator() @@ -137,6 +238,14 @@ extension Schedule { } } + /// Returns a new schedule by cutting out a specific number of this schedule. + /// + /// For example: + /// + /// let s0 = Schedule.every(1.second) + /// let s1 = s0.count(3) + /// > s1 + /// 1.second, 1.second, 1.second public func count(_ count: Int) -> Schedule { return Schedule.make { () -> AnyIterator in let iterator = self.makeIterator() @@ -149,48 +258,62 @@ extension Schedule { } } + /// Returns a new schedule by cutting out the part which is before the date. public func until(_ date: Date) -> Schedule { return Schedule.make { () -> AnyIterator in let iterator = self.makeIterator() var previous: Date! return AnyIterator { previous = previous ?? Date() - guard let interval = iterator.next(), previous.addingTimeInterval(interval.seconds) < date else { return nil } + guard let interval = iterator.next(), + previous.addingTimeInterval(interval.seconds) < date else { + return nil + } previous.addTimeInterval(interval.seconds) return interval } } } + /// Creates a schedule that invokes the job immediately. public static var now: Schedule { return Schedule.of(0.nanosecond) } + /// Creates a schedule that invokes the job after the delay. public static func after(_ delay: Interval) -> Schedule { return Schedule.of(delay) } + /// Creates a schedule that invokes the job every interval. public static func every(_ interval: Interval) -> Schedule { return Schedule.make { AnyIterator { interval } } } + /// Creates a schedule that invokes the job at the specific date. public static func at(_ date: Date) -> Schedule { return Schedule.of(date) } + /// Creates a schedule that invokes the job after the delay then repeat + /// every interval. public static func after(_ delay: Interval, repeating interval: Interval) -> Schedule { return Schedule.after(delay).concat(Schedule.every(interval)) } + /// Creates a schedule that invokes the job every period. public static func every(_ period: Period) -> Schedule { return Schedule.make { () -> AnyIterator in let calendar = Calendar.autoupdatingCurrent var previous: Date! return AnyIterator { previous = previous ?? Date() - guard let next = calendar.date(byAdding: period.asDateComponents(), to: previous) else { return nil } + guard let next = calendar.date(byAdding: period.asDateComponents(), + to: previous) else { + return nil + } defer { previous = next } return next.interval(since: previous) } @@ -200,11 +323,17 @@ extension Schedule { extension Schedule { + /// `EveryDateMiddleware` represents a middleware that wraps a schedule + /// which only specify date without time. + /// + /// You should call `at` method to get the time specified schedule. public struct EveryDateMiddleware { fileprivate let schedule: Schedule + /// Returns a schedule at the specific timing. public func at(_ timing: Time) -> Schedule { + return Schedule.make { () -> AnyIterator in let iterator = self.schedule.dates.makeIterator() let calendar = Calendar.autoupdatingCurrent @@ -212,13 +341,29 @@ extension Schedule { return AnyIterator { previous = previous ?? Date() guard let date = iterator.next(), - let next = calendar.nextDate(after: date, matching: timing.asDateComponents(), matchingPolicy: .strict) else { return nil } + let next = calendar.nextDate(after: date, + matching: timing.asDateComponents(), + matchingPolicy: .strict) else { + return nil + } defer { previous = next } return next.interval(since: previous) } } } + /// Returns a schedule at the specific timing. + /// + /// For example: + /// + /// let s = Schedule.every(.monday).at("11:11") + /// + /// Available format: + /// + /// Time("11") == Time(hour: 11) + /// Time("11:12") == Time(hour: 11, minute: 12) + /// Time("11:12:13") == Time(hour: 11, minute: 12, second: 13) + /// Time("11:12:13.123") == Time(hour: 11, minute: 12, second: 13, nanosecond: 123) public func at(_ timing: String) -> Schedule { guard let time = Time(timing: timing) else { return Schedule.never @@ -226,6 +371,7 @@ extension Schedule { return at(time) } + /// Returns a schedule at the specific timing. public func at(_ timing: Int...) -> Schedule { let hour = timing[0] let minute = timing.count > 1 ? timing[1] : 0 @@ -239,6 +385,7 @@ extension Schedule { } } + /// Creates a schedule that invokes the job every specific weekday. public static func every(_ weekday: Weekday) -> EveryDateMiddleware { let schedule = Schedule.make { () -> AnyIterator in let calendar = Calendar.autoupdatingCurrent @@ -257,6 +404,7 @@ extension Schedule { return EveryDateMiddleware(schedule: schedule) } + /// Creates a schedule that invokes the job every specific weekdays. public static func every(_ weekdays: Weekday...) -> EveryDateMiddleware { var schedule = every(weekdays[0]).schedule if weekdays.count > 1 { @@ -267,6 +415,7 @@ extension Schedule { return EveryDateMiddleware(schedule: schedule) } + /// Creates a schedule that invokes the job every specific day in the month. public static func every(_ monthDay: MonthDay) -> EveryDateMiddleware { let schedule = Schedule.make { () -> AnyIterator in let calendar = Calendar.autoupdatingCurrent @@ -286,6 +435,7 @@ extension Schedule { return EveryDateMiddleware(schedule: schedule) } + /// Creates a schedule that invokes the job every specific days in the months. public static func every(_ mondays: MonthDay...) -> EveryDateMiddleware { var schedule = every(mondays[0]).schedule if mondays.count > 1 { diff --git a/Sources/Schedule/Util.swift b/Sources/Schedule/Util.swift index 8031da8..bbd49e4 100644 --- a/Sources/Schedule/Util.swift +++ b/Sources/Schedule/Util.swift @@ -7,7 +7,7 @@ import Foundation -class Atomic { +class AtomicBox { private var lock = NSLock() @@ -41,3 +41,15 @@ extension String { } } +extension Int { + + func clampedAdding(_ other: Int) -> Int { + let r = addingReportingOverflow(other) + return r.overflow ? (other > 0 ? .max : .min) : r.partialValue + } + + func clampedSubtracting(_ other: Int) -> Int { + let r = subtractingReportingOverflow(other) + return r.overflow ? (other > 0 ? .max : .min) : r.partialValue + } +} diff --git a/Tests/ScheduleTests/DateTimeTests.swift b/Tests/ScheduleTests/DateTimeTests.swift index 5d10ee6..04b9189 100644 --- a/Tests/ScheduleTests/DateTimeTests.swift +++ b/Tests/ScheduleTests/DateTimeTests.swift @@ -12,10 +12,10 @@ final class DateTimeTests: XCTestCase { func testInterval2DispatchInterval() { let i0 = 1.23.seconds - XCTAssertEqual(i0.dispatchInterval, DispatchTimeInterval.nanoseconds(Int(i0.nanoseconds))) + XCTAssertEqual(i0.asDispatchTimeInterval(), DispatchTimeInterval.nanoseconds(Int(i0.nanoseconds))) let i1 = 4.56.minutes + 7.89.hours - XCTAssertEqual(i1.dispatchInterval, DispatchTimeInterval.nanoseconds(Int(i1.nanoseconds))) + XCTAssertEqual(i1.asDispatchTimeInterval(), DispatchTimeInterval.nanoseconds(Int(i1.nanoseconds))) } func testIntervalConvertible() { @@ -42,7 +42,7 @@ final class DateTimeTests: XCTestCase { } func testPeriodAnd() { - let dateComponents: Period = .years(1) + .months(2) + .days(3) + let dateComponents: Period = 1.year + 2.months + 3.days XCTAssertEqual(dateComponents.years, 1) XCTAssertEqual(dateComponents.months, 2) XCTAssertEqual(dateComponents.days, 3) diff --git a/Tests/ScheduleTests/ScheduleTests.swift b/Tests/ScheduleTests/ScheduleTests.swift index 502599d..b716c54 100644 --- a/Tests/ScheduleTests/ScheduleTests.swift +++ b/Tests/ScheduleTests/ScheduleTests.swift @@ -10,7 +10,6 @@ final class ScheduleTests: XCTestCase { let s1 = Schedule.from(intervals) XCTAssert(s1.makeIterator().isEqual(to: intervals, leeway: 0.001.seconds)) - let d0 = Date() + intervals[0] let d1 = d0 + intervals[1] let d2 = d1 + intervals[2] @@ -74,17 +73,13 @@ final class ScheduleTests: XCTestCase { func testMerge() { let intervals0: [Interval] = [1.second, 2.minutes, 1.hour] let intervals1: [Interval] = [2.seconds, 1.minutes, 1.seconds] - let scheudle0 = Schedule.from(intervals0).merge(Schedule.from(intervals1)) - let scheudle1 = Schedule.of(1.second, 1.second, 1.minutes, 1.seconds, 58.seconds, 1.hour) XCTAssert(scheudle0.makeIterator().isEqual(to: scheudle1.makeIterator(), leeway: 0.001.seconds)) } func testEveryPeriod() { - - let s = Schedule.every(.years(1)).count(10) - + let s = Schedule.every(1.year).count(10) var date = Date() for i in s.dates { XCTAssertEqual(i.dateComponents.year!, date.dateComponents.year! + 1) diff --git a/Tests/ScheduleTests/Util.swift b/Tests/ScheduleTests/Util.swift index 301e260..16351ab 100644 --- a/Tests/ScheduleTests/Util.swift +++ b/Tests/ScheduleTests/Util.swift @@ -34,8 +34,8 @@ extension Sequence where Element == Interval { var i0 = self.makeIterator() var i1 = sequence.makeIterator() while let l = i0.next(), let r = i1.next() { - let diff = Swift.max(l, r) - Swift.min(l, r) - if diff < leeway { continue } + let diff = Interval.longest(l, r) - Interval.shortest(l, r) + if diff.isShorter(than: leeway) { continue } return false } return i0.next() == i1.next() @@ -45,8 +45,8 @@ extension Sequence where Element == Interval { extension Interval { func isEqual(to interval: Interval, leeway: Interval) -> Bool { - let diff = Swift.max(self, interval) - Swift.min(self, interval) - return diff < leeway + let diff = Interval.longest(self, interval) - Interval.shortest(self, interval) + return diff.isShorter(than: leeway) } }