Compare commits

...

39 Commits
0.5.0 ... 0.6.0

Author SHA1 Message Date
e816ad88bc Update CHANGELOG.md 2021-05-25 15:47:47 +06:00
404ea5e188 Update gradle.properties 2021-05-25 15:21:45 +06:00
63e110dbc8 Update CHANGELOG.md 2021-05-15 01:35:10 +06:00
39323f3cd1 Update gradle.properties 2021-05-15 01:34:46 +06:00
15e3430a55 update dependencies 2021-05-07 13:33:35 +06:00
fde5d91375 start 0.6.0 2021-05-07 12:59:13 +06:00
ba548969e1 Merge pull request #16 from InsanusMokrassar/0.5.2
0.5.2
2021-04-24 18:46:21 +06:00
c6bfc5c8f8 optimize imports before merge 2021-04-24 18:33:59 +06:00
301afb1ee0 updates and fixes 2021-04-24 18:33:37 +06:00
7e58ec0873 fixes in flow according to time zones 2021-04-24 16:58:25 +06:00
db1af7818e Update CHANGELOG.md 2021-04-24 01:53:57 +06:00
635a2d6a44 Update CHANGELOG.md 2021-04-24 01:47:29 +06:00
da88d18d4d add note about offsets 2021-04-22 12:44:22 +06:00
20f12f3f2d update readme to include week days info 2021-04-22 12:32:45 +06:00
86cdda51a1 tests and fixes in week work 2021-04-22 12:24:26 +06:00
bfa546ad1f add support of week days 2021-04-22 11:58:19 +06:00
0c7ceb03ee temporarily add nodejs crutch 2021-04-20 17:24:33 +06:00
b0889d0677 remove redundant changes 2021-04-10 15:38:55 +06:00
3408d58cb7 fixes for Tz 2021-04-10 15:38:55 +06:00
5c81eede9c add CronDateTimeSchedulerTz variant in CollectionKronScheduler 2021-04-10 00:33:05 +06:00
5844859369 rewrite system with offsets 2021-04-10 00:28:08 +06:00
a07027399c add offset parameter in strings 2021-03-30 14:07:39 +06:00
93246cada7 fix of next with DateTimeTz 2021-03-29 22:29:10 +06:00
5311e651f4 add work with time zones 2021-03-29 22:14:49 +06:00
68c49faa1d update kotlin 2021-03-29 21:32:39 +06:00
8ce351ecfe start 0.5.2 2021-03-29 21:30:24 +06:00
b43bbea341 Delete .travis.yml 2021-03-29 21:07:52 +06:00
fff010b890 Merge pull request #13 from InsanusMokrassar/0.5.1
0.5.1
2021-03-13 22:19:34 +06:00
3a0870144b Update publishing_packages.yml 2021-03-13 22:03:38 +06:00
40adf80d6f Update publishing_packages.yml 2021-03-13 21:49:08 +06:00
0d1e382182 Update publishing_packages.yml 2021-03-13 21:34:56 +06:00
a15b4162fb Update publishing_packages.yml 2021-03-13 21:17:42 +06:00
d52435a014 include publishing to packages 2021-03-13 20:31:50 +06:00
9ce69efad5 update publishing scripts 2021-03-13 20:30:02 +06:00
092fc6bc9b update gradle wrapper 2021-03-13 20:23:15 +06:00
ec85fd77f9 update dependencies 2021-03-13 20:22:55 +06:00
496d4cf100 start 0.5.1 2021-03-13 20:19:03 +06:00
dce0fd87a1 Update CHANGELOG.md 2021-01-03 20:01:25 +06:00
fd842b17fa Merge pull request #12 from InsanusMokrassar/0.5.0
0.5.0
2021-01-03 13:32:04 +06:00
28 changed files with 546 additions and 123 deletions

View File

@@ -0,0 +1,22 @@
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: 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
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signMetadataPublication -x signAndroidDebugPublication -x signAndroidReleasePublication
env:
GITHUBPACKAGES_USER: ${{ github.actor }}
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,10 +0,0 @@
language: java
install: true
os: linux
dist: trusty
jdk: oraclejdk8
script:
- ./gradlew build allTests -s

