Merge pull request #12 from InsanusMokrassar/0.5.0

0.5.0
This commit is contained in:
InsanusMokrassar 2021-01-03 13:32:04 +06:00 committed by GitHub
commit fd842b17fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 513 additions and 73 deletions

View File

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

View File

@ -53,18 +53,18 @@ 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
* * * * * * * * * * *
``` ```
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
will use seconds and minutes with more probability than months (for example). In fact, developers will use something will use seconds and minutes with more probability than months (for example) or even years. In fact, developers will use
like: something like:
```kotlin ```kotlin
doWhile("/5 * * * *") { doWhile("/5 * * * *") {
@ -73,7 +73,7 @@ doWhile("/5 * * * *") {
} }
``` ```
Or more version: An other version:
```kotlin ```kotlin
doInfinity("/5 * * * *") { doInfinity("/5 * * * *") {
@ -85,7 +85,7 @@ Both of examples will print `Called` message every five seconds.
### Config via builder ### 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 ```kotlin
val kronScheduler = buildSchedule { val kronScheduler = buildSchedule {

View File

@ -3,11 +3,14 @@ buildscript {
mavenLocal() mavenLocal()
jcenter() jcenter()
mavenCentral() mavenCentral()
google()
} }
dependencies { dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.github.breadmoirai:github-release:$github_release_plugin_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'
} }
} }
@ -15,8 +18,9 @@ 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"
} }
apply plugin: "com.android.library"
project.version = "0.4.2" project.version = "$version"
project.group = "dev.inmo" project.group = "dev.inmo"
apply from: "publish.gradle" apply from: "publish.gradle"
@ -27,6 +31,7 @@ repositories {
jcenter() jcenter()
mavenCentral() mavenCentral()
maven { url "https://kotlin.bintray.com/kotlinx" } maven { url "https://kotlin.bintray.com/kotlinx" }
google()
} }
apply from: './dokka.gradle' apply from: './dokka.gradle'
@ -37,6 +42,9 @@ kotlin {
browser() browser()
nodejs() nodejs()
} }
android {
publishAllLibraryVariants()
}
sourceSets { sourceSets {
@ -48,6 +56,13 @@ kotlin {
api "com.soywiz.korlibs.klock:klock:$klockVersion" api "com.soywiz.korlibs.klock:klock:$klockVersion"
} }
} }
androidMain {
dependencies {
api "androidx.work:work-runtime-ktx:$androidx_work_version"
}
}
commonTest { commonTest {
dependencies { dependencies {
implementation kotlin('test-common') implementation kotlin('test-common')
@ -64,5 +79,48 @@ kotlin {
implementation kotlin('test-js') implementation kotlin('test-js')
} }
} }
androidTest {
dependencies {
implementation kotlin('test-junit')
}
}
}
}
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
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
test {
java.srcDir file("src/jvmTest")
}
} }
} }

View File

@ -4,6 +4,8 @@ kotlin.js.generate.externals=true
kotlin.incremental=true kotlin.incremental=true
kotlin.incremental.js=true kotlin.incremental.js=true
kotlin.incremental.multiplatform=true kotlin.incremental.multiplatform=true
android.useAndroidX=true
android.enableJetifier=false
kotlin_version=1.4.21 kotlin_version=1.4.21
@ -13,5 +15,24 @@ dokka_version=1.4.20
klockVersion=2.0.3 klockVersion=2.0.3
## Github reease
github_release_plugin_version=2.2.12 github_release_plugin_version=2.2.12
## Android
android_minSdkVersion=19
android_compileSdkVersion=30
android_buildToolsVersion=30.0.2
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
## Common
version=0.5.0
android_code_version=1

View File

@ -7,9 +7,13 @@ import kotlinx.coroutines.delay
* Execute [block] once at the [KronScheduler.next] time and return result of [block] calculation. * 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!!! 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 { suspend inline fun <T> KronScheduler.doOnce(noinline block: suspend () -> T): T {
delay((next() - DateTime.now()).millisecondsLong) next() ?.let {
delay((it - DateTime.now()).millisecondsLong)
}
return block() return block()
} }

