Compare commits

..

1 Commits
0.7.0 ... 0.2.1

Author SHA1 Message Date
081e3a8dab update publish script 2020-01-13 22:02:40 +06:00
61 changed files with 622 additions and 2417 deletions

5
.github/FUNDING.yml vendored
View File

@@ -1,5 +0,0 @@
# These are supported funding model platforms
patreon: InsanusMokrassar
custom: ['https://paypal.me/InsanusMokrassar?locale.x=ru_RU']

3
.github/labeler.yml vendored
View File

@@ -1,3 +0,0 @@
code: "**/*.kt"
gradle: "**/*.gradle"
markdown: "**/*.md"

View File

@@ -1,13 +0,0 @@
name: Greetings
on: [pull_request, issues]
jobs:
greeting:
runs-on: ubuntu-latest
steps:
- uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: 'Welcome with your first issue'
pr-message: 'Welcome with your first Pull Request'

View File

@@ -1,24 +0,0 @@
name: Publish KDocs
on:
push:
branches:
- master
jobs:
publishing:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Fix android 31.0.0 dx
continue-on-error: true
run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
- name: Build
run: ./gradlew dokkaHtml
- name: Publish KDocs
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./build/dokka/html
publish_branch: kdocs

View File

@@ -1,18 +0,0 @@
# This workflow will triage pull requests and apply a label based on the
# paths that are modified in the pull request.
#
# To use this workflow, you will need to set up a .github/labeler.yml
# file with configuration. For more information, see:
# https://github.com/actions/labeler/blob/master/README.md
name: "Pull Request Labeler"
on:
- pull_request
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v2
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -1,25 +0,0 @@
name: Publish package to GitHub Packages
on: [push]
jobs:
publishing:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Fix android 32.0.0 dx
continue-on-error: true
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
- name: Update version
run: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
sed -i -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" gradle.properties
- name: prebuild
run: ./gradlew clean build
- name: Publish package
continue-on-error: true
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidReleasePublication -x signAndroidDebugPublication -x signAndroidReleasePublication
env:
GITHUBPACKAGES_USER: ${{ github.actor }}
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,193 +1,12 @@
# Changelog
## 0.7.0
**ALL DEPRECATIONS HAVE BEEN REMOVED**
* Versions
* `Kotlin`: `1.6.10`
* `Klock`: `2.4.10`
* `Coroutines`: `1.6.0`
* `Androidx Work`: `2.7.1`
## 0.6.6
* Versions
* `Klock`: `2.4.8`
* `Androidx Work`: `2.7.0`
## 0.6.5
* Flows now use `doWhile` functions
* `doWhile` now use additional delay (for 1 ms) for cases when `block` executing too fast
* New extensions for `KronScheduler`: `do(Once/While/Infinity)Local`/`do(Once/While/Infinity)Tz`
## 0.6.4
* Versions
* `Kotlin`: `1.5.31`
* `Coroutines`: `1.5.2`
* `Klock`: `2.4.2`
* `Androidx Work`: `2.6.0`
## 0.6.3
* Versions
* `Kotlin`: `1.5.30`
* `Klock`: `2.4.0`
## 0.6.2
**LEGACY COMPILER VARIANT FOR JS IS NOT AVAILABLE SINCE THIS UPDATE**
* Versions
* `Kotlin`: `1.5.21`
* `Coroutines`: `1.5.1`
* `Klock`: `2.3.1`
## 0.6.1
* Versions
* `Klock`: `2.1.0` -> `2.1.2`
* Rewriting of default mechanism of `KronScheduler`s
## 0.6.0
* Versions
* `Kotlin`: `1.4.32` -> `1.5.10`
* `Coroutines`: `1.4.3` -> `1.5.0`
* `Klock`: `2.0.7` -> `2.1.0`
## 0.5.2
* Versions
* `Kotlin`: `1.4.31` -> `1.4.32`
* Supporting of weekdays
* Supporting of timezones
* Any `KronScheduler` now can be used for calling `next` with `DateTimeTz`
* New type `KronSchedulerTz`
* `SchedulerFlow` has been deprecated
* New extension `asTzFlow` and small changes in `asFlow` logic
* `merge` extensions now return `CollectionKronScheduler` instead of just `KronScheduler`
## 0.5.1
* Versions
* `Kotlin`: `1.4.21` -> `1.4.31`
* `Coroutines`: `1.4.2` -> `1.4.3`
* `Klock`: `2.0.3` -> `2.0.7`
* `Androidx Work`: `2.4.0` -> `2.5.0`
## 0.5.0 Years
**BREAKING CHANGES**
* `CronDateTimeScheduler` has been marked as `internal` and no longer accessible outside of internal functions
* Old methods `merge` and `plus` related to `CronDateTimeScheduler` has been marked as `deprecated` and changed their
parameters types - it is `KronScheduler` now
* New methods `merge` has been added
* **`KronScheduler#next` method now is nullable. Use `nextOrRelative`/`nextOrNow` to get next time certainly**
* **Years was added as optional part of krontab template and opportunity in `SchedulerBuilder`**
* New builder `YearsBuilder`
* `SchedulerFlow#collectSafely` will be normally (without exceptions) finish when `next` of scheduler will return
null
* `KronScheduler#doOnce` will run code immediately in case when `next` is returning null value
* `KrontabTemplateWrapper` has been added
* New extension `KrontabTemplate#toKronScheduler` (works as `toSchedule`)
* **Fixed issue related to the fact that `toNearDateTime` of `CronDateTime` incorrectly handled months**
* **Android target has been added**
## 0.4.2
* Versions
* `Kotlin`: `1.4.20` -> `1.4.21`
* `Klock`: `2.0.1` -> `2.0.3`
* `CronDateTimeScheduler` now is deprecated and will be set up as `internal` in future
## 0.4.1
* Versions:
* `Coroutines`: `1.4.1` -> `1.4.2`
* `Klock`: `2.0.0` -> `2.0.1`
* `CronDateTimeScheduler` now is public
* New functions for `CronDateTimeScheduler`
* Add `CollectionKronScheduler`. It will give opportunity to unite several schedulers in one
## 0.4.0
**BREAKING CHANGES**
Package of project has been changed. Migration:
* Replace in your dependencies `com.insanusmokrassar:krontab` by `dev.inmo:krontab`
* Replace in your project all imports `com.insanusmokrassar.krontab` by `dev.inmo.krontab`
* Versions:
* `Kotlin`: `1.4.10` -> `1.4.20`
* `Klock`: `1.12.1` -> `2.0.0`
## 0.3.3
* Versions:
* `Coroutines`: `1.3.9` -> `1.4.1`
## 0.3.2
* Function `TimeBuilder#each` was added (works as `at`)
* Add opportunity to use `first` shortcuts:
* Value property `TimeBuilder#first` for including via functions like `TimeBuilder#at`
* Shortcut for kron string format `f` or `F`
* Add opportunity to use `last` shortcuts:
* Value property `TimeBuilder#last` for including via functions like `TimeBuilder#at`
* Shortcut for kron string format `l` or `L`
## 0.3.1
* Versions:
* `Kotlin`: `1.4.0` -> `1.4.10`
* `Klock`: `1.12.0` -> `1.12.1`
## 0.3.0
* Versions:
* `Kotlin`: `1.3.72` -> `1.4.0`
* `Coroutines`: `1.3.8` -> `1.3.9`
* `Klock`: `1.11.14` -> `1.12.0`
* Typealias `KrontabTemplate` was added
* Extension `KrontabTemplate#toSchedule` was added
### 0.2.4
* Updates in libraries:
* Klock `1.11.3` -> `1.11.14`
* Coroutines `1.3.7` -> `1.3.8`
* Ranges support were included. Now it is possible to correctly use syntax `0-5` in strings schedules
## 0.2.3
* Updates in libraries:
* Kotlin `1.3.70` -> `1.3.72`
* Coroutines `1.3.5` -> `1.3.7`
* Klock `1.10.0` -> `1.11.3`
* A lot of KDocs added and fixed
* `EverySecondScheduler` changed its building logic - now it is lazy with builder using
* `KronScheduler#doOnce` was optimized: now it will be explicitly called once and return result of its calculations
* `KronScheduler#doWhile` was rewritten to use `KronScheduler#doOnce` for calculations of `block` result
* New `buildSchedule(String)` function as a shortcut for `createSimpleScheduler(String)`
## 0.2.2
* Updates in libraries:
* Kotlin `1.3.61` -> `1.3.70`
* Coroutines `1.3.3` -> `1.3.5`
* Klock `1.8.6` -> `1.10.0`
## 0.2.1
* Added support of flows: now any `KronScheduler` can be convert to `Flow<DateTime>` using `asFlow` extension
## 0.2.0
# 0.2.0
* Updated way of publishing (for more info look at the [git](https://git.insanusmokrassar.com/InsanusMokrassar/krontab))
* Updates in libraries:
* Coroutines `1.3.2` -> `1.3.3`
* Klock `1.7.3` -> `1.8.6`
## 0.2.1
* Added support of flows: now any `KronScheduler` can be convert to `Flow<DateTime>` using `asFlow` extension

101
README.md
View File

@@ -1,12 +1,18 @@
# krontab
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.inmo/krontab/badge.svg)](https://maven-badges.herokuapp.com/maven-central/dev.inmo/krontab)
[![Build Status](https://github.com/InsanusMokrassar/krontab/actions/workflows/publishing_packages.yml/badge.svg)](https://github.com/InsanusMokrassar/krontab/actions/workflows/publishing_packages.yml)
[![KDocs](https://raw.githubusercontent.com/InsanusMokrassar/badges/master/kdocs.svg)](https://krontab.inmo.dev/index.html)
[ ![Download](https://api.bintray.com/packages/insanusmokrassar/InsanusMokrassar/krontab/images/download.svg) ](https://bintray.com/insanusmokrassar/InsanusMokrassar/krontab/_latestVersion)
Library was created to give oppotunity to launch some things from time to time according to some schedule in
runtime of applications.
| Table of content |
|---|
| [ How to use ](#how-to-use) |
| [ How to use: Including in project ](#including-in-project) |
| [ How to use: Config from string ](#config-from-string) |
| [ How to use: Config via builder (DSL preview) ](#config-via-builder) |
| [ How to use: KronScheduler as a Flow ](#KronScheduler-as-a-Flow) |
## How to use
There are several ways to configure and use this library:
@@ -31,12 +37,12 @@ If you want to include `krontab` in your project, just add next line to your
dependencies part:
```groovy
implementation "dev.inmo:krontab:$krontab_version"
implementation "com.insanusmokrassar:krontab:$krontab_version"
```
Next version is the latest currently for the library:
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.inmo/krontab/badge.svg)](https://maven-badges.herokuapp.com/maven-central/dev.inmo/krontab)
[ ![Download](https://api.bintray.com/packages/insanusmokrassar/InsanusMokrassar/krontab/images/download.svg) ](https://bintray.com/insanusmokrassar/InsanusMokrassar/krontab/_latestVersion)
For old version of Gradle, instead of `implementation` word developers must use `compile`.
@@ -44,22 +50,19 @@ For old version of Gradle, instead of `implementation` word developers must use
Developers can use more simple way to configure repeat times is string. String configuring
like a `crontab`, but with a little bit different meanings:
```
/--------------- Seconds
| /------------- Minutes
| | /----------- Hours
| | | /--------- Days of months
| | | | /------- Months
| | | | | /----- (optional) Year
| | | | | | /--- (optional) Timezone offset
| | | | | | | / (optional) Week days
* * * * * * 0o *w
/-------- Seconds
| /------ Minutes
| | /---- Hours
| | | /-- Days of months
| | | | / Months
| | | | |
* * * * *
```
It is different with original `crontab` syntax for the reason, that expected that in practice developers
will use seconds and minutes with more probability than months (for example) or even years. In fact, developers will use
something like:
will use seconds and minutes with more probability than months (for example). In fact, developers will use something
like:
```kotlin
doWhile("/5 * * * *") {
@@ -68,7 +71,7 @@ doWhile("/5 * * * *") {
}
```
An other version:
Or more version:
```kotlin
doInfinity("/5 * * * *") {
@@ -80,7 +83,7 @@ Both of examples will print `Called` message every five seconds.
### Config via builder
Also, this library currently supports DSL for creating the same goals:
Also this library currently supports DSL for creating the same goals:
```kotlin
val kronScheduler = buildSchedule {
@@ -123,38 +126,6 @@ kronScheduler.doInfinity {
All of these examples will do the same things: print `Called` message every five seconds.
### do\* functions
With regular `doOnce`/`doWhile`/`doInfinity` there are two types of their variations: **local** and **timezoned**. Local
variations (`doOnceLocal`/`doWhileLocal`/`doInfinityLocal`) will pass `DateTime` as an argument into the block:
```kotlin
doInfinityLocal("/5 * * * *") {
println(it) // will print current date time
}
```
Timezoned variations (`doOnceTz`/`doWhileTz`/`doInfinityTz`) will do the same thing but pass as an argument `DateTimeTz`:
```kotlin
doInfinityTz("/5 * * * * 0o") {
println(it) // will print current date time in UTC
}
```
It is useful in cases when you need to get the time of calling and avoid extra calls to system time.
#### Helpful table for
| | No args | Local `DateTime` | Local `DateTimeTz` with offset of `KronScheduler` |
|---| ------- | ---------------- | ------------------------------------------------- |
| **Call only near time** | doOnce | doOnceLocal | doOnceTz |
| **Call while condition is true** | doWhile | doWhileLocal | doWhileTz |
| **Work infinity*** | doInfinity | doInfinityLocal | doInfinityTz |
*Here there is an important notice, that `Work infinity` is not exactly `infinity`. Actually, that means that `do while
coroutine is alive` and in fact executing will be stopped when coroutine became cancelled.
### KronScheduler as a Flow
Any `KronScheduler`can e converted to a `Flow<DateTime` using extension `asFlow`:
@@ -169,8 +140,8 @@ val kronScheduler = buildSchedule {
val flow = kronScheduler.asFlow()
```
So, in this case any operations related to flow are available and it is expected that they will work correctly. For
example, it is possible to use this flow with `takeWhile`:
So, in this case any operations related to flow are available and it is expected tt th will work correctly. For example,
it is possible to use this flow with `takeWhile`:
```kotlin
flow.takeWhile {
@@ -179,27 +150,3 @@ flow.takeWhile {
action()
}
```
### Offsets
Offsets in this library works via passing parameter ending with `o` in any place after `month` config. Currently
there is only one format supported for offsets: minutes of offsets. To use time zones you will need to call `next`
method with `DateTimeTz` argument or `nextTimeZoned` method with any `KronScheduler` instance, but in case if this
scheduler is not instance of `KronSchedulerTz` it will work like you passed just `DateTime`.
Besides, in case you wish to use time zones explicitly, you will need to get `KronSchedulerTz`. It is possible by:
* Using `createSimpleScheduler`/`buildSchedule`/`KrontabTemplate#toSchedule`/`KrontabTemplate#toKronScheduler` methods
with passing `defaultOffset` parameter
* Using `SchedulerBuilder#build`/`createSimpleScheduler`/`buildSchedule`/`KrontabTemplate#toSchedule`/`KrontabTemplate#toKronScheduler`
methods with casting to `KronSchedulerTz` in case you are pretty sure that it is timezoned `KronScheduler`
* Creating your own implementation of `KronSchedulerTz`
### Note about week days
Unlike original CRON, here week days:
* Works as `AND`: cron date time will search first day which will pass requirement according all parameters including
week days
* You may use any related to numbers syntax with week days: `0-3w`, `0,1,2,3w`, etc.
* Week days (like years and offsets) are optional and can be placed anywhere after `month`

View File

@@ -1 +0,0 @@
theme: jekyll-theme-cayman

View File

@@ -1,139 +1,70 @@
buildscript {
repositories {
mavenLocal()
// jcenter()
jcenter()
mavenCentral()
google()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.github.breadmoirai:github-release:$github_release_plugin_version"
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
classpath 'com.android.tools.build:gradle:4.2.2'
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$gradle_bintray_plugin_version"
}
}
plugins {
id "org.jetbrains.kotlin.multiplatform" version "$kotlin_version"
id "org.jetbrains.dokka" version "$dokka_version"
id "org.jetbrains.kotlin.plugin.serialization" version "$kotlin_version"
}
// temporal crutch until legacy tests will be stabled or legacy target will be removed
allprojects {
if (it != rootProject.findProject("docs")) {
tasks.whenTaskAdded { task ->
if(task.name == "jsLegacyBrowserTest" || task.name == "jsLegacyNodeTest") {
task.enabled = false
}
}
}
}
apply plugin: "com.android.library"
project.version = "$version"
project.group = "dev.inmo"
project.version = "0.2.1"
project.group = "com.insanusmokrassar"
apply from: "publish.gradle"
apply from: "github_release.gradle"
repositories {
mavenLocal()
// jcenter()
jcenter()
mavenCentral()
// maven { url "https://kotlin.bintray.com/kotlinx" }
google()
maven { url "https://kotlin.bintray.com/kotlinx" }
}
apply from: './dokka.gradle'
kotlin {
jvm()
js(IR) {
browser()
nodejs()
}
android {
publishAllLibraryVariants()
}
js()
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$kotlin_coroutines_version"
api "com.soywiz.korlibs.klock:klock:$klockVersion"
}
}
androidMain {
dependencies {
api "androidx.work:work-runtime-ktx:$androidx_work_version"
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlin_coroutines_version"
}
}
jvmMain {
dependencies {
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
}
}
jvmTest {
dependencies {
implementation kotlin('test-junit')
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
}
}
jsTest {
jsMain {
dependencies {
implementation kotlin('test-js')
}
}
androidTest {
dependencies {
implementation kotlin('test-junit')
api "org.jetbrains.kotlinx:kotlinx-coroutines-core-js:$kotlin_coroutines_version"
}
}
}
}
apply plugin: 'com.getkeepsafe.dexcount'
android {
compileSdkVersion "$android_compileSdkVersion".toInteger()
buildToolsVersion "$android_buildToolsVersion"
defaultConfig {
minSdkVersion "$android_minSdkVersion".toInteger()
targetSdkVersion "$android_compileSdkVersion".toInteger()
versionCode "${android_code_version}".toInteger()
versionName "$version"
}
buildTypes {
release {
minifyEnabled false
}
debug {
debuggable true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
test {
java.srcDir file("src/jvmTest")
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

View File

@@ -1,24 +0,0 @@
#!/bin/bash
function parse() {
version=$1
while IFS= read -r line && [ -z "`echo $line | grep -e "^#\+ $version"`" ]
do
: # do nothing
done
while IFS= read -r line && [ -z "`echo $line | grep -e "^#\+"`" ]
do
echo "$line"
done
}
version=$1
file=$2
if [ -n "$file" ]; then
parse $version < "$file"
else
parse $version
fi

View File

@@ -1,54 +0,0 @@
dokkaHtml {
String dokkaOutputDirectoryPath = null
switch (true) {
case project.hasProperty("DOKKA_PATH"):
dokkaOutputDirectoryPath = project.property("DOKKA_PATH").toString()
break
case System.getenv("DOKKA_PATH") != null:
dokkaOutputDirectoryPath = System.getenv("DOKKA_PATH")
break
}
if (dokkaOutputDirectoryPath != null) {
File outputDirectoryFile = new File(dokkaOutputDirectoryPath)
outputDirectoryFile.mkdirs()
outputDirectory = outputDirectoryFile
}
moduleName.set("kdocs")
dokkaSourceSets {
switch (true) {
case project.hasProperty("DOKKA_PATH"):
outputDirectory = new File(project.property("DOKKA_PATH").toString())
break
case System.getenv("DOKKA_PATH") != null:
outputDirectory = new File(System.getenv("DOKKA_PATH"))
break
}
dokkaSourceSets {
configureEach {
skipDeprecated.set(true)
sourceLink {
localDirectory.set(file("./"))
remoteUrl.set(new URL("https://github.com/InsanusMokrassar/krontab/blob/master/"))
remoteLineSuffix.set("#L")
}
}
named("commonMain") {
sourceRoots.setFrom(findSourcesWithName("commonMain"))
}
//
// named("jsMain") {
// sourceRoot { path = "src/jsMain" }
// }
//
// named("jvmMain") {
// sourceRoot { path = "src/jvmMain" }
// }
}
}
}

View File

@@ -1,30 +0,0 @@
private String getCurrentVersionChangelog(String version) {
OutputStream changelogDataOS = new ByteArrayOutputStream()
exec {
commandLine 'chmod', "+x", './changelog_parser.sh'
}
exec {
standardOutput = changelogDataOS
commandLine './changelog_parser.sh', "$version", 'CHANGELOG.md'
}
return changelogDataOS.toString().trim()
}
if (new File(projectDir, "secret.gradle").exists()) {
apply from: './secret.gradle'
apply plugin: "com.github.breadmoirai.github-release"
githubRelease {
token "${project.property('GITHUB_RELEASE_TOKEN')}"
owner "InsanusMokrassar"
repo "${rootProject.name}"
tagName "${project.version}"
releaseName "${project.version}"
targetCommitish "${project.version}"
body getCurrentVersionChangelog("${project.version}")
}
}

View File

@@ -1,37 +1,9 @@
kotlin.code.style=official
org.gradle.parallel=true
kotlin.js.generate.externals=true
kotlin.incremental=true
kotlin.incremental.js=true
kotlin_version=1.3.61
kotlin_coroutines_version=1.3.3
gradle_bintray_plugin_version=1.8.4
klockVersion=1.8.6
kotlin.incremental.multiplatform=true
android.useAndroidX=true
android.enableJetifier=false
kotlin_version=1.6.10
kotlin_coroutines_version=1.6.0
dokka_version=1.6.0
klockVersion=2.4.10
## Github reease
github_release_plugin_version=2.2.12
## Android
android_minSdkVersion=19
android_compileSdkVersion=32
android_buildToolsVersion=32.0.0
dexcount_version=3.0.0
junit_version=4.12
test_ext_junit_version=1.1.2
espresso_core=3.3.0
androidx_work_version=2.7.1
## Common
version=0.7.0
android_code_version=11

View File

@@ -1,6 +1,6 @@
# Fri Jul 24 23:52:00 +06 2020
# Tue May 21 17:58:54 HKT 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip

57
maven.publish.gradle Normal file
View File

@@ -0,0 +1,57 @@
apply plugin: 'maven-publish'
task javadocsJar(type: Jar) {
classifier = 'javadoc'
}
afterEvaluate {
project.publishing.publications.all {
// rename artifacts
groupId "${project.group}"
if (it.name.contains('kotlinMultiplatform')) {
artifactId = "${project.name}"
} else {
artifactId = "${project.name}-$name"
}
}
}
publishing {
publications.all {
artifact javadocsJar
pom.withXml {
asNode().children().last() + {
resolveStrategy = Closure.DELEGATE_FIRST
description "It is an analog of crontab util for Kotlin Coroutines"
name "Krontab"
url "https://git.insanusmokrassar.com/InsanusMokrassar/krontab"
scm {
developerConnection "scm:git:[fetch=]https://git.insanusmokrassar.com/InsanusMokrassar/krontab.git[push=]https://git.insanusmokrassar.com/InsanusMokrassar/krontab.git"
url "https://git.insanusmokrassar.com/InsanusMokrassar/krontab.git"
}
developers {
developer {
id "InsanusMokrassar"
name "Ovsiannikov Aleksei"
email "ovsyannikov.alexey95@gmail.com"
}
}
licenses {
license {
name "Apache Software License 2.0"
url "https://git.insanusmokrassar.com/InsanusMokrassar/krontab/src/master/LICENSE"
}
}
}
}
}
}

1
mpp_config.json Normal file
View File

@@ -0,0 +1 @@
{"bintrayConfig":{"repo":"InsanusMokrassar","packageName":"${project.name}","packageVcs":"https://git.insanusmokrassar.com/InsanusMokrassar/krontab"},"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://git.insanusmokrassar.com/InsanusMokrassar/krontab/src/master/LICENSE"}],"mavenConfig":{"name":"Krontab","description":"It is an analog of crontab util for Kotlin Coroutines","url":"https://git.insanusmokrassar.com/InsanusMokrassar/krontab","vcsUrl":"https://git.insanusmokrassar.com/InsanusMokrassar/krontab.git","developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}]}}

View File

@@ -1 +0,0 @@
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/krontab/blob/master/LICENSE"}],"mavenConfig":{"name":"Krontab","description":"It is an analog of crontab util for Kotlin Coroutines","url":"https://github.com/InsanusMokrassar/krontab","vcsUrl":"https://github.com/InsanusMokrassar/krontab.git","includeGpgSigning":true,"developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/krontab"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}]}}

View File

@@ -1,69 +1,48 @@
apply plugin: 'maven-publish'
apply plugin: 'signing'
apply plugin: 'com.jfrog.bintray'
task javadocsJar(type: Jar) {
classifier = 'javadoc'
}
apply from: "maven.publish.gradle"
publishing {
publications.all {
artifact javadocsJar
pom {
description = "It is an analog of crontab util for Kotlin Coroutines"
name = "Krontab"
url = "https://github.com/InsanusMokrassar/krontab"
scm {
developerConnection = "scm:git:[fetch=]https://github.com/InsanusMokrassar/krontab.git[push=]https://github.com/InsanusMokrassar/krontab.git"
url = "https://github.com/InsanusMokrassar/krontab.git"
}
developers {
developer {
id = "InsanusMokrassar"
name = "Ovsiannikov Aleksei"
email = "ovsyannikov.alexey95@gmail.com"
}
}
licenses {
license {
name = "Apache Software License 2.0"
url = "https://github.com/InsanusMokrassar/krontab/blob/master/LICENSE"
}
bintray {
user = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')
key = project.hasProperty('BINTRAY_KEY') ? project.property('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
filesSpec {
from "${buildDir}/publications/"
eachFile {
if (it.getName() == "module.json") {
File file = it.getFile()
String directorySubname = file.parentFile.name
if (directorySubname == "kotlinMultiplatform") {
it.setPath("${project.name}/${project.version}/${project.name}-${project.version}.module")
} else {
it.setPath("${project.name}-${directorySubname}/${project.version}/${project.name}-${directorySubname}-${project.version}.module")
}
} else {
it.exclude()
}
}
repositories {
if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) {
maven {
name = "GithubPackages"
url = uri("https://maven.pkg.github.com/InsanusMokrassar/krontab")
credentials {
username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER')
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD')
}
}
}
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
maven {
name = "sonatype"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
credentials {
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
}
}
into "${project.group}".replace(".", "/")
}
pkg {
repo = "InsanusMokrassar"
name = "${project.name}"
vcsUrl = "https://git.insanusmokrassar.com/InsanusMokrassar/krontab"
licenses = ["Apache-2.0"]
version {
name = "${project.version}"
released = new Date()
vcsTag = "${project.version}"
gpg {
sign = true
passphrase = project.hasProperty('signing.gnupg.passphrase') ? project.property('signing.gnupg.passphrase') : System.getenv('signing.gnupg.passphrase')
}
}
}
}
signing {
useGpgCmd()
sign publishing.publications
bintrayUpload.doFirst {
publications = publishing.publications.collect {
it.name
}
}
bintrayUpload.dependsOn publishToMavenLocal

View File

@@ -0,0 +1,32 @@
package com.insanusmokrassar.krontab
import com.soywiz.klock.DateTime
import kotlinx.coroutines.delay
suspend inline fun KronScheduler.doWhile(noinline block: suspend () -> Boolean) {
do {
delay(next().unixMillisLong - DateTime.now().unixMillisLong)
} while (block())
}
suspend inline fun doWhile(
scheduleConfig: String,
noinline block: suspend () -> Boolean
) = createSimpleScheduler(scheduleConfig).doWhile(block)
suspend inline fun KronScheduler.doInfinity(noinline block: suspend () -> Unit) = doWhile {
block()
true
}
suspend inline fun doInfinity(
scheduleConfig: String,
noinline block: suspend () -> Unit
) = createSimpleScheduler(scheduleConfig).doInfinity(block)
suspend inline fun KronScheduler.doOnce(noinline block: suspend () -> Unit) = doWhile {
block()
false
}
suspend inline fun doOnce(
scheduleConfig: String,
noinline block: suspend () -> Unit
) = createSimpleScheduler(scheduleConfig).doOnce(block)

View File

@@ -0,0 +1,7 @@
package com.insanusmokrassar.krontab
import com.soywiz.klock.DateTime
interface KronScheduler {
suspend fun next(relatively: DateTime = DateTime.now()): DateTime
}

View File

@@ -0,0 +1,29 @@
package com.insanusmokrassar.krontab
import com.insanusmokrassar.krontab.builder.buildSchedule
import com.insanusmokrassar.krontab.internal.CronDateTime
internal val anyCronDateTime by lazy {
CronDateTime()
}
val AnyTimeScheduler: KronScheduler by lazy {
CronDateTimeScheduler(listOf(anyCronDateTime))
}
val EverySecondScheduler: KronScheduler
get() = AnyTimeScheduler
val EveryMinuteScheduler: KronScheduler by lazy {
buildSchedule { minutes { 0 every 1 } }
}
val EveryHourScheduler: KronScheduler by lazy {
buildSchedule { hours { 0 every 1 } }
}
val EveryDayOfMonthScheduler: KronScheduler by lazy {
buildSchedule { dayOfMonth { 0 every 1 } }
}
val EveryMonthScheduler: KronScheduler by lazy {
buildSchedule { months { 0 every 1 } }
}

View File

@@ -0,0 +1,68 @@
package com.insanusmokrassar.krontab
import com.insanusmokrassar.krontab.internal.*
import com.insanusmokrassar.krontab.internal.CronDateTime
import com.insanusmokrassar.krontab.internal.parseDaysOfMonth
import com.insanusmokrassar.krontab.internal.parseHours
import com.insanusmokrassar.krontab.internal.parseMinutes
import com.insanusmokrassar.krontab.internal.parseMonths
import com.insanusmokrassar.krontab.internal.parseSeconds
/**
* Parse [incoming] string and adapt according to next format: "* * * * *" where order of things:
*
* seconds
* minutes
* hours
* dayOfMonth
* month
*
* And each one have next format:
*
* {number},{number},...
*
* and {number} here is one of {int}-{int} OR {int}/{int} OR *\/{int} OR {int}.
*
* Seconds ranges can be found in [com.insanusmokrassar.krontab.internal.secondsRange].
* Minutes ranges can be found in [com.insanusmokrassar.krontab.internal.minutesRange].
* Hours ranges can be found in [com.insanusmokrassar.krontab.internal.hoursRange].
* Days of month ranges can be found in [com.insanusmokrassar.krontab.internal.dayOfMonthRange].
* Months ranges can be found in [com.insanusmokrassar.krontab.internal.monthRange].
*
* @sample "0/5 * * * *" for every five seconds triggering
* @sample "0/15 30 * * *" for every 15th seconds in a half of each hour
* @sample "1 2 3 4 5" for triggering in near first second of second minute of third hour of fourth day of may
*/
fun createSimpleScheduler(incoming: String): KronScheduler {
val (secondsSource, minutesSource, hoursSource, dayOfMonthSource, monthSource) = incoming.split(" ")
val secondsParsed = parseSeconds(secondsSource)
val minutesParsed = parseMinutes(minutesSource)
val hoursParsed = parseHours(hoursSource)
val dayOfMonthParsed = parseDaysOfMonth(dayOfMonthSource)
val monthParsed = parseMonths(monthSource)
val resultCronDateTimes = mutableListOf(CronDateTime())
secondsParsed ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(seconds = currentTime)
}
minutesParsed ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(minutes = currentTime)
}
hoursParsed ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(hours = currentTime)
}
dayOfMonthParsed ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(dayOfMonth = currentTime)
}
monthParsed ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(month = currentTime)
}
return CronDateTimeScheduler(resultCronDateTimes.toList())
}

View File

@@ -0,0 +1,104 @@
package com.insanusmokrassar.krontab.builder
import com.insanusmokrassar.krontab.CronDateTimeScheduler
import com.insanusmokrassar.krontab.KronScheduler
import com.insanusmokrassar.krontab.internal.CronDateTime
import com.insanusmokrassar.krontab.internal.fillWith
fun buildSchedule(settingsBlock: SchedulerBuilder.() -> Unit): KronScheduler {
val builder = SchedulerBuilder()
builder.settingsBlock()
return builder.build()
}
class SchedulerBuilder(
private var seconds: Array<Byte>? = null,
private var minutes: Array<Byte>? = null,
private var hours: Array<Byte>? = null,
private var dayOfMonth: Array<Byte>? = null,
private var month: Array<Byte>? = null
) {
private fun <T : TimeBuilder> callAndReturn(
initial: Array<Byte>?,
builder: T,
block: T.() -> Unit
): Array<Byte>? {
builder.block()
val builderValue = builder.build()
return initial ?.let {
builderValue ?.let { _ ->
(it + builderValue).distinct().toTypedArray()
} ?: builderValue
} ?: builderValue
}
fun seconds(block: SecondsBuilder.() -> Unit) {
seconds = callAndReturn(
seconds,
SecondsBuilder(),
block
)
}
fun minutes(block: MinutesBuilder.() -> Unit) {
minutes = callAndReturn(
minutes,
MinutesBuilder(),
block
)
}
fun hours(block: HoursBuilder.() -> Unit) {
hours = callAndReturn(
hours,
HoursBuilder(),
block
)
}
fun dayOfMonth(block: DaysOfMonthBuilder.() -> Unit) {
dayOfMonth = callAndReturn(
dayOfMonth,
DaysOfMonthBuilder(),
block
)
}
fun months(block: MonthsBuilder.() -> Unit) {
month = callAndReturn(
month,
MonthsBuilder(),
block
)
}
fun build(): KronScheduler {
val resultCronDateTimes = mutableListOf(CronDateTime())
seconds ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(seconds = currentTime)
}
minutes ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(minutes = currentTime)
}
hours ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(hours = currentTime)
}
dayOfMonth ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(dayOfMonth = currentTime)
}
month ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(month = currentTime)
}
return CronDateTimeScheduler(resultCronDateTimes.toList())
}
}

View File

@@ -0,0 +1,53 @@
package com.insanusmokrassar.krontab.builder
import com.insanusmokrassar.krontab.internal.*
import com.insanusmokrassar.krontab.utils.clamp
sealed class TimeBuilder (
private val restrictionsRange: IntRange
) {
private var result: Set<Int>? = null
fun allowAll() {
result = null
}
infix fun include(array: Array<Int>) {
val clamped = array.map { it.clamp(restrictionsRange) } + (result ?: emptySet())
result = clamped.toSet()
}
infix fun at(value: Int) {
result = (result ?: emptySet()) + value.clamp(restrictionsRange)
}
inline fun from(value: Int) = value
infix fun Int.every(delay: Int): Array<Int> {
val progression = clamp(restrictionsRange) .. restrictionsRange.last step delay
val result = progression.toSet().toTypedArray()
this@TimeBuilder include result
return result
}
infix fun every(delay: Int): Array<Int> = 0 every delay
infix fun Int.upTo(endIncluding: Int): Array<Int> {
val progression = clamp(restrictionsRange) .. endIncluding.clamp(restrictionsRange)
val result = progression.toSet().toTypedArray()
this@TimeBuilder include result
return result
}
infix fun upTo(endIncluding: Int): Array<Int> = 0 upTo endIncluding
internal fun build() = result ?.map { it.toByte() } ?.toTypedArray()
}
class SecondsBuilder : TimeBuilder(secondsRange)
class MinutesBuilder : TimeBuilder(minutesRange)
class HoursBuilder : TimeBuilder(com.insanusmokrassar.krontab.internal.hoursRange)
class DaysOfMonthBuilder : TimeBuilder(com.insanusmokrassar.krontab.internal.dayOfMonthRange)
class MonthsBuilder : TimeBuilder(monthRange)

View File

@@ -0,0 +1,77 @@
package com.insanusmokrassar.krontab.internal
import com.insanusmokrassar.krontab.utils.clamp
import com.soywiz.klock.DateTime
import com.soywiz.klock.DateTimeSpan
/**
* [month] 0-11
* [dayOfMonth] 0-31
* [hours] 0-23
* [minutes] 0-59
* [seconds] 0-59
*/
internal data class CronDateTime(
val month: Byte? = null,
val dayOfMonth: Byte? = null,
val hours: Byte? = null,
val minutes: Byte? = null,
val seconds: Byte? = null
) {
init {
check(month ?.let { it in com.insanusmokrassar.krontab.internal.monthRange } ?: true)
check(dayOfMonth ?.let { it in com.insanusmokrassar.krontab.internal.dayOfMonthRange } ?: true)
check(hours?.let { it in com.insanusmokrassar.krontab.internal.hoursRange } ?: true)
check(minutes?.let { it in com.insanusmokrassar.krontab.internal.minutesRange } ?: true)
check(seconds?.let { it in com.insanusmokrassar.krontab.internal.secondsRange } ?: true)
}
internal val klockDayOfMonth = dayOfMonth ?.plus(1)
companion object {
fun create(
month: Int? = null,
dayOfMonth: Int? = null,
hours: Int? = null,
minutes: Int? = null,
seconds: Int? = null
) = CronDateTime(
month ?.clamp(com.insanusmokrassar.krontab.internal.monthRange) ?.toByte(),
dayOfMonth ?.clamp(com.insanusmokrassar.krontab.internal.dayOfMonthRange) ?.toByte(),
hours ?.clamp(com.insanusmokrassar.krontab.internal.hoursRange) ?.toByte(),
minutes ?.clamp(com.insanusmokrassar.krontab.internal.minutesRange) ?.toByte(),
seconds ?.clamp(com.insanusmokrassar.krontab.internal.secondsRange) ?.toByte()
)
}
}
internal fun CronDateTime.toNearDateTime(relativelyTo: DateTime = DateTime.now()): DateTime {
var current = relativelyTo
seconds?.let {
val left = it - current.seconds
current += DateTimeSpan(minutes = if (left <= 0) 1 else 0, seconds = left)
}
minutes?.let {
val left = it - current.minutes
current += DateTimeSpan(hours = if (left < 0) 1 else 0, minutes = left)
}
hours?.let {
val left = it - current.hours
current += DateTimeSpan(days = if (left < 0) 1 else 0, hours = left)
}
klockDayOfMonth ?.let {
val left = it - current.dayOfMonth
current += DateTimeSpan(months = if (left < 0) 1 else 0, days = left)
}
month ?.let {
val left = it - current.month0
current += DateTimeSpan(months = if (left < 0) 1 else 0, days = left)
}
return current
}

View File

@@ -0,0 +1,13 @@
package com.insanusmokrassar.krontab
import com.insanusmokrassar.krontab.internal.*
import com.soywiz.klock.DateTime
internal data class CronDateTimeScheduler internal constructor(
internal val cronDateTimes: List<CronDateTime>
) : KronScheduler {
override suspend fun next(relatively: DateTime): DateTime {
return cronDateTimes.map { it.toNearDateTime(relatively) }.min() ?: anyCronDateTime.toNearDateTime(relatively)
}
}

View File

@@ -0,0 +1,48 @@
package com.insanusmokrassar.krontab.internal
import com.insanusmokrassar.krontab.utils.clamp
private fun createSimpleScheduler(from: String, dataRange: IntRange): Array<Byte>? {
val things = from.split(",")
val results = things.flatMap {
when {
it.contains("/") -> {
val (start, step) = it.split("/")
val startNum = (if (start.isEmpty() || start == "*") {
0
} else {
start.toInt()
}).clamp(dataRange)
val stepNum = step.toInt().clamp(dataRange)
(startNum .. dataRange.last step stepNum).map { it }
}
it == "*" -> return null
else -> listOf(it.toInt().clamp(dataRange))
}
}
return results.map { it.toByte() }.toTypedArray()
}
internal fun parseMonths(from: String) = createSimpleScheduler(from, monthRange)
internal fun parseDaysOfMonth(from: String) = createSimpleScheduler(from, dayOfMonthRange)
internal fun parseHours(from: String) = createSimpleScheduler(from, hoursRange)
internal fun parseMinutes(from: String) = createSimpleScheduler(from, minutesRange)
internal fun parseSeconds(from: String) = createSimpleScheduler(from, secondsRange)
internal fun Array<Byte>.fillWith(
whereToPut: MutableList<CronDateTime>,
createFactory: (CronDateTime, Byte) -> CronDateTime
) {
val previousValues = whereToPut.toList()
whereToPut.clear()
previousValues.forEach { previousValue ->
forEach {
whereToPut.add(createFactory(previousValue, it))
}
}
}

View File

@@ -1,7 +1,5 @@
package dev.inmo.krontab.internal
package com.insanusmokrassar.krontab.internal
internal val dayOfWeekRange = 0 .. 6
internal val yearRange = Int.MIN_VALUE .. Int.MAX_VALUE
internal val monthRange = 0 .. 11
internal val dayOfMonthRange = 0 .. 30
internal val hoursRange = 0 .. 23

View File

@@ -0,0 +1,4 @@
package com.insanusmokrassar.krontab.utils
internal fun Int.clamp(min: Int, max: Int): Int = if (this < min) min else if (this > max) max else this
internal fun Int.clamp(range: IntRange): Int = clamp(range.first, range.last)

View File

@@ -0,0 +1,25 @@
package com.insanusmokrassar.krontab.utils
import com.insanusmokrassar.krontab.KronScheduler
import com.soywiz.klock.DateTime
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@FlowPreview
fun KronScheduler.asFlow(): Flow<DateTime> = SchedulerFlow(this)
@FlowPreview
class SchedulerFlow(
private val scheduler: KronScheduler
) : AbstractFlow<DateTime>() {
@FlowPreview
override suspend fun collectSafely(collector: FlowCollector<DateTime>) {
while (true) {
val now = DateTime.now()
val nextTime = scheduler.next(now)
val sleepDelay = (nextTime - now).millisecondsLong
delay(sleepDelay)
collector.emit(nextTime)
}
}
}

View File

@@ -1,182 +0,0 @@
package dev.inmo.krontab
import com.soywiz.klock.DateTime
import com.soywiz.klock.DateTimeTz
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlin.coroutines.coroutineContext
/**
* Execute [block] once at the [KronScheduler.next] time and return result of [block] calculation.
*
* WARNING!!! If you want to launch it in parallel, you must do this explicitly.
*
* WARNING!!! In case if [KronScheduler.next] of [this] instance will return null, [block] will be called immediately
*/
suspend inline fun <T> KronScheduler.doOnceLocal(noinline block: suspend (DateTime) -> T): T {
val time = nextOrNow().also {
delay((it - DateTime.now()).millisecondsLong)
}
return block(time)
}
/**
* Execute [block] once at the [KronScheduler.next] time and return result of [block] calculation.
*
* WARNING!!! If you want to launch it in parallel, you must do this explicitly.
*
* WARNING!!! In case if [KronScheduler.next] of [this] instance will return null, [block] will be called immediatelly
*/
suspend inline fun <T> KronScheduler.doOnceTz(noinline block: suspend (DateTimeTz) -> T): T {
val time = when (this) {
is KronSchedulerTz -> nextOrNowWithOffset()
else -> nextOrNow().local
}
delay((time - DateTimeTz.nowLocal()).millisecondsLong)
return block(time)
}
/**
* Execute [block] once at the [KronScheduler.next] time and return result of [block] calculation.
*
* WARNING!!! If you want to launch it in parallel, you must do this explicitly.
*
* WARNING!!! In case if [KronScheduler.next] of [this] instance will return null, [block] will be called immediately
*/
suspend inline fun <T> KronScheduler.doOnce(noinline block: suspend () -> T): T = doOnceLocal { _ -> block() }
/**
* Will [buildSchedule] using [scheduleConfig] and call [doOnceLocal] on it
* @see buildSchedule
*/
suspend inline fun <T> doOnce(
scheduleConfig: String,
noinline block: suspend (DateTime) -> T
) = buildSchedule(scheduleConfig).doOnceLocal(block)
/**
* Will [buildSchedule] using [scheduleConfig] and call [doOnceLocal] on it
* @see buildSchedule
*/
suspend inline fun <T> doOnceTz(
scheduleConfig: String,
noinline block: suspend (DateTimeTz) -> T
) = buildSchedule(scheduleConfig).doOnceTz(block)
/**
* Will [buildSchedule] using [scheduleConfig] and call [doOnceLocal] on it
* @see buildSchedule
*/
suspend inline fun <T> doOnce(
scheduleConfig: String,
noinline block: suspend () -> T
) = doOnce(scheduleConfig) { _ -> block() }
/**
* Will execute [block] while it will return true as a result of its calculation
*/
suspend inline fun KronScheduler.doWhileLocal(noinline block: suspend (DateTime) -> Boolean) {
do {
delay(1L)
} while (doOnceLocal(block))
}
/**
* Will execute [block] while it will return true as a result of its calculation
*/
suspend inline fun KronScheduler.doWhileTz(noinline block: suspend (DateTimeTz) -> Boolean) {
do {
delay(1L)
} while (doOnceTz(block))
}
/**
* Will execute [block] while it will return true as a result of its calculation
*/
suspend inline fun KronScheduler.doWhile(noinline block: suspend () -> Boolean) = doWhileLocal { block() }
/**
* Will [buildSchedule] using [scheduleConfig] and call [doWhile] with [block]
*
* @see buildSchedule
*/
suspend inline fun doWhileLocal(
scheduleConfig: String,
noinline block: suspend (DateTime) -> Boolean
) = buildSchedule(scheduleConfig).doWhileLocal(block)
/**
* Will [buildSchedule] using [scheduleConfig] and call [doWhile] with [block]
*
* @see buildSchedule
*/
suspend inline fun doWhileTz(
scheduleConfig: String,
noinline block: suspend (DateTimeTz) -> Boolean
) = buildSchedule(scheduleConfig).doWhileTz(block)
/**
* Will [buildSchedule] using [scheduleConfig] and call [doWhile] with [block]
*
* @see buildSchedule
*/
suspend inline fun doWhile(
scheduleConfig: String,
noinline block: suspend () -> Boolean
) = doWhileLocal(scheduleConfig) { block() }
/**
* Will execute [block] without any checking of result
*/
suspend inline fun KronScheduler.doInfinityLocal(noinline block: suspend (DateTime) -> Unit) = doWhileLocal {
block(it)
coroutineContext.isActive
}
/**
* Will execute [block] without any checking of result
*/
suspend inline fun KronScheduler.doInfinityTz(noinline block: suspend (DateTimeTz) -> Unit) = doWhileTz {
block(it)
coroutineContext.isActive
}
/**
* Will execute [block] without any checking of result
*/
suspend inline fun KronScheduler.doInfinity(noinline block: suspend () -> Unit) = doWhile {
block()
coroutineContext.isActive
}
/**
* Will [buildSchedule] using [scheduleConfig] and call [doInfinity] with [block]
*
* @see buildSchedule
*/
suspend inline fun doInfinityLocal(
scheduleConfig: String,
noinline block: suspend (DateTime) -> Unit
) = buildSchedule(scheduleConfig).doInfinityLocal(block)
/**
* Will [buildSchedule] using [scheduleConfig] and call [doInfinity] with [block]
*
* @see buildSchedule
*/
suspend inline fun doInfinityTz(
scheduleConfig: String,
noinline block: suspend (DateTimeTz) -> Unit
) = buildSchedule(scheduleConfig).doInfinityTz(block)
/**
* Will [buildSchedule] using [scheduleConfig] and call [doInfinity] with [block]
*
* @see buildSchedule
*/
suspend inline fun doInfinity(
scheduleConfig: String,
noinline block: suspend () -> Unit
) = buildSchedule(scheduleConfig).doInfinity(block)

View File

@@ -1,26 +0,0 @@
package dev.inmo.krontab
import com.soywiz.klock.DateTime
/**
* This interface was created for abstraction of [next] operation. Currently, there is only
* [dev.inmo.krontab.internal.CronDateTimeScheduler] realisation of this interface inside of this library,
* but you it is possible to create your own realisation of this interface for scheduling, for example, depending of
* users activity or something like this
*
* @see dev.inmo.krontab.internal.CronDateTimeScheduler
*/
interface KronScheduler {
/**
* @return Next [DateTime] when some action must be triggered according to settings of this instance
*
* @see dev.inmo.krontab.internal.CronDateTimeScheduler.next
*/
suspend fun next(relatively: DateTime = DateTime.now()): DateTime?
}
suspend fun KronScheduler.nextOrRelative(relatively: DateTime = DateTime.now()): DateTime = next(relatively) ?: getAnyNext(relatively)
suspend fun KronScheduler.nextOrNow(): DateTime = DateTime.now().let {
next(it) ?: getAnyNext(it)
}

View File

@@ -1,34 +0,0 @@
package dev.inmo.krontab
import com.soywiz.klock.DateTime
import com.soywiz.klock.DateTimeTz
/**
* This interface extending [KronScheduler] to use [DateTimeTz] with taking into account offset of incoming time for
* [next] operation.
*
* @see dev.inmo.krontab.internal.CronDateTimeScheduler
* @see dev.inmo.krontab.KronScheduler
*/
interface KronSchedulerTz : KronScheduler {
suspend fun next(relatively: DateTimeTz): DateTimeTz?
override suspend fun next(relatively: DateTime): DateTime? = next(relatively.localUnadjusted) ?.local
}
suspend fun KronSchedulerTz.nextOrRelative(relatively: DateTimeTz): DateTimeTz = next(relatively) ?: getAnyNext(
relatively.local
).toOffsetUnadjusted(relatively.offset)
suspend fun KronSchedulerTz.nextOrNowWithOffset(): DateTimeTz = DateTimeTz.nowLocal().let {
next(it) ?: getAnyNext(
it.local
).toOffsetUnadjusted(it.offset)
}
suspend fun KronScheduler.next(relatively: DateTimeTz) = if (this is KronSchedulerTz) {
this.next(relatively)
} else {
this.next(relatively.local) ?.toOffsetUnadjusted(relatively.offset)
}
suspend fun KronScheduler.nextTimeZoned() = next(DateTime.now().local)

View File

@@ -1,46 +0,0 @@
package dev.inmo.krontab
import dev.inmo.krontab.collection.CollectionKronScheduler
import dev.inmo.krontab.collection.includeAll
import dev.inmo.krontab.internal.*
/**
* Create new one [CollectionKronScheduler] to include all [KronScheduler]s of [this] [Iterator]
*
* @see CollectionKronScheduler
* @see CollectionKronScheduler.include
*/
fun Iterator<KronScheduler>.merge(): CollectionKronScheduler {
val cronDateTimes = mutableListOf<CronDateTime>()
val timezonedCronDateTimes = mutableListOf<CronDateTimeSchedulerTz>()
val collectionScheduler = CollectionKronScheduler()
forEach {
when (it) {
is CronDateTimeScheduler -> cronDateTimes.add(it.cronDateTime)
is CronDateTimeSchedulerTz -> timezonedCronDateTimes.add(it)
else -> collectionScheduler.include(it)
}
}
if (cronDateTimes.isNotEmpty()) {
collectionScheduler.include(CronDateTimeScheduler(cronDateTimes.merge()))
}
if (timezonedCronDateTimes.isNotEmpty()) {
collectionScheduler.includeAll(mergeCronDateTimeSchedulers(timezonedCronDateTimes))
}
return collectionScheduler
}
/**
* Create new one [CollectionKronScheduler] to include all [KronScheduler]s of [this] [Iterator]
*
* @see CollectionKronScheduler
* @see CollectionKronScheduler.include
*/
@Suppress("NOTHING_TO_INLINE")
inline fun Iterable<KronScheduler>.merge(): CollectionKronScheduler = iterator().merge()
/**
* @return Vararg shortcut for [merge]
*/
@Suppress("NOTHING_TO_INLINE")
inline fun merge(vararg kronDateTimeSchedulers: KronScheduler): CollectionKronScheduler = kronDateTimeSchedulers.iterator().merge()

View File

@@ -1,18 +0,0 @@
package dev.inmo.krontab
/**
* This class contains [template] and can be simply serialized/deserialized. In fact that class will work as
* [dev.inmo.krontab.internal.CronDateTimeScheduler] due to the fact that [toKronScheduler] will return it under the
* hood
*/
data class KrontabTemplateWrapper(
val template: KrontabTemplate
) : KronScheduler by template.toKronScheduler()
/**
* Will create [KrontabTemplateWrapper] from [this] [KrontabTemplate]
*
* @see [toKronScheduler]
* @see [KrontabTemplateWrapper]
*/
fun KrontabTemplate.wrapAsKronScheduler() = KrontabTemplateWrapper(this)

View File

@@ -1,59 +0,0 @@
package dev.inmo.krontab
import com.soywiz.klock.DateTime
import dev.inmo.krontab.builder.buildSchedule
import dev.inmo.krontab.internal.*
internal val anyCronDateTime by lazy {
CronDateTime()
}
internal fun getAnyNext(relatively: DateTime) = anyCronDateTime.toNearDateTime(relatively)!!
/**
* [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now]
*/
val AnyTimeScheduler: KronScheduler by lazy {
CronDateTimeScheduler(anyCronDateTime)
}
/**
* [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one second
*/
val EverySecondScheduler: KronScheduler by lazy {
buildSchedule { seconds { 0 every 1 } }
}
/**
* [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one minute
*/
val EveryMinuteScheduler: KronScheduler by lazy {
buildSchedule { minutes { 0 every 1 } }
}
/**
* [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one hour
*/
val EveryHourScheduler: KronScheduler by lazy {
buildSchedule { hours { 0 every 1 } }
}
/**
* [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one day
*/
val EveryDayOfMonthScheduler: KronScheduler by lazy {
buildSchedule { dayOfMonth { 0 every 1 } }
}
/**
* [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one month
*/
val EveryMonthScheduler: KronScheduler by lazy {
buildSchedule { months { 0 every 1 } }
}
/**
* [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one year
*/
val EveryYearScheduler: KronScheduler by lazy {
buildSchedule { years { 0 every 1 } }
}

View File

@@ -1,150 +0,0 @@
package dev.inmo.krontab
import com.soywiz.klock.TimezoneOffset
import com.soywiz.klock.minutes
import dev.inmo.krontab.internal.*
import dev.inmo.krontab.utils.Minutes
/**
* @see createSimpleScheduler
* @see buildSchedule
*/
typealias KrontabTemplate = String
/**
* Parse [incoming] string and adapt according to next format: "* * * * *" where order of things:
*
* * seconds
* * minutes
* * hours
* * dayOfMonth
* * month
* * (optional) year
* * (optional) (can be placed anywhere after month) (must be marked with `o` at the end, for example: 60o == +01:00) offset
* * (optional) (can be placed anywhere after month) dayOfWeek
*
* And each one (except of offsets) have next format:
*
* `{number}[,{number},...]` or `*`
*
* and {number} here is one of
*
* * {int}-{int}
* * {int}/{int}
* * *&#47;{int}
* * {int}
* * F
* * L
*
* Week days must be marked with `w` at the end, and starts with 0 which means Sunday. For example, 0w == Sunday. With
* weeks you can use syntax like with any number like seconds, for example: 0-2w means Sunday-Tuesday
*
* Additional info about ranges can be found in follow accordance:
*
* * Seconds ranges can be found in [secondsRange]
* * Minutes ranges can be found in [minutesRange]
* * Hours ranges can be found in [hoursRange]
* * Days of month ranges can be found in [dayOfMonthRange]
* * Months ranges can be found in [monthRange]
* * Years ranges can be found in [yearRange] (in fact - any [Int])
* * WeekDay (timezone) ranges can be found in [dayOfWeekRange]
*
* Examples:
*
* * "0/5 * * * *" for every five seconds triggering
* * "0/5,L * * * *" for every five seconds triggering and on 59 second
* * "0/15 30 * * *" for every 15th seconds in a half of each hour
* * "1 2 3 F,4,L 5" for triggering in near first second of second minute of third hour of fourth day of may
* * "1 2 3 F,4,L 5 60o" for triggering in near first second of second minute of third hour of fourth day of may with timezone UTC+01:00
* * "1 2 3 F,4,L 5 60o 0-2w" for triggering in near first second of second minute of third hour of fourth day of may in case if it will be in Sunday-Tuesday week days with timezone UTC+01:00
* * "1 2 3 F,4,L 5 2021" for triggering in near first second of second minute of third hour of fourth day of may of 2021st year
* * "1 2 3 F,4,L 5 2021 60o" for triggering in near first second of second minute of third hour of fourth day of may of 2021st year with timezone UTC+01:00
* * "1 2 3 F,4,L 5 2021 60o 0-2w" for triggering in near first second of second minute of third hour of fourth day of may of 2021st year if it will be in Sunday-Tuesday week days with timezone UTC+01:00
*
* @return In case when offset parameter is absent in [incoming] will be used [createSimpleScheduler] method and
* returned [CronDateTimeScheduler]. In case when offset parameter there is in [incoming] [KrontabTemplate] will be used
* [createKronSchedulerWithOffset] and returned [CronDateTimeSchedulerTz]
*
* @see dev.inmo.krontab.internal.createKronScheduler
*/
fun createSimpleScheduler(
incoming: KrontabTemplate
): KronScheduler {
var offsetParsed: Int? = null
var dayOfWeekParsed: Array<Byte>? = null
var yearParsed: Array<Int>? = null
val (secondsSource, minutesSource, hoursSource, dayOfMonthSource, monthSource) = incoming.split(" ").also {
listOfNotNull(
it.getOrNull(5),
it.getOrNull(6),
it.getOrNull(7)
).forEach {
val offsetFromString = parseOffset(it)
val dayOfWeekFromString = parseWeekDay(it)
offsetParsed = offsetParsed ?: offsetFromString
dayOfWeekParsed = dayOfWeekParsed ?: dayOfWeekFromString
when {
dayOfWeekFromString != null || offsetFromString != null -> return@forEach
yearParsed == null -> {
yearParsed = parseYears(it)
}
}
}
}
val secondsParsed = parseSeconds(secondsSource)
val minutesParsed = parseMinutes(minutesSource)
val hoursParsed = parseHours(hoursSource)
val dayOfMonthParsed = parseDaysOfMonth(dayOfMonthSource)
val monthParsed = parseMonths(monthSource)
return offsetParsed ?.let { offset ->
createKronSchedulerWithOffset(
secondsParsed, minutesParsed, hoursParsed, dayOfMonthParsed, monthParsed, yearParsed, dayOfWeekParsed, TimezoneOffset(offset.minutes)
)
} ?: createKronScheduler(
secondsParsed, minutesParsed, hoursParsed, dayOfMonthParsed, monthParsed, yearParsed, dayOfWeekParsed
)
}
fun createSimpleScheduler(
incoming: KrontabTemplate,
defaultOffset: Minutes
): KronSchedulerTz {
val scheduler = createSimpleScheduler(incoming)
return if (scheduler is KronSchedulerTz) {
scheduler
} else {
CronDateTimeSchedulerTz(
(scheduler as CronDateTimeScheduler).cronDateTime,
TimezoneOffset(defaultOffset.minutes)
)
}
}
/**
* Shortcut for [createSimpleScheduler]
*/
fun buildSchedule(incoming: KrontabTemplate): KronScheduler = createSimpleScheduler(incoming)
/**
* Shortcut for [createSimpleScheduler]
*/
fun buildSchedule(incoming: KrontabTemplate, defaultOffset: Minutes): KronSchedulerTz = createSimpleScheduler(incoming, defaultOffset)
/**
* Shortcut for [buildSchedule]
*/
fun KrontabTemplate.toSchedule(): KronScheduler = buildSchedule(this)
/**
* Shortcut for [buildSchedule]
*/
fun KrontabTemplate.toSchedule(defaultOffset: Minutes): KronSchedulerTz = buildSchedule(this, defaultOffset)
/**
* Shortcut for [buildSchedule]
*/
fun KrontabTemplate.toKronScheduler(): KronScheduler = buildSchedule(this)
/**
* Shortcut for [buildSchedule]
*/
fun KrontabTemplate.toKronScheduler(defaultOffset: Minutes): KronSchedulerTz = buildSchedule(this, defaultOffset)

View File

@@ -1,152 +0,0 @@
package dev.inmo.krontab.builder
import com.soywiz.klock.TimezoneOffset
import com.soywiz.klock.minutes
import dev.inmo.krontab.KronScheduler
import dev.inmo.krontab.KronSchedulerTz
import dev.inmo.krontab.internal.createKronScheduler
import dev.inmo.krontab.internal.createKronSchedulerWithOffset
import dev.inmo.krontab.utils.Minutes
/**
* Will help to create an instance of [KronScheduler]
*
* @see dev.inmo.krontab.createSimpleScheduler
*/
fun buildSchedule(settingsBlock: SchedulerBuilder.() -> Unit): KronScheduler {
val builder = SchedulerBuilder()
builder.settingsBlock()
return builder.build()
}
/**
* Will help to create an instance of [KronScheduler]
*
* @see dev.inmo.krontab.createSimpleScheduler
*/
fun buildSchedule(
offset: Minutes,
settingsBlock: SchedulerBuilder.() -> Unit
): KronSchedulerTz {
val builder = SchedulerBuilder(offset = offset)
builder.settingsBlock()
return builder.build() as KronSchedulerTz
}
class SchedulerBuilder(
private var seconds: Array<Byte>? = null,
private var minutes: Array<Byte>? = null,
private var hours: Array<Byte>? = null,
private var dayOfMonth: Array<Byte>? = null,
private var month: Array<Byte>? = null,
private var year: Array<Int>? = null,
private var dayOfWeek: Array<Byte>? = null,
private val offset: Minutes? = null
) {
private fun <I, T : TimeBuilder<I>> callAndReturn(
initial: Array<I>?,
builder: T,
block: T.() -> Unit
): List<I>? {
builder.block()
val builderValue = builder.build()
return initial ?.let {
builderValue ?.let { _ ->
(it + builderValue).distinct()
} ?: builderValue
} ?: builderValue
}
/**
* Starts an seconds block
*/
fun seconds(block: SecondsBuilder.() -> Unit) {
seconds = callAndReturn(
seconds,
SecondsBuilder(),
block
) ?.toTypedArray()
}
/**
* Starts an minutes block
*/
fun minutes(block: MinutesBuilder.() -> Unit) {
minutes = callAndReturn(
minutes,
MinutesBuilder(),
block
) ?.toTypedArray()
}
/**
* Starts an hours block
*/
fun hours(block: HoursBuilder.() -> Unit) {
hours = callAndReturn(
hours,
HoursBuilder(),
block
) ?.toTypedArray()
}
/**
* Starts an days of month block
*/
fun dayOfMonth(block: DaysOfMonthBuilder.() -> Unit) {
dayOfMonth = callAndReturn(
dayOfMonth,
DaysOfMonthBuilder(),
block
) ?.toTypedArray()
}
/**
* Starts an hours block
*/
fun dayOfWeek(block: WeekDaysBuilder.() -> Unit) {
dayOfWeek = callAndReturn(
dayOfWeek,
WeekDaysBuilder(),
block
) ?.toTypedArray()
}
/**
* Starts an months block
*/
fun months(block: MonthsBuilder.() -> Unit) {
month = callAndReturn(
month,
MonthsBuilder(),
block
) ?.toTypedArray()
}
/**
* Starts an year block
*/
fun years(block: YearsBuilder.() -> Unit) {
year = callAndReturn(
year,
YearsBuilder(),
block
) ?.toTypedArray()
}
/**
* @return Completely built and independent [KronScheduler]
*
* @see dev.inmo.krontab.createSimpleScheduler
* @see dev.inmo.krontab.internal.createKronScheduler
*/
fun build(): KronScheduler = offset ?.let {
createKronSchedulerWithOffset(seconds, minutes, hours, dayOfMonth, month, year, dayOfWeek, TimezoneOffset(it.minutes))
} ?: createKronScheduler(seconds, minutes, hours, dayOfMonth, month, year, dayOfWeek)
}

View File

@@ -1,131 +0,0 @@
package dev.inmo.krontab.builder
import dev.inmo.krontab.internal.*
/**
* This class was created for incapsulation of builder work with specified [restrictionsRange]. For example,
* [include] function of [TimeBuilder] will always [coerceIn] incoming data using its [restrictionsRange]
*/
sealed class TimeBuilder<T : Number> (
private val restrictionsRange: IntRange,
private val converter: Converter<T>
) {
private var result: Set<Int>? = null
/**
* The first possible value of builder
*/
val first
get() = restrictionsRange.first
/**
* The last possible value of builder. Using of this variable equal to using "L" in strings
*/
val last
get() = restrictionsRange.last
/**
* After calling of this function this builder will allow any value of current time
*/
@Suppress("unused")
fun allowAll() {
result = null
}
/**
* Will include all variations from this array inside of this timeline
*/
@Suppress("MemberVisibilityCanBePrivate")
infix fun include(array: Array<Int>) {
val clamped = array.map { it.coerceIn(restrictionsRange) } + (result ?: emptySet())
result = clamped.toSet()
}
/**
* Add one [value] to current timeline
*/
@Suppress("unused")
infix fun at(value: Int) {
result = (result ?: emptySet()) + value.coerceIn(restrictionsRange)
}
/**
* Shortcut for [at]. In fact will
*/
@Suppress("unused", "NOTHING_TO_INLINE")
inline infix fun each(value: Int) = at(value)
/**
* Just wrapper for more obvious writing something like "[from] 2 [every] 5". For example, for [SecondsBuilder] it
* will mean "[from] second second [every] 5 seconds", or "2, 7, 13, ..."
*/
@Suppress("NOTHING_TO_INLINE")
inline infix fun from(value: Int) = value
/**
* Will create an sequence of times starting [from] [this] [every] [delay] times. For example, for [SecondsBuilder] it
* will mean "[from] second second [every] 5 seconds", or "2, 7, 13, ..."
*
* @see [from]
*/
infix fun Int.every(delay: Int): Array<Int> {
val progression = coerceIn(restrictionsRange) .. restrictionsRange.last step delay
val result = progression.toSet().toTypedArray()
this@TimeBuilder include result
return result
}
/**
* Shortcut for "[from] 0 [every] [delay]"
*/
infix fun every(delay: Int): Array<Int> = this from 0 every delay
/**
* Will fill up this timeline from [this] up to [endIncluding]
*/
@Suppress("MemberVisibilityCanBePrivate")
infix fun Int.upTo(endIncluding: Int): Array<Int> {
val progression = coerceIn(restrictionsRange) .. endIncluding.coerceIn(restrictionsRange)
val result = progression.toSet().toTypedArray()
this@TimeBuilder include result
return result
}
/**
* Shortcut for "[from] 0 [upTo] [endIncluding]"
*/
@Suppress("unused")
infix fun upTo(endIncluding: Int): Array<Int> = this from 0 upTo endIncluding
/**
* Will fill up this timeline from [this] up to [endIncluding]
*/
@Suppress("MemberVisibilityCanBePrivate")
infix operator fun Int.rangeTo(endIncluding: Int) = upTo(endIncluding)
/**
* Shortcut for "[from] 0 [rangeTo] [endIncluding]"
*/
@Suppress("MemberVisibilityCanBePrivate")
infix operator fun rangeTo(endIncluding: Int) = (this from 0) rangeTo endIncluding
/**
* Will include the last possible value
*/
fun includeLast() = at(restrictionsRange.last)
/**
* Will include the first possible value
*/
fun includeFirst() = at(restrictionsRange.first)
internal fun build() = result ?.map(converter)
}
class SecondsBuilder : TimeBuilder<Byte>(secondsRange, intToByteConverter)
class MinutesBuilder : TimeBuilder<Byte>(minutesRange, intToByteConverter)
class HoursBuilder : TimeBuilder<Byte>(hoursRange, intToByteConverter)
class DaysOfMonthBuilder : TimeBuilder<Byte>(dayOfMonthRange, intToByteConverter)
class MonthsBuilder : TimeBuilder<Byte>(monthRange, intToByteConverter)
class YearsBuilder : TimeBuilder<Int>(yearRange, intToIntConverter)
class WeekDaysBuilder : TimeBuilder<Byte>(dayOfWeekRange, intToByteConverter)

View File

@@ -1,25 +0,0 @@
package dev.inmo.krontab.collection
import dev.inmo.krontab.KronScheduler
@Suppress("NOTHING_TO_INLINE")
inline fun CollectionKronScheduler.includeAll(kronSchedulers: List<KronScheduler>) {
kronSchedulers.forEach {
include(it)
}
}
@Suppress("NOTHING_TO_INLINE")
inline fun CollectionKronScheduler.includeAll(vararg kronSchedulers: KronScheduler) {
includeAll(kronSchedulers.toList())
}
operator fun KronScheduler.plus(kronScheduler: KronScheduler): CollectionKronScheduler {
return CollectionKronScheduler().apply {
includeAll(this, kronScheduler)
}
}
operator fun CollectionKronScheduler.plusAssign(kronScheduler: KronScheduler) {
include(kronScheduler)
}

View File

@@ -1,66 +0,0 @@
package dev.inmo.krontab.collection
import com.soywiz.klock.DateTime
import com.soywiz.klock.DateTimeTz
import dev.inmo.krontab.*
import dev.inmo.krontab.internal.*
/**
* This scheduler will be useful in case you want to unite several different [KronScheduler]s
*/
data class CollectionKronScheduler internal constructor(
internal val schedulers: MutableList<KronScheduler>
) : KronSchedulerTz {
internal constructor() : this(mutableListOf())
/**
* Add [kronScheduler] into its [schedulers] list
*
* * When [kronScheduler] is [CronDateTimeScheduler] it will merge all [CronDateTimeScheduler]s from [schedulers] list
* and this [kronScheduler] using [mergeCronDateTimeSchedulers] function
* * When [kronScheduler] is [CollectionKronScheduler] it this instance will include all [kronScheduler]
* [schedulers]
* * Otherwise [kronScheduler] will be added to [schedulers] list
*/
fun include(kronScheduler: KronScheduler) {
when (kronScheduler) {
is CronDateTimeScheduler -> {
val resultCronDateTimes = mutableListOf(kronScheduler)
schedulers.removeAll {
if (it is CronDateTimeScheduler) {
resultCronDateTimes.add(it)
} else {
false
}
}
schedulers.add(
mergeCronDateTimeSchedulers(resultCronDateTimes)
)
}
is CronDateTimeSchedulerTz -> {
val newCronDateTimes = mutableListOf(kronScheduler.cronDateTime)
schedulers.removeAll {
if (it is CronDateTimeSchedulerTz && it.offset == kronScheduler.offset) {
newCronDateTimes.add(it.cronDateTime)
true
} else {
false
}
}
schedulers.add(CronDateTimeSchedulerTz(newCronDateTimes.merge(), kronScheduler.offset))
}
is CollectionKronScheduler -> kronScheduler.schedulers.forEach {
include(it)
}
else -> schedulers.add(kronScheduler)
}
}
override suspend fun next(relatively: DateTime): DateTime {
return schedulers.mapNotNull { it.next(relatively) }.minOrNull() ?: getAnyNext(relatively)
}
override suspend fun next(relatively: DateTimeTz): DateTimeTz {
return schedulers.mapNotNull { it.next(relatively) }.minOrNull() ?: getAnyNext(relatively.local).toOffsetUnadjusted(relatively.offset)
}
}

View File

@@ -1,107 +0,0 @@
package dev.inmo.krontab.internal
import com.soywiz.klock.DateTime
import com.soywiz.klock.TimezoneOffset
import dev.inmo.krontab.KronScheduler
/**
* @param daysOfWeek 0-6
* @param years any int
* @param months 0-11
* @param daysOfMonth 0-31
* @param hours 0-23
* @param minutes 0-59
* @param seconds 0-59
*/
internal data class CronDateTime(
val daysOfWeek: Array<Byte>? = null,
val years: Array<Int>? = null,
val months: Array<Byte>? = null,
val daysOfMonth: Array<Byte>? = null,
val hours: Array<Byte>? = null,
val minutes: Array<Byte>? = null,
val seconds: Array<Byte>? = null
) {
init {
check(daysOfWeek ?.all { it in dayOfWeekRange } ?: true)
check(years?.all { it in yearRange } ?: true)
check(months?.all { it in monthRange } ?: true)
check(daysOfMonth ?.all { it in dayOfMonthRange } ?: true)
check(hours?.all { it in hoursRange } ?: true)
check(minutes?.all { it in minutesRange } ?: true)
check(seconds?.all { it in secondsRange } ?: true)
}
internal val calculators = listOf(
years ?.let { NearDateTimeCalculatorYears(it) },
daysOfWeek ?.let { NearDateTimeCalculatorWeekDays(it) },
NearDateTimeCalculatorMillis(arrayOf(0)),
seconds ?.let { NearDateTimeCalculatorSeconds(it) },
minutes ?.let { NearDateTimeCalculatorMinutes(it) },
hours ?.let { NearDateTimeCalculatorHours(it) },
daysOfMonth ?.let { NearDateTimeCalculatorDays(it) },
months ?.let { NearDateTimeCalculatorMonths(it) },
)
internal fun toNearDateTime(relativelyTo: DateTime = DateTime.now()): DateTime? {
var current = relativelyTo
whileLoop@while (true) {
for (calculator in calculators) {
val (calculated, requireRecalculation) = (calculator ?: continue).calculateNearTime(current) ?: return null
current = calculated
if (requireRecalculation) {
continue@whileLoop
}
}
return current
}
}
}
internal fun createCronDateTime(
seconds: Array<Byte>? = null,
minutes: Array<Byte>? = null,
hours: Array<Byte>? = null,
dayOfMonth: Array<Byte>? = null,
month: Array<Byte>? = null,
years: Array<Int>? = null,
weekDays: Array<Byte>? = null
): CronDateTime {
return CronDateTime(weekDays, years, month, dayOfMonth, hours, minutes, seconds)
}
/**
* @return [KronScheduler] (in fact [CronDateTimeScheduler]) based on incoming data
*/
internal fun createKronScheduler(
seconds: Array<Byte>? = null,
minutes: Array<Byte>? = null,
hours: Array<Byte>? = null,
dayOfMonth: Array<Byte>? = null,
month: Array<Byte>? = null,
years: Array<Int>? = null,
weekDays: Array<Byte>? = null
): KronScheduler = CronDateTimeScheduler(createCronDateTime(seconds, minutes, hours, dayOfMonth, month, years, weekDays))
/**
* @return [KronScheduler] (in fact [CronDateTimeScheduler]) based on incoming data
*/
internal fun createKronSchedulerWithOffset(
seconds: Array<Byte>? = null,
minutes: Array<Byte>? = null,
hours: Array<Byte>? = null,
dayOfMonth: Array<Byte>? = null,
month: Array<Byte>? = null,
years: Array<Int>? = null,
weekDays: Array<Byte>? = null,
offset: TimezoneOffset
): KronScheduler = CronDateTimeSchedulerTz(createCronDateTime(seconds, minutes, hours, dayOfMonth, month, years, weekDays), offset)
internal fun List<CronDateTime>.merge() = CronDateTime(
flatMap { it.daysOfWeek ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
flatMap { it.years ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
flatMap { it.months ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
flatMap { it.daysOfMonth ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
flatMap { it.hours ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
flatMap { it.minutes ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
flatMap { it.seconds ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
)

View File

@@ -1,37 +0,0 @@
package dev.inmo.krontab.internal
import com.soywiz.klock.DateTime
import dev.inmo.krontab.KronScheduler
/**
* Cron-oriented realisation of [KronScheduler]
*
* @see dev.inmo.krontab.AnyTimeScheduler
* @see dev.inmo.krontab.EverySecondScheduler
* @see dev.inmo.krontab.EveryMinuteScheduler
* @see dev.inmo.krontab.EveryHourScheduler
* @see dev.inmo.krontab.EveryDayOfMonthScheduler
* @see dev.inmo.krontab.EveryMonthScheduler
* @see dev.inmo.krontab.EveryYearScheduler
*
* @see dev.inmo.krontab.builder.buildSchedule
* @see dev.inmo.krontab.builder.SchedulerBuilder
*/
internal data class CronDateTimeScheduler internal constructor(
internal val cronDateTime: CronDateTime
) : KronScheduler {
/**
* @return Near date using [cronDateTimes] list and getting the [Iterable.minByOrNull] one
*
* @see toNearDateTime
*/
override suspend fun next(relatively: DateTime): DateTime? {
return cronDateTime.toNearDateTime(relatively)
}
}
internal fun mergeCronDateTimeSchedulers(
schedulers: List<CronDateTimeScheduler>
): CronDateTimeScheduler = CronDateTimeScheduler(
schedulers.map { it.cronDateTime }.merge()
)

View File

@@ -1,32 +0,0 @@
package dev.inmo.krontab.internal
import com.soywiz.klock.DateTimeTz
import com.soywiz.klock.TimezoneOffset
import dev.inmo.krontab.KronScheduler
import dev.inmo.krontab.KronSchedulerTz
/**
* Cron-oriented realisation of [KronScheduler] with taking into account [offset] for list of [cronDateTimes]
*
* @see CronDateTime
*/
internal data class CronDateTimeSchedulerTz internal constructor(
internal val cronDateTime: CronDateTime,
internal val offset: TimezoneOffset
) : KronSchedulerTz {
override suspend fun next(relatively: DateTimeTz): DateTimeTz? {
val dateTimeWithActualOffset = relatively.toOffset(offset).local
return cronDateTime.toNearDateTime(dateTimeWithActualOffset) ?.toOffsetUnadjusted(offset) ?.toOffset(relatively.offset)
}
}
internal fun mergeCronDateTimeSchedulers(
schedulers: List<CronDateTimeSchedulerTz>
) = schedulers.groupBy {
it.offset
}.map { (offset, schedulers) ->
CronDateTimeSchedulerTz(
schedulers.map { it.cronDateTime }.merge(),
offset
)
}

View File

@@ -1,183 +0,0 @@
package dev.inmo.krontab.internal
import com.soywiz.klock.*
import dev.inmo.krontab.utils.copy
import kotlin.math.min
fun interface NearDateTimeCalculator {
/**
* @return pair of near [DateTime] for this checker and [Boolean] flag that all previous calculations must be
* recalculated
*/
fun calculateNearTime(
relativelyTo: DateTime
): Pair<DateTime, Boolean>?
}
internal class CommonNearDateTimeCalculator<T>(
private val times: Array<T>,
private val partGetter: (DateTime) -> T,
private val partSetter: (DateTime, T) -> DateTime?
) : NearDateTimeCalculator where T : Comparable<T>, T : Number {
/**
* @return pair of near [DateTime] for this checker and [Boolean] flag that all previous calculations must be
* recalculated
*/
override fun calculateNearTime(
relativelyTo: DateTime
): Pair<DateTime, Boolean>? {
val currentData = partGetter(relativelyTo)
val greaterOrEquals = times.firstOrNull { it >= currentData }
val newDateTime = when (greaterOrEquals) {
null -> partSetter(relativelyTo, times.first()) ?: return null
currentData -> relativelyTo
else -> partSetter(relativelyTo, greaterOrEquals) ?: return null
}
return if (newDateTime == relativelyTo) {
relativelyTo to false
} else {
newDateTime to true
}
}
}
internal fun NearDateTimeCalculatorMillis(
times: Array<Short>
) = CommonNearDateTimeCalculator(
times,
{ it.milliseconds.toShort() },
{ dateTime, newOne ->
(if (newOne < dateTime.milliseconds) {
dateTime.plus(1.seconds)
} else {
dateTime
}).copy(milliseconds = newOne.toInt())
}
)
internal fun NearDateTimeCalculatorSeconds(
times: Array<Byte>
) = CommonNearDateTimeCalculator(
times,
{ it.seconds.toByte() },
{ dateTime, newOne ->
(if (newOne < dateTime.seconds) {
dateTime.plus(1.minutes)
} else {
dateTime
}).copy(second = newOne.toInt(), milliseconds = 0)
}
)
internal fun NearDateTimeCalculatorMinutes(
times: Array<Byte>
) = CommonNearDateTimeCalculator(
times,
{ it.minutes.toByte() },
{ dateTime, newOne ->
(if (newOne < dateTime.minutes) {
dateTime.plus(1.hours)
} else {
dateTime
}).copy(minute = newOne.toInt(), second = 0, milliseconds = 0)
}
)
internal fun NearDateTimeCalculatorHours(
times: Array<Byte>
) = CommonNearDateTimeCalculator(
times,
{ it.hours.toByte() },
{ dateTime, newOne ->
(if (newOne < dateTime.hours) {
dateTime.plus(1.days)
} else {
dateTime
}).copy(hour = newOne.toInt(), minute = 0, second = 0, milliseconds = 0)
}
)
internal fun NearDateTimeCalculatorDays(
times: Array<Byte>
) = CommonNearDateTimeCalculator(
times,
{ it.dayOfMonth.toByte() },
{ dateTime, newOne ->
(if (newOne < dateTime.dayOfMonth) {
dateTime.plus(1.months)
} else {
dateTime
}).copy(
dayOfMonth = min(dateTime.month.days(dateTime.year), newOne.toInt() + 1), // index1
hour = 0,
minute = 0,
second = 0,
milliseconds = 0
)
}
)
internal fun NearDateTimeCalculatorMonths(
times: Array<Byte>
) = CommonNearDateTimeCalculator(
times,
{ it.dayOfMonth.toByte() },
{ dateTime, newOne ->
(if (newOne < dateTime.month0) {
dateTime.plus(1.years)
} else {
dateTime
}).copy(
month = newOne.toInt() + 1, // index1
dayOfMonth = 1, // index1
hour = 0,
minute = 0,
second = 0,
milliseconds = 0
)
}
)
internal fun NearDateTimeCalculatorWeekDays(
times: Array<Byte>
) = CommonNearDateTimeCalculator(
times,
{ it.dayOfWeek.index0.toByte() },
{ dateTime, newOne ->
val currentDayOfWeek = dateTime.dayOfWeek.index0
if (newOne.toInt() == currentDayOfWeek) return@CommonNearDateTimeCalculator dateTime
(if (newOne < currentDayOfWeek) {
dateTime.plus(7.days - (currentDayOfWeek - newOne).days)
} else {
dateTime.plus(newOne.toInt().days - currentDayOfWeek.days)
}).copy(
hour = 0,
minute = 0,
second = 0,
milliseconds = 0
)
}
)
internal fun NearDateTimeCalculatorYears(
times: Array<Int>
) = CommonNearDateTimeCalculator(
times,
{ it.yearInt },
{ dateTime, newOne ->
val currentYear = dateTime.yearInt
if (newOne == currentYear) return@CommonNearDateTimeCalculator dateTime
(if (newOne < currentYear) {
null
} else {
dateTime.plus(newOne.years - currentYear.years)
}) ?.copy(
month = 1, // index1
dayOfMonth = 1, // index1
hour = 0,
minute = 0,
second = 0,
milliseconds = 0
)
}
)

View File

@@ -1,75 +0,0 @@
package dev.inmo.krontab.internal
typealias Converter<T> = (Int) -> T
internal val intToByteConverter: Converter<Byte> = { it: Int -> it.toByte() }
internal val intToIntConverter: Converter<Int> = { it: Int -> it }
private fun <T> createSimpleScheduler(from: String, dataRange: IntRange, dataConverter: Converter<T>): List<T>? {
val things = from.split(",")
val results = things.flatMap {
val currentToken = it.lowercase().replace(
"f", dataRange.first.toString()
).replace(
"l", dataRange.last.toString()
)
when {
currentToken.contains("-") -> {
val splitted = currentToken.split("-")
(splitted.first().toInt().coerceIn(dataRange) .. splitted[1].toInt().coerceIn(dataRange)).toList()
}
currentToken.contains("/") -> {
val (start, step) = currentToken.split("/")
val startNum = (if (start.isEmpty() || start == "*") {
0
} else {
start.toInt()
}).coerceIn(dataRange)
val stepNum = step.toInt().coerceIn(dataRange)
(startNum .. dataRange.last step stepNum).map { it }
}
currentToken == "*" -> return null
else -> listOf(currentToken.toInt().coerceIn(dataRange))
}
}
return results.map(dataConverter)
}
internal fun parseWeekDay(from: String?) = from ?.let { if (it.endsWith("w")) createSimpleScheduler(it.removeSuffix("w"), dayOfWeekRange, intToByteConverter) ?.toTypedArray() else null }
internal fun parseOffset(from: String?) = from ?.let { if (it.endsWith("o")) it.removeSuffix("o").toIntOrNull() else null }
internal fun parseYears(from: String?) = from ?.let { createSimpleScheduler(from, yearRange, intToIntConverter) ?.toTypedArray() }
internal fun parseMonths(from: String) = createSimpleScheduler(from, monthRange, intToByteConverter) ?.toTypedArray()
internal fun parseDaysOfMonth(from: String) = createSimpleScheduler(from, dayOfMonthRange, intToByteConverter) ?.toTypedArray()
internal fun parseHours(from: String) = createSimpleScheduler(from, hoursRange, intToByteConverter) ?.toTypedArray()
internal fun parseMinutes(from: String) = createSimpleScheduler(from, minutesRange, intToByteConverter) ?.toTypedArray()
internal fun parseSeconds(from: String) = createSimpleScheduler(from, secondsRange, intToByteConverter) ?.toTypedArray()
internal fun <T> Array<T>.fillWith(
whereToPut: MutableList<CronDateTime>,
createFactory: (CronDateTime, T) -> CronDateTime
) {
val previousValues = whereToPut.toList()
whereToPut.clear()
previousValues.forEach { previousValue ->
forEach {
whereToPut.add(createFactory(previousValue, it))
}
}
}
internal fun <T> T.fillWith(
whereToPut: MutableList<CronDateTime>,
createFactory: (CronDateTime, T) -> CronDateTime
) {
val previousValues = whereToPut.toList()
whereToPut.clear()
previousValues.forEach { previousValue ->
whereToPut.add(createFactory(previousValue, this))
}
}

View File

@@ -1,23 +0,0 @@
package dev.inmo.krontab.utils
import com.soywiz.klock.DateTime
import com.soywiz.klock.Month
import kotlin.math.min
fun DateTime.copy(
year: Int = yearInt,
month: Int = month1,
dayOfMonth: Int = this.dayOfMonth,
hour: Int = hours,
minute: Int = minutes,
second: Int = seconds,
milliseconds: Int = this.milliseconds
) = DateTime(
year,
month,
min(Month(month).days(yearInt), dayOfMonth),
hour,
minute,
second,
milliseconds
)

View File

@@ -1,35 +0,0 @@
package dev.inmo.krontab.utils
import com.soywiz.klock.DateTime
import com.soywiz.klock.DateTimeTz
import dev.inmo.krontab.*
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
/**
* This [Flow] will trigger emitting each near time which will be returned from [this] [KronScheduler] with attention to
* time zones
*
* @see channelFlow
* @see KronSchedulerTz.doInfinityTz
*/
@FlowPreview
fun KronScheduler.asTzFlow(): Flow<DateTimeTz> = channelFlow {
doInfinityTz {
send(it)
}
}
/**
* This method is a map for [asTzFlow] and will works the same but return flow with [DateTime]s
*
* @see channelFlow
* @see KronScheduler.doInfinityLocal
*/
@FlowPreview
fun KronScheduler.asFlow(): Flow<DateTime> = channelFlow {
doInfinityLocal {
send(it)
}
}

View File

@@ -1,3 +0,0 @@
package dev.inmo.krontab.utils
typealias Minutes = Int

View File

@@ -1,4 +1,4 @@
package dev.inmo.krontab.utils
package com.insanusmokrassar.krontab.utils
import kotlinx.coroutines.*

View File

@@ -1,4 +1,4 @@
package dev.inmo.krontab.utils
package com.insanusmokrassar.krontab.utils
import kotlinx.coroutines.CoroutineScope

View File

@@ -1,6 +1,7 @@
package dev.inmo.krontab.utils
package com.insanusmokrassar.krontab.utils
import dev.inmo.krontab.builder.buildSchedule
import com.insanusmokrassar.krontab.builder.buildSchedule
import com.soywiz.klock.DateTime
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.takeWhile

View File

@@ -1,96 +0,0 @@
package dev.inmo.krontab.utils
import com.soywiz.klock.*
import dev.inmo.krontab.KronSchedulerTz
import dev.inmo.krontab.buildSchedule
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.takeWhile
import kotlin.test.Test
import kotlin.test.assertEquals
@ExperimentalCoroutinesApi
@FlowPreview
class StringParseTest {
@Test
fun testThatFlowIsCorrectlyWorkEverySecondBuiltOnString() {
val kronScheduler = buildSchedule("*/1 * * * *")
val flow = kronScheduler.asFlow()
runTest {
val mustBeCollected = 10
var collected = 0
flow.takeWhile {
collected < mustBeCollected
}.collect {
collected++
}
assertEquals(mustBeCollected, collected)
}
}
@Test
fun testThatFlowIsCorrectlyWorkEverySecondWithMuchOfEmittersBuiltOnString() {
val kronScheduler = buildSchedule("*/1 * * * *")
val flow = kronScheduler.asFlow()
runTest {
val testsCount = 10
val failJob = it.createFailJob((testsCount * 2) * 1000L)
val mustBeCollected = 10
val answers = (0 until testsCount).map { _ ->
it.async {
var collected = 0
flow.takeWhile {
collected < mustBeCollected
}.collect {
collected++
}
collected
}
}.awaitAll()
failJob.cancel()
answers.forEach {
assertEquals(mustBeCollected, it)
}
}
}
@Test
fun testThatFlowIsCorrectlyWorkEverySeveralSecondsRangeBuiltOnString() {
val rangesEnds = listOf(0 to 5, 30 to 35)
val kronScheduler = buildSchedule("${rangesEnds.joinToString(",") { "${it.first}-${it.second}" }} * * * *")
val flow = kronScheduler.asFlow()
runTest {
val ranges = rangesEnds.map { it.first .. it.second }.flatten().distinct().toMutableList()
val expectedCollects = ranges.size
var collected = 0
flow.takeWhile { ranges.isNotEmpty() }.collect {
ranges.remove(it.seconds)
collected++
}
assertEquals(expectedCollects, collected)
}
}
@Test
fun testThatTimezoneCorrectlyDeserialized() {
val now = DateTime.now().copy(milliseconds = 0).local
runTest {
for (i in 0 .. 1339) {
val expectedInCurrentOffset = now.toOffset(TimezoneOffset(i.minutes)) + 1.hours
val kronScheduler = buildSchedule(
"${expectedInCurrentOffset.seconds} ${expectedInCurrentOffset.minutes} ${expectedInCurrentOffset.hours} * * ${i}o"
) as KronSchedulerTz
val next = kronScheduler.next(now)
assertEquals(expectedInCurrentOffset.toOffset(now.offset), next)
}
}
}
}

View File

@@ -1,28 +0,0 @@
package dev.inmo.krontab.utils
import com.soywiz.klock.*
import dev.inmo.krontab.builder.buildSchedule
import dev.inmo.krontab.next
import kotlin.test.Test
import kotlin.test.assertEquals
class TimeZoneTest {
@Test
fun testDifferentTimeZonesReturnsDifferentTimes() {
val scheduler = buildSchedule { seconds { every(1) } }
val additionalMilliseconds = 100.milliseconds
val baseDate = DateTime.now().startOfWeek.copy(milliseconds = additionalMilliseconds.millisecondsInt)
runTest {
for (i in 0 until 7) {
val now = baseDate + i.days
for (j in 0 .. 24) {
val nowTz = now.toOffset(j.hours)
val next = scheduler.next(nowTz)!!
assertEquals(
(nowTz + 1.seconds - additionalMilliseconds).utc.unixMillisLong, next.utc.unixMillisLong
)
}
}
}
}
}

View File

@@ -1,38 +0,0 @@
package dev.inmo.krontab.utils
import com.soywiz.klock.*
import dev.inmo.krontab.builder.buildSchedule
import kotlin.math.ceil
import kotlin.test.*
class WeekDaysTest {
@Test
fun testThatWeekDaysSchedulingWorks() {
val startDateTime = DateTime.now().startOfDay
val weekDay = startDateTime.dayOfWeek.index0
val testDays = 400
val scheduler = buildSchedule {
dayOfWeek {
at(weekDay)
}
years {
at(startDateTime.yearInt)
}
}
runTest {
for (day in 0 until testDays) {
val currentDateTime = startDateTime + day.days
val next = scheduler.next(currentDateTime)
val expected = when {
day % 7 == 0 -> currentDateTime
else -> startDateTime + ceil(day.toFloat() / 7).weeks
}
if (expected.yearInt != startDateTime.yearInt) {
assertNull(next)
} else {
assertEquals(expected, next)
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
package dev.inmo.krontab.utils
package com.insanusmokrassar.krontab.utils
import kotlinx.coroutines.*

View File

@@ -1,4 +1,4 @@
package dev.inmo.krontab.utils
package com.insanusmokrassar.krontab.utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking

View File

@@ -1 +0,0 @@
<manifest package="dev.inmo.krontab"/>

View File

@@ -1,147 +0,0 @@
package dev.inmo.krontab
import android.content.Context
import androidx.work.*
import com.soywiz.klock.DateTime
import java.util.concurrent.TimeUnit
/**
* This method will enqueue [OneTimeWorkRequest] with [workName] and [existingWorkPolicy]. Use [setUpRequest] callback
* in case you need some additional actions to do before request will be enqueued
*/
suspend fun <T : KronSchedulerWork> Context.enqueueKronSchedulerWork(
workName: String,
delayMillis: Long,
workClass: Class<T>,
existingWorkPolicy: ExistingWorkPolicy = ExistingWorkPolicy.REPLACE,
setUpRequest: suspend OneTimeWorkRequest.Builder.() -> Unit = {}
) = WorkManager.getInstance(applicationContext).enqueueUniqueWork(
workName,
existingWorkPolicy,
OneTimeWorkRequest.Builder(workClass).apply {
setInitialDelay(delayMillis, TimeUnit.MILLISECONDS)
setUpRequest()
}.build()
)
/**
* This method is shortcut for [enqueueKronSchedulerWork] with reified [T] parameter
*/
suspend inline fun <reified T : KronSchedulerWork> Context.enqueueKronSchedulerWork(
workName: String,
delayMillis: Long,
existingWorkPolicy: ExistingWorkPolicy = ExistingWorkPolicy.REPLACE,
noinline setUpRequest: suspend OneTimeWorkRequest.Builder.() -> Unit = {}
) = enqueueKronSchedulerWork(workName, delayMillis, T::class.java, existingWorkPolicy, setUpRequest)
/**
* This method is shortcut for [enqueueKronSchedulerWork] with [initialScheduler]. It will try to calculate delay by
* itself. In case if [KronScheduler.next] of [initialScheduler] will return null, work WILL NOT be enqueued
*
* @return null in case if [KronScheduler.next] of [initialScheduler] has returned null and work has not been enqueued
*/
suspend fun <T : KronSchedulerWork> Context.enqueueKronSchedulerWork(
workName: String,
initialScheduler: KronScheduler,
workClass: Class<T>,
existingWorkPolicy: ExistingWorkPolicy = ExistingWorkPolicy.REPLACE,
setUpRequest: suspend OneTimeWorkRequest.Builder.() -> Unit = {}
): Operation? {
val now = DateTime.now()
val nextTriggerTime = initialScheduler.next(now)
val delayMillis = nextTriggerTime ?.minus(now) ?.millisecondsLong ?: return null
return enqueueKronSchedulerWork(workName, delayMillis, workClass, existingWorkPolicy, setUpRequest)
}
/**
* This method is shortcut for [enqueueKronSchedulerWork] with reified [T]
*/
suspend inline fun <reified T : KronSchedulerWork> Context.enqueueKronSchedulerWork(
workName: String,
initialScheduler: KronScheduler,
existingWorkPolicy: ExistingWorkPolicy = ExistingWorkPolicy.REPLACE,
noinline setUpRequest: suspend OneTimeWorkRequest.Builder.() -> Unit = {}
) = enqueueKronSchedulerWork(workName, initialScheduler, T::class.java, existingWorkPolicy, setUpRequest)
/**
* Use this class as a super class in case you wish to implement krontab-based enqueuing of works
*
* @see enqueueKronSchedulerWork
* @see KrontabTemplateSchedulerWork
*/
abstract class KronSchedulerWork(
context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(
context,
workerParams
) {
/**
* This variable will be used to reorder new work after that one is happen
*
* @see prolongOnException
* @see prolongOnFailure
* @see prolongOnSuccess
*/
protected abstract val workName: String
/**
* Set this to false in case when this work must not be enqueued after successful complete
*/
protected open val prolongOnSuccess: Boolean = true
/**
* Set this to false in case when this work must not be enqueued after failure complete
*/
protected open val prolongOnFailure
get() = prolongOnSuccess
/**
* Set this to false in case when this work must not be enqueued after exception happen
*/
protected open val prolongOnException = false
/**
* [KronScheduler] of this method will be used to [prolong] this worker
*/
protected abstract suspend fun kronScheduler(): KronScheduler?
/**
* This method is replacement of [doWork]. It is required to wrap work with [prolong]ing and handling of complete
* state
*/
protected abstract suspend fun onWork(): Result
/**
* Override this method in case you have some additional settings for future [OneTimeWorkRequest]
*/
protected open suspend fun OneTimeWorkRequest.Builder.setUpRequest() {}
/**
* This method will [enqueueKronSchedulerWork] using [workName], [kronScheduler] and default
* [ExistingWorkPolicy.REPLACE]. You can call this method in case you want to enqueue work by yourself, but you must
* be sure that you set up to false [prolongOnSuccess], [prolongOnFailure] and [prolongOnException]
*/
protected suspend fun prolong() {
applicationContext.enqueueKronSchedulerWork(
workName,
kronScheduler() ?: return,
this::class.java
) {
setUpRequest()
}
}
override suspend fun doWork(): Result {
val result = try {
onWork()
} catch (e: Throwable) {
if (prolongOnException) {
prolong()
}
throw e
}
when (result) {
is Result.Failure -> if (prolongOnFailure) prolong()
is Result.Success -> if (prolongOnSuccess) prolong()
}
return result
}
}

View File

@@ -1,68 +0,0 @@
package dev.inmo.krontab
import android.content.Context
import androidx.work.*
const val krontabTemplateWorkField = "krontabTemplate"
/**
* Will [enqueueKronSchedulerWork] with [KronScheduler] from [krontabTemplate] and call [setUpRequest] on setting up
* [OneTimeWorkRequest.Builder] with [Data] which will be used to [OneTimeWorkRequest.Builder.setInputData] after
* [setUpRequest] completed
*/
suspend inline fun <reified T : KrontabTemplateSchedulerWork> Context.enqueueKrontabTemplateSchedulerWork(
workName: String,
krontabTemplate: KrontabTemplate,
existingWorkPolicy: ExistingWorkPolicy = ExistingWorkPolicy.REPLACE,
noinline setUpRequest: suspend OneTimeWorkRequest.Builder.(
data: Data
) -> Unit = {}
) = enqueueKronSchedulerWork(workName, krontabTemplate.toKronScheduler(), T::class.java, existingWorkPolicy) {
val data = workDataOf(
krontabTemplateWorkField to krontabTemplate
)
setUpRequest(data)
setInputData(data)
}
/**
* Extend this class in case you wish to base on [KrontabTemplate]. It will automatically handle request of
* [kronScheduler] and put it in [setUpRequest]
*/
abstract class KrontabTemplateSchedulerWork(
context: Context,
workerParams: WorkerParameters
) : KronSchedulerWork(context, workerParams) {
/**
* Will try to get [KrontabTemplate] from [getInputData] by key [krontabTemplateWorkField]
*
* @see setUpRequest
*/
protected val krontabTemplate: KrontabTemplate?
get() = inputData.getString(krontabTemplateWorkField)
/**
* Override this methods instead of old [setUpRequest] in case you wish to set up some work request parameters
*
* @param data This parameter will be used to put data inside of [OneTimeWorkRequest.Builder] after this method
* will be completed
*/
protected open suspend fun OneTimeWorkRequest.Builder.setUpRequest(data: Data) {}
/**
* Will automatically put [krontabTemplate] into work data, call [setUpRequest] with future [Data] object and then
* call [OneTimeWorkRequest.Builder.setInputData] with that [Data] object
*/
override suspend fun OneTimeWorkRequest.Builder.setUpRequest() {
val data = workDataOf(
krontabTemplateWorkField to krontabTemplate,
)
setUpRequest(data)
setInputData(data)
}
/**
* Will return [KronScheduler] in case if [krontabTemplate] was not null
*/
override suspend fun kronScheduler(): KronScheduler? = krontabTemplate ?.toKronScheduler()
}