View File

@@ -1,5 +1,32 @@
# Changelog
## 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**
@@ -16,7 +43,7 @@
* `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
* **Fixed issue related to the fact that `toNearDateTime` of `CronDateTime` incorrectly handled months**
* **Android target has been added**
## 0.4.2

View File

@@ -14,6 +14,8 @@ runtime of applications.
| [ 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: Offsets ](#Offsets) |
| [ How to use: Note about week days ](#Note-about-week-days) |
## How to use
@@ -52,14 +54,17 @@ 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
* * * * * *
/--------------- Seconds
| /------------- Minutes
| | /----------- Hours
| | | /--------- Days of months
| | | | /------- Months
| | | | | /----- (optional) Year
| | | | | | /--- (optional) Timezone offset
| | | | | | | / (optional) Week days
* * * * * * 0o *w
```
It is different with original `crontab` syntax for the reason, that expected that in practice developers
@@ -152,3 +157,27 @@ 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 works 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

@@ -10,7 +10,7 @@ buildscript {
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.0.2'
classpath 'com.android.tools.build:gradle:4.1.3'
}
}
@@ -18,6 +18,18 @@ plugins {
id "org.jetbrains.kotlin.multiplatform" version "$kotlin_version"
id "org.jetbrains.dokka" version "$dokka_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"
@@ -113,10 +125,6 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
test {

View File

@@ -8,12 +8,12 @@ android.useAndroidX=true
android.enableJetifier=false
kotlin_version=1.4.21
kotlin_coroutines_version=1.4.2
kotlin_version=1.5.10
kotlin_coroutines_version=1.5.0
dokka_version=1.4.20
dokka_version=1.4.32
klockVersion=2.0.3
klockVersion=2.1.0
## Github reease
@@ -23,16 +23,16 @@ github_release_plugin_version=2.2.12
android_minSdkVersion=19
android_compileSdkVersion=30
android_buildToolsVersion=30.0.2
android_buildToolsVersion=30.0.3
dexcount_version=2.0.0
junit_version=4.12
test_ext_junit_version=1.1.2
espresso_core=3.3.0
androidx_work_version=2.4.0
androidx_work_version=2.5.0
## Common
version=0.5.0
android_code_version=1
version=0.6.0
android_code_version=4

View File

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip

View File

@@ -1 +1 @@
{"bintrayConfig":{"repo":"InsanusMokrassar","packageName":"${project.name}-mpp","packageVcs":"https://git.insanusmokrassar.com/InsanusMokrassar/krontab","autoPublish":true,"overridePublish":true},"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:8322/InsanusMokrassar/krontab.git","developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}]}}
{"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,24 +1,9 @@
apply plugin: 'maven-publish'
apply plugin: 'signing'
task javadocsJar(type: Jar) {
classifier = 'javadoc'
}
task sourceJar (type : Jar) {
classifier = 'sources'
}
afterEvaluate {
project.publishing.publications.all {
// rename artifacts
groupId "${project.group}"
if (it.name.contains('kotlinMultiplatform')) {
artifactId = "${project.name}"
artifact sourceJar
} else {
artifactId = "${project.name}-$name"
}
}
}
publishing {
publications.all {
@@ -27,11 +12,11 @@ publishing {
pom {
description = "It is an analog of crontab util for Kotlin Coroutines"
name = "Krontab"
url = "https://git.insanusmokrassar.com/InsanusMokrassar/krontab"
url = "https://github.com/InsanusMokrassar/krontab"
scm {
developerConnection = "scm:git:[fetch=]https://git.insanusmokrassar.com:8322/InsanusMokrassar/krontab.git[push=]https://git.insanusmokrassar.com:8322/InsanusMokrassar/krontab.git"
url = "https://git.insanusmokrassar.com:8322/InsanusMokrassar/krontab.git"
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 {
@@ -48,22 +33,37 @@ publishing {
license {
name = "Apache Software License 2.0"
url = "https://git.insanusmokrassar.com/InsanusMokrassar/krontab/src/master/LICENSE"
url = "https://github.com/InsanusMokrassar/krontab/blob/master/LICENSE"
}
}
}
repositories {
maven {
name = "bintray"
url = uri("https://api.bintray.com/maven/${project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')}/InsanusMokrassar/${project.name}-mpp/;publish=1;override=1")
credentials {
username = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')
password = project.hasProperty('BINTRAY_KEY') ? project.property('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
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')
}
}
}
}
}
}
}
signing {
useGpgCmd()
sign publishing.publications
}

View File

@@ -1,7 +1,6 @@
package dev.inmo.krontab
import com.soywiz.klock.DateTime
import dev.inmo.krontab.internal.toNearDateTime
/**
* This interface was created for abstraction of [next] operation. Currently, there is only

View File

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

View File

@@ -3,8 +3,6 @@ package dev.inmo.krontab
import com.soywiz.klock.DateTime
import dev.inmo.krontab.builder.buildSchedule
import dev.inmo.krontab.internal.*
import dev.inmo.krontab.internal.CronDateTime
import dev.inmo.krontab.internal.CronDateTimeScheduler
internal val anyCronDateTime by lazy {
CronDateTime()

View File

@@ -1,6 +1,9 @@
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
@@ -17,8 +20,10 @@ typealias KrontabTemplate = String
* * 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 have next format:
* And each one (except of offsets) have next format:
*
* `{number}[,{number},...]` or `*`
*
@@ -31,6 +36,9 @@ typealias KrontabTemplate = String
* * 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]
@@ -39,6 +47,7 @@ typealias KrontabTemplate = String
* * 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:
*
@@ -46,14 +55,41 @@ typealias KrontabTemplate = String
* * "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 {
val yearSource: String?
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 {
yearSource = it.getOrNull(5)
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)
@@ -61,24 +97,54 @@ fun createSimpleScheduler(incoming: KrontabTemplate): KronScheduler {
val hoursParsed = parseHours(hoursSource)
val dayOfMonthParsed = parseDaysOfMonth(dayOfMonthSource)
val monthParsed = parseMonths(monthSource)
val yearParsed = parseYears(yearSource)
return createKronScheduler(
secondsParsed, minutesParsed, hoursParsed, dayOfMonthParsed, monthParsed, yearParsed
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).cronDateTimes,
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)
fun KrontabTemplate.toKronScheduler(): KronScheduler = buildSchedule(this)
/**
* Shortcut for [buildSchedule]
*/
fun KrontabTemplate.toKronScheduler(defaultOffset: Minutes): KronSchedulerTz = buildSchedule(this, defaultOffset)

View File

@@ -1,7 +1,12 @@
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]
@@ -16,13 +21,31 @@ fun buildSchedule(settingsBlock: SchedulerBuilder.() -> Unit): KronScheduler {
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 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>?,
@@ -84,6 +107,17 @@ class SchedulerBuilder(
) ?.toTypedArray()
}
/**
* Starts an hours block
*/
fun dayOfWeek(block: WeekDaysBuilder.() -> Unit) {
dayOfWeek = callAndReturn(
dayOfWeek,
WeekDaysBuilder(),
block
) ?.toTypedArray()
}
/**
* Starts an months block
*/
@@ -112,5 +146,7 @@ class SchedulerBuilder(
* @see dev.inmo.krontab.createSimpleScheduler
* @see dev.inmo.krontab.internal.createKronScheduler
*/
fun build(): KronScheduler = createKronScheduler(seconds, minutes, hours, dayOfMonth, month, year)
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,11 +1,10 @@
package dev.inmo.krontab.builder
import dev.inmo.krontab.internal.*
import dev.inmo.krontab.utils.clamp
/**
* This class was created for incapsulation of builder work with specified [restrictionsRange]. For example,
* [include] function of [TimeBuilder] will always [clamp] incoming data using its [restrictionsRange]
* [include] function of [TimeBuilder] will always [coerceIn] incoming data using its [restrictionsRange]
*/
sealed class TimeBuilder<T : Number> (
private val restrictionsRange: IntRange,
@@ -37,7 +36,7 @@ sealed class TimeBuilder<T : Number> (
*/
@Suppress("MemberVisibilityCanBePrivate")
infix fun include(array: Array<Int>) {
val clamped = array.map { it.clamp(restrictionsRange) } + (result ?: emptySet())
val clamped = array.map { it.coerceIn(restrictionsRange) } + (result ?: emptySet())
result = clamped.toSet()
}
@@ -46,7 +45,7 @@ sealed class TimeBuilder<T : Number> (
*/
@Suppress("unused")
infix fun at(value: Int) {
result = (result ?: emptySet()) + value.clamp(restrictionsRange)
result = (result ?: emptySet()) + value.coerceIn(restrictionsRange)
}
@@ -70,7 +69,7 @@ sealed class TimeBuilder<T : Number> (
* @see [from]
*/
infix fun Int.every(delay: Int): Array<Int> {
val progression = clamp(restrictionsRange) .. restrictionsRange.last step delay
val progression = coerceIn(restrictionsRange) .. restrictionsRange.last step delay
val result = progression.toSet().toTypedArray()
this@TimeBuilder include result
@@ -88,7 +87,7 @@ sealed class TimeBuilder<T : Number> (
*/
@Suppress("MemberVisibilityCanBePrivate")
infix fun Int.upTo(endIncluding: Int): Array<Int> {
val progression = clamp(restrictionsRange) .. endIncluding.clamp(restrictionsRange)
val progression = coerceIn(restrictionsRange) .. endIncluding.coerceIn(restrictionsRange)
val result = progression.toSet().toTypedArray()
this@TimeBuilder include result
@@ -129,3 +128,4 @@ 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,17 +1,16 @@
package dev.inmo.krontab.collection
import com.soywiz.klock.DateTime
import com.soywiz.klock.DateTimeTz
import dev.inmo.krontab.*
import dev.inmo.krontab.internal.*
import dev.inmo.krontab.internal.CronDateTimeScheduler
import dev.inmo.krontab.internal.toNearDateTime
/**
* 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>
) : KronScheduler {
) : KronSchedulerTz {
internal constructor() : this(mutableListOf())
/**
@@ -38,6 +37,18 @@ data class CollectionKronScheduler internal constructor(
mergeCronDateTimeSchedulers(resultCronDateTimes)
)
}
is CronDateTimeSchedulerTz -> {
val newCronDateTimes = kronScheduler.cronDateTimes.toMutableList()
val cronDateTimes = schedulers.removeAll {
if (it is CronDateTimeSchedulerTz && it.offset == kronScheduler.offset) {
newCronDateTimes.addAll(it.cronDateTimes)
true
} else {
false
}
}
schedulers.add(CronDateTimeSchedulerTz(newCronDateTimes.toList(), kronScheduler.offset))
}
is CollectionKronScheduler -> kronScheduler.schedulers.forEach {
include(it)
}
@@ -48,4 +59,8 @@ data class CollectionKronScheduler internal constructor(
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,10 +1,11 @@
package dev.inmo.krontab.internal
import com.soywiz.klock.DateTime
import com.soywiz.klock.DateTimeSpan
import com.soywiz.klock.*
import dev.inmo.krontab.KronScheduler
/**
* @param dayOfweek 0-6
* @param year any int
* @param month 0-11
* @param dayOfMonth 0-31
* @param hours 0-23
@@ -12,6 +13,7 @@ import dev.inmo.krontab.KronScheduler
* @param seconds 0-59
*/
internal data class CronDateTime(
val dayOfweek: Byte? = null,
val year: Int? = null,
val month: Byte? = null,
val dayOfMonth: Byte? = null,
@@ -20,6 +22,7 @@ internal data class CronDateTime(
val seconds: Byte? = null
) {
init {
check(dayOfweek ?.let { it in dayOfWeekRange } ?: true)
check(year ?.let { it in yearRange } ?: true)
check(month ?.let { it in monthRange } ?: true)
check(dayOfMonth ?.let { it in dayOfMonthRange } ?: true)
@@ -29,14 +32,34 @@ internal data class CronDateTime(
}
internal val klockDayOfMonth = dayOfMonth ?.plus(1)
internal val dayOfWeekInt: Int? = dayOfweek ?.toInt()
}
/**
* THIS METHOD WILL <b>NOT</b> TAKE CARE ABOUT [offset] PARAMETER. It was decided due to the fact that we unable to get
* real timezone offset from simple [DateTime]
*
* @return The near [DateTime] which happens after [relativelyTo] or will be equal to [relativelyTo]
*/
internal fun CronDateTime.toNearDateTime(relativelyTo: DateTime = DateTime.now()): DateTime? {
var current = relativelyTo
val weekDay = dayOfWeekInt
if (weekDay != null && current.dayOfWeek.index0 != weekDay) {
do {
var diff = weekDay - current.dayOfWeek.index0
if (diff < 0) {
diff += 7 /* days in week */
}
current = (current + diff.days).startOfDay
val next = toNearDateTime(current)
if (next == null || next.dayOfWeek.index0 == weekDay) {
return next
}
} while (true)
}
seconds?.let {
val left = it - current.seconds
current += DateTimeSpan(minutes = if (left <= 0) 1 else 0, seconds = left)
@@ -77,17 +100,15 @@ internal fun CronDateTime.toNearDateTime(relativelyTo: DateTime = DateTime.now()
return current
}
/**
* @return [KronScheduler] (in fact [CronDateTimeScheduler]) based on incoming data
*/
internal fun createKronScheduler(
internal fun createCronDateTimeList(
seconds: Array<Byte>? = null,
minutes: Array<Byte>? = null,
hours: Array<Byte>? = null,
dayOfMonth: Array<Byte>? = null,
month: Array<Byte>? = null,
years: Array<Int>? = null
): KronScheduler {
years: Array<Int>? = null,
weekDays: Array<Byte>? = null
): List<CronDateTime> {
val resultCronDateTimes = mutableListOf(CronDateTime())
seconds ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
@@ -114,5 +135,35 @@ internal fun createKronScheduler(
previousCronDateTime.copy(year = currentTime)
}
return CronDateTimeScheduler(resultCronDateTimes.toList())
weekDays ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(dayOfweek = currentTime)
}
return resultCronDateTimes.toList()
}
/**
* @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(createCronDateTimeList(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(createCronDateTimeList(seconds, minutes, hours, dayOfMonth, month, years, weekDays), offset)

View File

@@ -1,7 +1,7 @@
package dev.inmo.krontab.internal
import com.soywiz.klock.DateTime
import dev.inmo.krontab.*
import dev.inmo.krontab.KronScheduler
import dev.inmo.krontab.collection.plus
/**
@@ -26,8 +26,8 @@ internal data class CronDateTimeScheduler internal constructor(
*
* @see toNearDateTime
*/
override suspend fun next(relatively: DateTime): DateTime {
return cronDateTimes.mapNotNull { it.toNearDateTime(relatively) }.minOrNull() ?: getAnyNext(relatively)
override suspend fun next(relatively: DateTime): DateTime? {
return cronDateTimes.mapNotNull { it.toNearDateTime(relatively) }.minOrNull()
}
}

View File

@@ -0,0 +1,31 @@
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 cronDateTimes: List<CronDateTime>,
internal val offset: TimezoneOffset
) : KronSchedulerTz {
override suspend fun next(relatively: DateTimeTz): DateTimeTz? {
val dateTimeWithActualOffset = relatively.toOffset(offset).local
return cronDateTimes.mapNotNull {
it.toNearDateTime(dateTimeWithActualOffset)
}.minOrNull() ?.toOffsetUnadjusted(offset) ?.toOffset(relatively.offset)
}
}
internal fun mergeCronDateTimeSchedulers(
schedulers: List<CronDateTimeSchedulerTz>
) = schedulers.groupBy {
it.offset
}.map { (offset, schedulers) ->
CronDateTimeSchedulerTz(schedulers.flatMap { it.cronDateTimes }, offset)
}

View File

@@ -1,7 +1,5 @@
package dev.inmo.krontab.internal
import dev.inmo.krontab.utils.clamp
typealias Converter<T> = (Int) -> T
internal val intToByteConverter: Converter<Byte> = { it: Int -> it.toByte() }
@@ -18,7 +16,7 @@ private fun <T> createSimpleScheduler(from: String, dataRange: IntRange, dataCon
when {
currentToken.contains("-") -> {
val splitted = currentToken.split("-")
(splitted.first().toInt().clamp(dataRange) .. splitted[1].toInt().clamp(dataRange)).toList()
(splitted.first().toInt().coerceIn(dataRange) .. splitted[1].toInt().coerceIn(dataRange)).toList()
}
currentToken.contains("/") -> {
val (start, step) = currentToken.split("/")
@@ -26,18 +24,20 @@ private fun <T> createSimpleScheduler(from: String, dataRange: IntRange, dataCon
0
} else {
start.toInt()
}).clamp(dataRange)
val stepNum = step.toInt().clamp(dataRange)
}).coerceIn(dataRange)
val stepNum = step.toInt().coerceIn(dataRange)
(startNum .. dataRange.last step stepNum).map { it }
}
currentToken == "*" -> return null
else -> listOf(currentToken.toInt().clamp(dataRange))
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()
@@ -60,3 +60,16 @@ internal fun <T> Array<T>.fillWith(
}
}
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,5 +1,6 @@
package dev.inmo.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

View File

@@ -1,12 +0,0 @@
package dev.inmo.krontab.utils
/**
* @return [min] in case if [this] less than [min]. Otherwise will check that [max] grant than [this] and return [this]
* if so or [max] otherwise
*/
internal fun Int.clamp(min: Int, max: Int): Int = if (this < min) min else if (this > max) max else this
/**
* Wrapper function for [clamp] extension
*/
internal fun Int.clamp(range: IntRange): Int = clamp(range.first, range.last)

View File

@@ -1,14 +1,39 @@
package dev.inmo.krontab.utils
import com.soywiz.klock.DateTime
import com.soywiz.klock.DateTimeTz
import dev.inmo.krontab.KronScheduler
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import dev.inmo.krontab.next
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
/**
* This [Flow] will trigger emitting each near time which will be returned from [this] [KronScheduler] with attention to
* time zones
*
* @see channelFlow
*/
@FlowPreview
fun KronScheduler.asFlow(): Flow<DateTime> = SchedulerFlow(this)
fun KronScheduler.asTzFlow(): Flow<DateTimeTz> = channelFlow {
while (isActive) {
val now = DateTime.now().local
val nextTime = next(now) ?: break
val sleepDelay = (nextTime - DateTime.now().local).millisecondsLong
delay(sleepDelay)
send(nextTime)
}
}
/**
* This method is a map for [asTzFlow] and will works the same but return flow with [DateTime]s
*/
@FlowPreview
fun KronScheduler.asFlow(): Flow<DateTime> = asTzFlow().map { it.local }
@Deprecated(
"It is not recommended to use this class in future. This functionality will be removed soon",
ReplaceWith("asFlow", "dev.inmo.krontab.utils.asFlow")
)
@FlowPreview
class SchedulerFlow(
private val scheduler: KronScheduler

View File

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

View File

@@ -1,5 +1,7 @@
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
@@ -66,7 +68,7 @@ class StringParseTest {
runTest {
val ranges = rangesEnds.map { it.first .. it.second }.flatten().toMutableList()
val expectedCollects = rangesEnds.sumBy { it.second - it.first + 1 }
val expectedCollects = rangesEnds.sumOf { it.second - it.first + 1 }
var collected = 0
flow.takeWhile { ranges.isNotEmpty() }.collect {
@@ -76,4 +78,19 @@ class StringParseTest {
assertEquals(expectedCollects, collected)
}
}
@Test
fun testThatTimezoneCorrectlyDeserialized() {
val now = DateTimeTz.nowLocal()
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

@@ -0,0 +1,27 @@
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 baseDate = DateTime.now().startOfWeek
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).utc.unixMillisLong, next.utc.unixMillisLong
)
}
}
}
}
}

View File

@@ -0,0 +1,38 @@
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)
}
}
}
}
}