View File

@ -1,6 +1,7 @@
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
@ -17,5 +18,10 @@ interface KronScheduler {
* *
* @see dev.inmo.krontab.internal.CronDateTimeScheduler.next * @see dev.inmo.krontab.internal.CronDateTimeScheduler.next
*/ */
suspend fun next(relatively: DateTime = DateTime.now()): DateTime 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

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

View File

@ -0,0 +1,18 @@
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,12 +1,15 @@
package dev.inmo.krontab package dev.inmo.krontab
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.CronDateTime import dev.inmo.krontab.internal.CronDateTime
import dev.inmo.krontab.internal.CronDateTimeScheduler import dev.inmo.krontab.internal.CronDateTimeScheduler
internal val anyCronDateTime by lazy { internal val anyCronDateTime by lazy {
CronDateTime() CronDateTime()
} }
internal fun getAnyNext(relatively: DateTime) = anyCronDateTime.toNearDateTime(relatively)!!
/** /**
* [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] * [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now]
@ -49,3 +52,10 @@ val EveryDayOfMonthScheduler: KronScheduler by lazy {
val EveryMonthScheduler: KronScheduler by lazy { val EveryMonthScheduler: KronScheduler by lazy {
buildSchedule { months { 0 every 1 } } 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

@ -16,6 +16,7 @@ typealias KrontabTemplate = String
* * hours * * hours
* * dayOfMonth * * dayOfMonth
* * month * * month
* * (optional) year
* *
* And each one have next format: * And each one have next format:
* *
@ -37,6 +38,7 @@ typealias KrontabTemplate = String
* * Hours ranges can be found in [hoursRange] * * Hours ranges can be found in [hoursRange]
* * 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])
* *
* Examples: * Examples:
* *
@ -44,20 +46,25 @@ 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 2021" for triggering in near first second of second minute of third hour of fourth day of may of 2021st year
* *
* @see dev.inmo.krontab.internal.createKronScheduler * @see dev.inmo.krontab.internal.createKronScheduler
*/ */
fun createSimpleScheduler(incoming: KrontabTemplate): KronScheduler { fun createSimpleScheduler(incoming: KrontabTemplate): KronScheduler {
val (secondsSource, minutesSource, hoursSource, dayOfMonthSource, monthSource) = incoming.split(" ") val yearSource: String?
val (secondsSource, minutesSource, hoursSource, dayOfMonthSource, monthSource) = incoming.split(" ").also {
yearSource = it.getOrNull(5)
}
val secondsParsed = parseSeconds(secondsSource) val secondsParsed = parseSeconds(secondsSource)
val minutesParsed = parseMinutes(minutesSource) val minutesParsed = parseMinutes(minutesSource)
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 createKronScheduler(
secondsParsed, minutesParsed, hoursParsed, dayOfMonthParsed, monthParsed secondsParsed, minutesParsed, hoursParsed, dayOfMonthParsed, monthParsed, yearParsed
) )
} }
@ -70,3 +77,8 @@ fun buildSchedule(incoming: KrontabTemplate): KronScheduler = createSimpleSchedu
* Shortcut for [buildSchedule] * Shortcut for [buildSchedule]
*/ */
fun KrontabTemplate.toSchedule(): KronScheduler = buildSchedule(this) fun KrontabTemplate.toSchedule(): KronScheduler = buildSchedule(this)
/**
* Shortcut for [buildSchedule]
*/
fun KrontabTemplate.toKronScheduler(): KronScheduler = buildSchedule(this)

View File

