Merge pull request #16 from InsanusMokrassar/0.5.2

0.5.2
This commit is contained in:
InsanusMokrassar 2021-04-24 18:46:21 +06:00 committed by GitHub
commit ba548969e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 471 additions and 71 deletions

View File

@ -1,5 +1,17 @@
# Changelog # Changelog
## 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 ## 0.5.1
* Versions * Versions

View File

@ -14,6 +14,8 @@ runtime of applications.
| [ How to use: Config from string ](#config-from-string) | | [ How to use: Config from string ](#config-from-string) |
| [ How to use: Config via builder (DSL preview) ](#config-via-builder) | | [ 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: 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 ## 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 Developers can use more simple way to configure repeat times is string. String configuring
like a `crontab`, but with a little bit different meanings: like a `crontab`, but with a little bit different meanings:
``` ```
/---------- Seconds /--------------- Seconds
| /-------- Minutes | /------------- Minutes
| | /------ Hours | | /----------- Hours
| | | /---- Days of months | | | /--------- Days of months
| | | | /-- Months | | | | /------- Months
| | | | | / (optional) Year | | | | | /----- (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 It is different with original `crontab` syntax for the reason, that expected that in practice developers
@ -152,3 +157,27 @@ flow.takeWhile {
action() 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

@ -18,6 +18,18 @@ plugins {
id "org.jetbrains.kotlin.multiplatform" version "$kotlin_version" id "org.jetbrains.kotlin.multiplatform" version "$kotlin_version"
id "org.jetbrains.dokka" version "$dokka_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" apply plugin: "com.android.library"
project.version = "$version" project.version = "$version"

View File

@ -8,10 +8,10 @@ android.useAndroidX=true
android.enableJetifier=false android.enableJetifier=false
kotlin_version=1.4.31 kotlin_version=1.4.32
kotlin_coroutines_version=1.4.3 kotlin_coroutines_version=1.4.3
dokka_version=1.4.20 dokka_version=1.4.30
klockVersion=2.0.7 klockVersion=2.0.7
@ -33,6 +33,6 @@ androidx_work_version=2.5.0
## Common ## Common
version=0.5.1 version=0.5.2
android_code_version=2 android_code_version=3

View File

@ -1,7 +1,6 @@
package dev.inmo.krontab package dev.inmo.krontab
import com.soywiz.klock.DateTime import com.soywiz.klock.DateTime
import dev.inmo.krontab.internal.toNearDateTime
/** /**
* This interface was created for abstraction of [next] operation. Currently, there is only * 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 package dev.inmo.krontab
import dev.inmo.krontab.collection.CollectionKronScheduler import dev.inmo.krontab.collection.CollectionKronScheduler
import dev.inmo.krontab.internal.CronDateTime import dev.inmo.krontab.collection.includeAll
import dev.inmo.krontab.internal.CronDateTimeScheduler import dev.inmo.krontab.internal.*
/** /**
* Create new one [CollectionKronScheduler] to include all [KronScheduler]s of [this] [Iterator] * 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
* @see CollectionKronScheduler.include * @see CollectionKronScheduler.include
*/ */
fun Iterator<KronScheduler>.merge(): KronScheduler { fun Iterator<KronScheduler>.merge(): CollectionKronScheduler {
val cronDateTimes = mutableListOf<CronDateTime>() val cronDateTimes = mutableListOf<CronDateTime>()
val timezonedCronDateTimes = mutableListOf<CronDateTimeSchedulerTz>()
val collectionScheduler = CollectionKronScheduler() val collectionScheduler = CollectionKronScheduler()
forEach { forEach {
when (it) { when (it) {
is CronDateTimeScheduler -> cronDateTimes.addAll(it.cronDateTimes) is CronDateTimeScheduler -> cronDateTimes.addAll(it.cronDateTimes)
is CronDateTimeSchedulerTz -> timezonedCronDateTimes.add(it)
else -> collectionScheduler.include(it) else -> collectionScheduler.include(it)
} }
} }
if (cronDateTimes.isNotEmpty()) { if (cronDateTimes.isNotEmpty()) {
collectionScheduler.include(CronDateTimeScheduler(cronDateTimes)) collectionScheduler.include(CronDateTimeScheduler(cronDateTimes))
} }
if (timezonedCronDateTimes.isNotEmpty()) {
collectionScheduler.includeAll(mergeCronDateTimeSchedulers(timezonedCronDateTimes))
}
return collectionScheduler return collectionScheduler
} }
@ -32,10 +37,10 @@ fun Iterator<KronScheduler>.merge(): KronScheduler {
* @see CollectionKronScheduler.include * @see CollectionKronScheduler.include
*/ */
@Suppress("NOTHING_TO_INLINE") @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] * @return Vararg shortcut for [merge]
*/ */
@Suppress("NOTHING_TO_INLINE") @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 com.soywiz.klock.DateTime
import dev.inmo.krontab.builder.buildSchedule import dev.inmo.krontab.builder.buildSchedule
import dev.inmo.krontab.internal.* import dev.inmo.krontab.internal.*
import dev.inmo.krontab.internal.CronDateTime
import dev.inmo.krontab.internal.CronDateTimeScheduler
internal val anyCronDateTime by lazy { internal val anyCronDateTime by lazy {
CronDateTime() CronDateTime()

View File

@ -1,6 +1,9 @@
package dev.inmo.krontab package dev.inmo.krontab
import com.soywiz.klock.TimezoneOffset
import com.soywiz.klock.minutes
import dev.inmo.krontab.internal.* import dev.inmo.krontab.internal.*
import dev.inmo.krontab.utils.Minutes
/** /**
* @see createSimpleScheduler * @see createSimpleScheduler
@ -17,8 +20,10 @@ typealias KrontabTemplate = String
* * dayOfMonth * * dayOfMonth
* * month * * month
* * (optional) year * * (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 `*` * `{number}[,{number},...]` or `*`
* *
@ -31,6 +36,9 @@ typealias KrontabTemplate = String
* * F * * F
* * L * * 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: * Additional info about ranges can be found in follow accordance:
* *
* * Seconds ranges can be found in [secondsRange] * * Seconds ranges can be found in [secondsRange]
@ -39,6 +47,7 @@ typealias KrontabTemplate = String
* * Days of month ranges can be found in [dayOfMonthRange] * * Days of month ranges can be found in [dayOfMonthRange]
* * Months ranges can be found in [monthRange] * * Months ranges can be found in [monthRange]
* * Years ranges can be found in [yearRange] (in fact - any [Int]) * * Years ranges can be found in [yearRange] (in fact - any [Int])
* * WeekDay (timezone) ranges can be found in [dayOfWeekRange]
* *
* Examples: * Examples:
* *
@ -46,14 +55,41 @@ typealias KrontabTemplate = String
* * "0/5,L * * * *" for every five seconds triggering and on 59 second * * "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 * * "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" 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" 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 * @see dev.inmo.krontab.internal.createKronScheduler
*/ */
fun createSimpleScheduler(incoming: KrontabTemplate): KronScheduler { fun createSimpleScheduler(
val yearSource: String? 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 { 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) val secondsParsed = parseSeconds(secondsSource)
@ -61,24 +97,54 @@ fun createSimpleScheduler(incoming: KrontabTemplate): KronScheduler {
val hoursParsed = parseHours(hoursSource) val hoursParsed = parseHours(hoursSource)
val dayOfMonthParsed = parseDaysOfMonth(dayOfMonthSource) val dayOfMonthParsed = parseDaysOfMonth(dayOfMonthSource)
val monthParsed = parseMonths(monthSource) val monthParsed = parseMonths(monthSource)
val yearParsed = parseYears(yearSource)
return createKronScheduler( return offsetParsed ?.let { offset ->
secondsParsed, minutesParsed, hoursParsed, dayOfMonthParsed, monthParsed, yearParsed 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] * Shortcut for [createSimpleScheduler]
*/ */
fun buildSchedule(incoming: KrontabTemplate): KronScheduler = createSimpleScheduler(incoming) fun buildSchedule(incoming: KrontabTemplate): KronScheduler = createSimpleScheduler(incoming)
/**
* Shortcut for [createSimpleScheduler]
*/
fun buildSchedule(incoming: KrontabTemplate, defaultOffset: Minutes): KronSchedulerTz = createSimpleScheduler(incoming, defaultOffset)
/** /**
* Shortcut for [buildSchedule] * Shortcut for [buildSchedule]
*/ */
fun KrontabTemplate.toSchedule(): KronScheduler = buildSchedule(this) fun KrontabTemplate.toSchedule(): KronScheduler = buildSchedule(this)
/**
* Shortcut for [buildSchedule]
*/
fun KrontabTemplate.toSchedule(defaultOffset: Minutes): KronSchedulerTz = buildSchedule(this, defaultOffset)
/** /**
* Shortcut for [buildSchedule] * 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 package dev.inmo.krontab.builder
import com.soywiz.klock.TimezoneOffset
import com.soywiz.klock.minutes
import dev.inmo.krontab.KronScheduler import dev.inmo.krontab.KronScheduler
import dev.inmo.krontab.KronSchedulerTz
import dev.inmo.krontab.internal.createKronScheduler 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] * Will help to create an instance of [KronScheduler]
@ -16,13 +21,31 @@ fun buildSchedule(settingsBlock: SchedulerBuilder.() -> Unit): KronScheduler {
return builder.build() 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( class SchedulerBuilder(
private var seconds: Array<Byte>? = null, private var seconds: Array<Byte>? = null,
private var minutes: Array<Byte>? = null, private var minutes: Array<Byte>? = null,
private var hours: Array<Byte>? = null, private var hours: Array<Byte>? = null,
private var dayOfMonth: Array<Byte>? = null, private var dayOfMonth: Array<Byte>? = null,
private var month: 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( private fun <I, T : TimeBuilder<I>> callAndReturn(
initial: Array<I>?, initial: Array<I>?,
@ -84,6 +107,17 @@ class SchedulerBuilder(
) ?.toTypedArray() ) ?.toTypedArray()
} }
/**
* Starts an hours block
*/
fun dayOfWeek(block: WeekDaysBuilder.() -> Unit) {
dayOfWeek = callAndReturn(
dayOfWeek,
WeekDaysBuilder(),
block
) ?.toTypedArray()
}
/** /**
* Starts an months block * Starts an months block
*/ */
@ -112,5 +146,7 @@ class SchedulerBuilder(
* @see dev.inmo.krontab.createSimpleScheduler * @see dev.inmo.krontab.createSimpleScheduler
* @see dev.inmo.krontab.internal.createKronScheduler * @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 package dev.inmo.krontab.builder
import dev.inmo.krontab.internal.* 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, * 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> ( sealed class TimeBuilder<T : Number> (
private val restrictionsRange: IntRange, private val restrictionsRange: IntRange,
@ -37,7 +36,7 @@ sealed class TimeBuilder<T : Number> (
*/ */
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
infix fun include(array: Array<Int>) { 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() result = clamped.toSet()
} }
@ -46,7 +45,7 @@ sealed class TimeBuilder<T : Number> (
*/ */
@Suppress("unused") @Suppress("unused")
infix fun at(value: Int) { 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] * @see [from]
*/ */
infix fun Int.every(delay: Int): Array<Int> { 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() val result = progression.toSet().toTypedArray()
this@TimeBuilder include result this@TimeBuilder include result
@ -88,7 +87,7 @@ sealed class TimeBuilder<T : Number> (
*/ */
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
infix fun Int.upTo(endIncluding: Int): Array<Int> { 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() val result = progression.toSet().toTypedArray()
this@TimeBuilder include result this@TimeBuilder include result
@ -129,3 +128,4 @@ class HoursBuilder : TimeBuilder<Byte>(hoursRange, intToByteConverter)
class DaysOfMonthBuilder : TimeBuilder<Byte>(dayOfMonthRange, intToByteConverter) class DaysOfMonthBuilder : TimeBuilder<Byte>(dayOfMonthRange, intToByteConverter)
class MonthsBuilder : TimeBuilder<Byte>(monthRange, intToByteConverter) class MonthsBuilder : TimeBuilder<Byte>(monthRange, intToByteConverter)
class YearsBuilder : TimeBuilder<Int>(yearRange, intToIntConverter) class YearsBuilder : TimeBuilder<Int>(yearRange, intToIntConverter)
class WeekDaysBuilder : TimeBuilder<Byte>(dayOfWeekRange, intToByteConverter)

View File

@ -1,17 +1,16 @@
package dev.inmo.krontab.collection package dev.inmo.krontab.collection
import com.soywiz.klock.DateTime import com.soywiz.klock.DateTime
import com.soywiz.klock.DateTimeTz
import dev.inmo.krontab.* import dev.inmo.krontab.*
import dev.inmo.krontab.internal.* 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 * This scheduler will be useful in case you want to unite several different [KronScheduler]s
*/ */
data class CollectionKronScheduler internal constructor( data class CollectionKronScheduler internal constructor(
internal val schedulers: MutableList<KronScheduler> internal val schedulers: MutableList<KronScheduler>
) : KronScheduler { ) : KronSchedulerTz {
internal constructor() : this(mutableListOf()) internal constructor() : this(mutableListOf())
/** /**
@ -38,6 +37,18 @@ data class CollectionKronScheduler internal constructor(
mergeCronDateTimeSchedulers(resultCronDateTimes) 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 { is CollectionKronScheduler -> kronScheduler.schedulers.forEach {
include(it) include(it)
} }
@ -48,4 +59,8 @@ data class CollectionKronScheduler internal constructor(
override suspend fun next(relatively: DateTime): DateTime { override suspend fun next(relatively: DateTime): DateTime {
return schedulers.mapNotNull { it.next(relatively) }.minOrNull() ?: getAnyNext(relatively) 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 package dev.inmo.krontab.internal
import com.soywiz.klock.DateTime import com.soywiz.klock.*
import com.soywiz.klock.DateTimeSpan
import dev.inmo.krontab.KronScheduler import dev.inmo.krontab.KronScheduler
/** /**
* @param dayOfweek 0-6
* @param year any int
* @param month 0-11 * @param month 0-11
* @param dayOfMonth 0-31 * @param dayOfMonth 0-31
* @param hours 0-23 * @param hours 0-23
@ -12,6 +13,7 @@ import dev.inmo.krontab.KronScheduler
* @param seconds 0-59 * @param seconds 0-59
*/ */
internal data class CronDateTime( internal data class CronDateTime(
val dayOfweek: Byte? = null,
val year: Int? = null, val year: Int? = null,
val month: Byte? = null, val month: Byte? = null,
val dayOfMonth: Byte? = null, val dayOfMonth: Byte? = null,
@ -20,6 +22,7 @@ internal data class CronDateTime(
val seconds: Byte? = null val seconds: Byte? = null
) { ) {
init { init {
check(dayOfweek ?.let { it in dayOfWeekRange } ?: true)
check(year ?.let { it in yearRange } ?: true) check(year ?.let { it in yearRange } ?: true)
check(month ?.let { it in monthRange } ?: true) check(month ?.let { it in monthRange } ?: true)
check(dayOfMonth ?.let { it in dayOfMonthRange } ?: true) check(dayOfMonth ?.let { it in dayOfMonthRange } ?: true)
@ -29,14 +32,34 @@ internal data class CronDateTime(
} }
internal val klockDayOfMonth = dayOfMonth ?.plus(1) 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] * @return The near [DateTime] which happens after [relativelyTo] or will be equal to [relativelyTo]
*/ */
internal fun CronDateTime.toNearDateTime(relativelyTo: DateTime = DateTime.now()): DateTime? { internal fun CronDateTime.toNearDateTime(relativelyTo: DateTime = DateTime.now()): DateTime? {
var current = relativelyTo 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 { seconds?.let {
val left = it - current.seconds val left = it - current.seconds
current += DateTimeSpan(minutes = if (left <= 0) 1 else 0, seconds = left) 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 current
} }
/** internal fun createCronDateTimeList(
* @return [KronScheduler] (in fact [CronDateTimeScheduler]) based on incoming data
*/
internal fun createKronScheduler(
seconds: Array<Byte>? = null, seconds: Array<Byte>? = null,
minutes: Array<Byte>? = null, minutes: Array<Byte>? = null,
hours: Array<Byte>? = null, hours: Array<Byte>? = null,
dayOfMonth: Array<Byte>? = null, dayOfMonth: Array<Byte>? = null,
month: Array<Byte>? = null, month: Array<Byte>? = null,
years: Array<Int>? = null years: Array<Int>? = null,
): KronScheduler { weekDays: Array<Byte>? = null
): List<CronDateTime> {
val resultCronDateTimes = mutableListOf(CronDateTime()) val resultCronDateTimes = mutableListOf(CronDateTime())
seconds ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte -> seconds ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
@ -114,5 +135,35 @@ internal fun createKronScheduler(
previousCronDateTime.copy(year = currentTime) 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 package dev.inmo.krontab.internal
import com.soywiz.klock.DateTime import com.soywiz.klock.DateTime
import dev.inmo.krontab.* import dev.inmo.krontab.KronScheduler
import dev.inmo.krontab.collection.plus import dev.inmo.krontab.collection.plus
/** /**
@ -26,8 +26,8 @@ internal data class CronDateTimeScheduler internal constructor(
* *
* @see toNearDateTime * @see toNearDateTime
*/ */
override suspend fun next(relatively: DateTime): DateTime { override suspend fun next(relatively: DateTime): DateTime? {
return cronDateTimes.mapNotNull { it.toNearDateTime(relatively) }.minOrNull() ?: getAnyNext(relatively) 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 package dev.inmo.krontab.internal
import dev.inmo.krontab.utils.clamp
typealias Converter<T> = (Int) -> T typealias Converter<T> = (Int) -> T
internal val intToByteConverter: Converter<Byte> = { it: Int -> it.toByte() } internal val intToByteConverter: Converter<Byte> = { it: Int -> it.toByte() }
@ -18,7 +16,7 @@ private fun <T> createSimpleScheduler(from: String, dataRange: IntRange, dataCon
when { when {
currentToken.contains("-") -> { currentToken.contains("-") -> {
val splitted = currentToken.split("-") 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("/") -> { currentToken.contains("/") -> {
val (start, step) = currentToken.split("/") val (start, step) = currentToken.split("/")
@ -26,18 +24,20 @@ private fun <T> createSimpleScheduler(from: String, dataRange: IntRange, dataCon
0 0
} else { } else {
start.toInt() start.toInt()
}).clamp(dataRange) }).coerceIn(dataRange)
val stepNum = step.toInt().clamp(dataRange) val stepNum = step.toInt().coerceIn(dataRange)
(startNum .. dataRange.last step stepNum).map { it } (startNum .. dataRange.last step stepNum).map { it }
} }
currentToken == "*" -> return null currentToken == "*" -> return null
else -> listOf(currentToken.toInt().clamp(dataRange)) else -> listOf(currentToken.toInt().coerceIn(dataRange))
} }
} }
return results.map(dataConverter) 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 parseYears(from: String?) = from ?.let { createSimpleScheduler(from, yearRange, intToIntConverter) ?.toTypedArray() }
internal fun parseMonths(from: String) = createSimpleScheduler(from, monthRange, intToByteConverter) ?.toTypedArray() internal fun parseMonths(from: String) = createSimpleScheduler(from, monthRange, intToByteConverter) ?.toTypedArray()
internal fun parseDaysOfMonth(from: String) = createSimpleScheduler(from, dayOfMonthRange, 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 package dev.inmo.krontab.internal
internal val dayOfWeekRange = 0 .. 6
internal val yearRange = Int.MIN_VALUE .. Int.MAX_VALUE internal val yearRange = Int.MIN_VALUE .. Int.MAX_VALUE
internal val monthRange = 0 .. 11 internal val monthRange = 0 .. 11
internal val dayOfMonthRange = 0 .. 30 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 package dev.inmo.krontab.utils
import com.soywiz.klock.DateTime import com.soywiz.klock.DateTime
import com.soywiz.klock.DateTimeTz
import dev.inmo.krontab.KronScheduler import dev.inmo.krontab.KronScheduler
import kotlinx.coroutines.FlowPreview import dev.inmo.krontab.next
import kotlinx.coroutines.delay import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* 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 @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 @FlowPreview
class SchedulerFlow( class SchedulerFlow(
private val scheduler: KronScheduler 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 package dev.inmo.krontab.utils
import com.soywiz.klock.*
import dev.inmo.krontab.KronSchedulerTz
import dev.inmo.krontab.buildSchedule import dev.inmo.krontab.buildSchedule
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
@ -76,4 +78,19 @@ class StringParseTest {
assertEquals(expectedCollects, collected) 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)
}
}
}
}
}