@ -21,20 +21,21 @@ class SchedulerBuilder(
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 fun <T : TimeBuilder> callAndReturn( private fun <I, T : TimeBuilder<I>> callAndReturn(
initial: Array<Byte>?, initial: Array<I>?,
builder: T, builder: T,
block: T.() -> Unit block: T.() -> Unit
): Array<Byte>? { ): List<I>? {
builder.block() builder.block()
val builderValue = builder.build() val builderValue = builder.build()
return initial ?.let { return initial ?.let {
builderValue ?.let { _ -> builderValue ?.let { _ ->
(it + builderValue).distinct().toTypedArray() (it + builderValue).distinct()
} ?: builderValue } ?: builderValue
} ?: builderValue } ?: builderValue
} }
@ -47,7 +48,7 @@ class SchedulerBuilder(
seconds, seconds,
SecondsBuilder(), SecondsBuilder(),
block block
) ) ?.toTypedArray()
} }
/** /**
@ -58,7 +59,7 @@ class SchedulerBuilder(
minutes, minutes,
MinutesBuilder(), MinutesBuilder(),
block block
) ) ?.toTypedArray()
} }
/** /**
@ -69,7 +70,7 @@ class SchedulerBuilder(
hours, hours,
HoursBuilder(), HoursBuilder(),
block block
) ) ?.toTypedArray()
} }
/** /**
@ -80,7 +81,7 @@ class SchedulerBuilder(
dayOfMonth, dayOfMonth,
DaysOfMonthBuilder(), DaysOfMonthBuilder(),
block block
) ) ?.toTypedArray()
} }
/** /**
@ -91,7 +92,18 @@ class SchedulerBuilder(
month, month,
MonthsBuilder(), MonthsBuilder(),
block block
) ) ?.toTypedArray()
}
/**
* Starts an year block
*/
fun years(block: YearsBuilder.() -> Unit) {
year = callAndReturn(
year,
YearsBuilder(),
block
) ?.toTypedArray()
} }
/** /**
@ -100,5 +112,5 @@ 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) fun build(): KronScheduler = createKronScheduler(seconds, minutes, hours, dayOfMonth, month, year)
} }

View File

@ -7,8 +7,9 @@ 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 [clamp] incoming data using its [restrictionsRange]
*/ */
sealed class TimeBuilder ( sealed class TimeBuilder<T : Number> (
private val restrictionsRange: IntRange private val restrictionsRange: IntRange,
private val converter: Converter<T>
) { ) {
private var result: Set<Int>? = null private var result: Set<Int>? = null
@ -119,11 +120,12 @@ sealed class TimeBuilder (
*/ */
fun includeFirst() = at(restrictionsRange.first) fun includeFirst() = at(restrictionsRange.first)
internal fun build() = result ?.map { it.toByte() } ?.toTypedArray() internal fun build() = result ?.map(converter)
} }
class SecondsBuilder : TimeBuilder(secondsRange) class SecondsBuilder : TimeBuilder<Byte>(secondsRange, intToByteConverter)
class MinutesBuilder : TimeBuilder(minutesRange) class MinutesBuilder : TimeBuilder<Byte>(minutesRange, intToByteConverter)
class HoursBuilder : TimeBuilder(hoursRange) class HoursBuilder : TimeBuilder<Byte>(hoursRange, intToByteConverter)
class DaysOfMonthBuilder : TimeBuilder(dayOfMonthRange) class DaysOfMonthBuilder : TimeBuilder<Byte>(dayOfMonthRange, intToByteConverter)
class MonthsBuilder : TimeBuilder(monthRange) class MonthsBuilder : TimeBuilder<Byte>(monthRange, intToByteConverter)
class YearsBuilder : TimeBuilder<Int>(yearRange, intToIntConverter)

View File

@ -1,11 +1,9 @@
package dev.inmo.krontab.collection package dev.inmo.krontab.collection
import com.soywiz.klock.DateTime import com.soywiz.klock.DateTime
import dev.inmo.krontab.KronScheduler import dev.inmo.krontab.*
import dev.inmo.krontab.anyCronDateTime
import dev.inmo.krontab.internal.* import dev.inmo.krontab.internal.*
import dev.inmo.krontab.internal.CronDateTimeScheduler import dev.inmo.krontab.internal.CronDateTimeScheduler
import dev.inmo.krontab.internal.merge
import dev.inmo.krontab.internal.toNearDateTime import dev.inmo.krontab.internal.toNearDateTime
/** /**
@ -20,7 +18,7 @@ data class CollectionKronScheduler internal constructor(
* Add [kronScheduler] into its [schedulers] list * Add [kronScheduler] into its [schedulers] list
* *
* * When [kronScheduler] is [CronDateTimeScheduler] it will merge all [CronDateTimeScheduler]s from [schedulers] list * * When [kronScheduler] is [CronDateTimeScheduler] it will merge all [CronDateTimeScheduler]s from [schedulers] list
* and this [kronScheduler] using [merge] function * and this [kronScheduler] using [mergeCronDateTimeSchedulers] function
* * When [kronScheduler] is [CollectionKronScheduler] it this instance will include all [kronScheduler] * * When [kronScheduler] is [CollectionKronScheduler] it this instance will include all [kronScheduler]
* [schedulers] * [schedulers]
* * Otherwise [kronScheduler] will be added to [schedulers] list * * Otherwise [kronScheduler] will be added to [schedulers] list
@ -37,7 +35,7 @@ data class CollectionKronScheduler internal constructor(
} }
} }
schedulers.add( schedulers.add(
merge(resultCronDateTimes) mergeCronDateTimeSchedulers(resultCronDateTimes)
) )
} }
is CollectionKronScheduler -> kronScheduler.schedulers.forEach { is CollectionKronScheduler -> kronScheduler.schedulers.forEach {
@ -48,6 +46,6 @@ data class CollectionKronScheduler internal constructor(
} }
override suspend fun next(relatively: DateTime): DateTime { override suspend fun next(relatively: DateTime): DateTime {
return schedulers.minOfOrNull { it.next(relatively) } ?: anyCronDateTime.toNearDateTime(relatively) return schedulers.mapNotNull { it.next(relatively) }.minOrNull() ?: getAnyNext(relatively)
} }
} }

View File

@ -12,6 +12,7 @@ import dev.inmo.krontab.KronScheduler
* @param seconds 0-59 * @param seconds 0-59
*/ */
internal data class CronDateTime( internal data class CronDateTime(
val year: Int? = null,
val month: Byte? = null, val month: Byte? = null,
val dayOfMonth: Byte? = null, val dayOfMonth: Byte? = null,
val hours: Byte? = null, val hours: Byte? = null,
@ -19,6 +20,7 @@ internal data class CronDateTime(
val seconds: Byte? = null val seconds: Byte? = null
) { ) {
init { init {
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)
check(hours?.let { it in hoursRange } ?: true) check(hours?.let { it in hoursRange } ?: true)
@ -32,7 +34,7 @@ internal data class CronDateTime(
/** /**
* @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
seconds?.let { seconds?.let {
@ -63,7 +65,13 @@ internal fun CronDateTime.toNearDateTime(relativelyTo: DateTime = DateTime.now()
month ?.let { month ?.let {
val left = it - current.month0 val left = it - current.month0
current += DateTimeSpan(months = if (left < 0) 1 else 0, days = left) current += DateTimeSpan(years = if (left < 0) 1 else 0, months = left)
}
year ?.let {
if (current.yearInt != it) {
return null
}
} }
return current return current
@ -77,7 +85,8 @@ internal fun createKronScheduler(
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
): KronScheduler { ): KronScheduler {
val resultCronDateTimes = mutableListOf(CronDateTime()) val resultCronDateTimes = mutableListOf(CronDateTime())
@ -101,5 +110,9 @@ internal fun createKronScheduler(
previousCronDateTime.copy(month = currentTime) previousCronDateTime.copy(month = currentTime)
} }
years ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Int ->
previousCronDateTime.copy(year = currentTime)
}
return CronDateTimeScheduler(resultCronDateTimes.toList()) return CronDateTimeScheduler(resultCronDateTimes.toList())
} }

View File

@ -1,8 +1,8 @@
package dev.inmo.krontab.internal package dev.inmo.krontab.internal
import com.soywiz.klock.DateTime import com.soywiz.klock.DateTime
import dev.inmo.krontab.KronScheduler import dev.inmo.krontab.*
import dev.inmo.krontab.anyCronDateTime import dev.inmo.krontab.collection.plus
/** /**
* Cron-oriented realisation of [KronScheduler] * Cron-oriented realisation of [KronScheduler]
@ -13,40 +13,44 @@ import dev.inmo.krontab.anyCronDateTime
* @see dev.inmo.krontab.EveryHourScheduler * @see dev.inmo.krontab.EveryHourScheduler
* @see dev.inmo.krontab.EveryDayOfMonthScheduler * @see dev.inmo.krontab.EveryDayOfMonthScheduler
* @see dev.inmo.krontab.EveryMonthScheduler * @see dev.inmo.krontab.EveryMonthScheduler
* @see dev.inmo.krontab.EveryYearScheduler
* *
* @see dev.inmo.krontab.builder.buildSchedule * @see dev.inmo.krontab.builder.buildSchedule
* @see dev.inmo.krontab.builder.SchedulerBuilder * @see dev.inmo.krontab.builder.SchedulerBuilder
*/ */
@Deprecated("This class will get internal status in future") internal data class CronDateTimeScheduler internal constructor(
data class CronDateTimeScheduler internal constructor(
internal val cronDateTimes: List<CronDateTime> internal val cronDateTimes: List<CronDateTime>
) : KronScheduler { ) : KronScheduler {
/** /**
* @return Near date using [cronDateTimes] list and getting the [Iterable.min] one * @return Near date using [cronDateTimes] list and getting the [Iterable.minByOrNull] one
* *
* @see toNearDateTime * @see toNearDateTime
*/ */
override suspend fun next(relatively: DateTime): DateTime { override suspend fun next(relatively: DateTime): DateTime {
return cronDateTimes.map { it.toNearDateTime(relatively) }.minOrNull() ?: anyCronDateTime.toNearDateTime(relatively) return cronDateTimes.mapNotNull { it.toNearDateTime(relatively) }.minOrNull() ?: getAnyNext(relatively)
} }
} }
/** internal fun mergeCronDateTimeSchedulers(schedulers: List<CronDateTimeScheduler>) = CronDateTimeScheduler(
* @return New instance of [CronDateTimeScheduler] with all unique [CronDateTimeScheduler.cronDateTimes] of schedulers.flatMap { it.cronDateTimes }
* [kronDateTimeSchedulers] included
*/
@Suppress("NOTHING_TO_INLINE")
fun merge(kronDateTimeSchedulers: List<CronDateTimeScheduler>) = CronDateTimeScheduler(
kronDateTimeSchedulers.flatMap { it.cronDateTimes }.distinct()
) )
/** /**
* @return Vararg shortcyut for [merge] * @return New instance of [CronDateTimeScheduler] with all unique [CronDateTimeScheduler.cronDateTimes] of
* [kronSchedulers] included
*/ */
@Suppress("NOTHING_TO_INLINE") @Deprecated("Will be removed in next major release", ReplaceWith("merge", "dev.inmo.krontab"))
inline fun merge(vararg kronDateTimeSchedulers: CronDateTimeScheduler) = merge(kronDateTimeSchedulers.toList()) fun merge(kronSchedulers: List<KronScheduler>) = kronSchedulers.apply { dev.inmo.krontab.merge() }
/** /**
* Use [merge] operation to internalcreate new [CronDateTimeScheduler] with all [CronDateTimeScheduler.cronDateTimes] * @return Vararg shortcut for [dev.inmo.krontab.merge]
*/ */
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
inline fun CronDateTimeScheduler.plus(other: CronDateTimeScheduler) = merge(this, other) @Deprecated("Will be removed in next major release", ReplaceWith("merge", "dev.inmo.krontab"))
inline fun merge(vararg kronDateTimeSchedulers: KronScheduler) = kronDateTimeSchedulers.apply { dev.inmo.krontab.merge() }
/**
* @return Vararg shortcut for [dev.inmo.krontab.merge]
*/
@Suppress("NOTHING_TO_INLINE")
@Deprecated("Will be removed in next major release", ReplaceWith("merge", "dev.inmo.krontab"))
inline fun KronScheduler.plus(other: KronScheduler) = this + other

View File

@ -2,7 +2,11 @@ package dev.inmo.krontab.internal
import dev.inmo.krontab.utils.clamp import dev.inmo.krontab.utils.clamp
private fun createSimpleScheduler(from: String, dataRange: IntRange): Array<Byte>? { 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 things = from.split(",")
val results = things.flatMap { val results = things.flatMap {
@ -31,18 +35,19 @@ private fun createSimpleScheduler(from: String, dataRange: IntRange): Array<Byte
} }
} }
return results.map { it.toByte() }.toTypedArray() return results.map(dataConverter)
} }
internal fun parseMonths(from: String) = createSimpleScheduler(from, monthRange) internal fun parseYears(from: String?) = from ?.let { createSimpleScheduler(from, yearRange, intToIntConverter) ?.toTypedArray() }
internal fun parseDaysOfMonth(from: String) = createSimpleScheduler(from, dayOfMonthRange) internal fun parseMonths(from: String) = createSimpleScheduler(from, monthRange, intToByteConverter) ?.toTypedArray()
internal fun parseHours(from: String) = createSimpleScheduler(from, hoursRange) internal fun parseDaysOfMonth(from: String) = createSimpleScheduler(from, dayOfMonthRange, intToByteConverter) ?.toTypedArray()
internal fun parseMinutes(from: String) = createSimpleScheduler(from, minutesRange) internal fun parseHours(from: String) = createSimpleScheduler(from, hoursRange, intToByteConverter) ?.toTypedArray()
internal fun parseSeconds(from: String) = createSimpleScheduler(from, secondsRange) internal fun parseMinutes(from: String) = createSimpleScheduler(from, minutesRange, intToByteConverter) ?.toTypedArray()
internal fun parseSeconds(from: String) = createSimpleScheduler(from, secondsRange, intToByteConverter) ?.toTypedArray()
internal fun Array<Byte>.fillWith( internal fun <T> Array<T>.fillWith(
whereToPut: MutableList<CronDateTime>, whereToPut: MutableList<CronDateTime>,
createFactory: (CronDateTime, Byte) -> CronDateTime createFactory: (CronDateTime, T) -> CronDateTime
) { ) {
val previousValues = whereToPut.toList() val previousValues = whereToPut.toList()

View File

@ -1,5 +1,6 @@
package dev.inmo.krontab.internal package dev.inmo.krontab.internal
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
internal val hoursRange = 0 .. 23 internal val hoursRange = 0 .. 23

View File

@ -17,7 +17,7 @@ class SchedulerFlow(
override suspend fun collectSafely(collector: FlowCollector<DateTime>) { override suspend fun collectSafely(collector: FlowCollector<DateTime>) {
while (true) { while (true) {
val now = DateTime.now() val now = DateTime.now()
val nextTime = scheduler.next(now) val nextTime = scheduler.next(now) ?: break
val sleepDelay = (nextTime - now).millisecondsLong val sleepDelay = (nextTime - now).millisecondsLong
delay(sleepDelay) delay(sleepDelay)
collector.emit(nextTime) collector.emit(nextTime)

View File

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

View File

@ -0,0 +1,147 @@
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

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