diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..58ddeec --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,5 @@ +# These are supported funding model platforms + +patreon: InsanusMokrassar + +custom: ['https://paypal.me/InsanusMokrassar?locale.x=ru_RU'] diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..26df369 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,3 @@ +code: "**/*.kt" +gradle: "**/*.gradle" +markdown: "**/*.md" diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml new file mode 100644 index 0000000..febc30d --- /dev/null +++ b/.github/workflows/greetings.yml @@ -0,0 +1,13 @@ +name: Greetings + +on: [pull_request, issues] + +jobs: + greeting: + runs-on: ubuntu-latest + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: 'Welcome with your first issue' + pr-message: 'Welcome with your first Pull Request' diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml new file mode 100644 index 0000000..be1ee46 --- /dev/null +++ b/.github/workflows/label.yml @@ -0,0 +1,18 @@ +# This workflow will triage pull requests and apply a label based on the +# paths that are modified in the pull request. +# +# To use this workflow, you will need to set up a .github/labeler.yml +# file with configuration. For more information, see: +# https://github.com/actions/labeler/blob/master/README.md + +name: "Pull Request Labeler" +on: + - pull_request + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v2 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index e973f11..0524856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ * Coroutines `1.3.2` -> `1.3.3` * Klock `1.7.3` -> `1.8.6` +### 0.2.3 + +* Updates in libraries: + * Kotlin `1.3.70` -> `1.3.72` + * Coroutines `1.3.5` -> `1.3.7` + * Klock `1.10.0` -> `1.11.3` +* A lot of KDocs added and fixed +* `EverySecondScheduler` changed its building logic - now it is lazy with builder using +* `KronScheduler#doOnce` was optimized: now it will be explicitly called once and return result of its calculations + * `KronScheduler#doWhile` was rewritten to use `KronScheduler#doOnce` for calculations of `block` result +* New `buildSchedule(String)` function as a shortcut for `createSimpleScheduler(String)` + ### 0.2.2 * Updates in libraries: diff --git a/build.gradle b/build.gradle index 9a9ffd2..735b4cd 100644 --- a/build.gradle +++ b/build.gradle @@ -15,9 +15,10 @@ buildscript { plugins { id "org.jetbrains.kotlin.multiplatform" version "$kotlin_version" id "org.jetbrains.kotlin.plugin.serialization" version "$kotlin_version" + id "org.jetbrains.dokka" version "$dokka_version" } -project.version = "0.2.2" +project.version = "0.2.3" project.group = "com.insanusmokrassar" apply from: "publish.gradle" @@ -29,6 +30,8 @@ repositories { maven { url "https://kotlin.bintray.com/kotlinx" } } +apply from: './dokka.gradle' + kotlin { jvm() js() diff --git a/dokka.gradle b/dokka.gradle new file mode 100644 index 0000000..4248c4b --- /dev/null +++ b/dokka.gradle @@ -0,0 +1,42 @@ +dokka { + outputFormat = 'html' + + switch (true) { + case project.hasProperty("DOKKA_PATH"): + outputDirectory = project.property("DOKKA_PATH").toString() + break + case System.getenv("DOKKA_PATH") != null: + outputDirectory = System.getenv("DOKKA_PATH") + break + } + + multiplatform { + global { + perPackageOption { + prefix = "com.insanusmokrassar" + skipDeprecated = true + includeNonPublic = true + reportUndocumented = true + } + + sourceLink { + path = "./" + url = "https://github.com/InsanusMokrassar/krontab/blob/master/" + lineSuffix = "#L" + } + } + + common { + targets = ["JVM", "JS"] + sourceRoot { path = "src/commonMain" } + } + js { + targets = ["JS"] + sourceRoot { path = "src/jsMain" } + } + jvm { + targets = ["JVM"] + sourceRoot { path = "src/jvmMain" } + } + } +} diff --git a/gradle.properties b/gradle.properties index 5b1c537..4da145e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,11 @@ kotlin.code.style=official -kotlin_version=1.3.70 -kotlin_coroutines_version=1.3.5 +kotlin_version=1.3.72 +kotlin_coroutines_version=1.3.7 + +dokka_version=0.10.1 gradle_bintray_plugin_version=1.8.4 -klockVersion=1.10.0 +klockVersion=1.11.3 kotlin.incremental.multiplatform=true diff --git a/src/commonMain/kotlin/com/insanusmokrassar/krontab/Executes.kt b/src/commonMain/kotlin/com/insanusmokrassar/krontab/Executes.kt index 6c2fe51..55acf5b 100644 --- a/src/commonMain/kotlin/com/insanusmokrassar/krontab/Executes.kt +++ b/src/commonMain/kotlin/com/insanusmokrassar/krontab/Executes.kt @@ -1,32 +1,53 @@ package com.insanusmokrassar.krontab import com.soywiz.klock.DateTime +import kotlinx.coroutines.Deferred import kotlinx.coroutines.delay -suspend inline fun KronScheduler.doWhile(noinline block: suspend () -> Boolean) { - do { - delay(next().unixMillisLong - DateTime.now().unixMillisLong) - } while (block()) +/** + * 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 explicit. + */ +suspend inline fun KronScheduler.doOnce(noinline block: suspend () -> T): T { + delay((next() - DateTime.now()).millisecondsLong) + return block() } + +/** + * Will [createSimpleScheduler] using [scheduleConfig] and call [doOnce] on it + */ +suspend inline fun doOnce( + scheduleConfig: String, + noinline block: suspend () -> T +) = createSimpleScheduler(scheduleConfig).doOnce(block) + +/** + * Will execute [block] while it will return true as a result of its calculation + */ +suspend inline fun KronScheduler.doWhile(noinline block: suspend () -> Boolean) { + do { val doNext = doOnce(block) } while (doNext) +} + +/** + * Will [createSimpleScheduler] using [scheduleConfig] and call [doWhile] with [block] + */ suspend inline fun doWhile( scheduleConfig: String, noinline block: suspend () -> Boolean ) = createSimpleScheduler(scheduleConfig).doWhile(block) +/** + * Will execute [block] without any checking of result + */ suspend inline fun KronScheduler.doInfinity(noinline block: suspend () -> Unit) = doWhile { block() true } +/** + * Will [createSimpleScheduler] using [scheduleConfig] and call [doInfinity] with [block] + */ suspend inline fun doInfinity( scheduleConfig: String, noinline block: suspend () -> Unit ) = createSimpleScheduler(scheduleConfig).doInfinity(block) - -suspend inline fun KronScheduler.doOnce(noinline block: suspend () -> Unit) = doWhile { - block() - false -} -suspend inline fun doOnce( - scheduleConfig: String, - noinline block: suspend () -> Unit -) = createSimpleScheduler(scheduleConfig).doOnce(block) diff --git a/src/commonMain/kotlin/com/insanusmokrassar/krontab/KronScheduler.kt b/src/commonMain/kotlin/com/insanusmokrassar/krontab/KronScheduler.kt index b4ea91f..827e8bb 100644 --- a/src/commonMain/kotlin/com/insanusmokrassar/krontab/KronScheduler.kt +++ b/src/commonMain/kotlin/com/insanusmokrassar/krontab/KronScheduler.kt @@ -2,6 +2,20 @@ package com.insanusmokrassar.krontab import com.soywiz.klock.DateTime +/** + * This interface was created for abstraction of [next] operation. Currently, there is only + * [com.insanusmokrassar.krontab.internal.CronDateTimeScheduler] realisation of this interface inside of this library, + * but you it is possible to create your own realisation of this interface for scheduling, for example, depending of + * users activity or something like this + * + * @see com.insanusmokrassar.krontab.internal.CronDateTimeScheduler + */ interface KronScheduler { + + /** + * @return Next [DateTime] when some action must be triggered according to settings of this instance + * + * @see com.insanusmokrassar.krontab.internal.CronDateTimeScheduler.next + */ suspend fun next(relatively: DateTime = DateTime.now()): DateTime } diff --git a/src/commonMain/kotlin/com/insanusmokrassar/krontab/SchedulerShortcuts.kt b/src/commonMain/kotlin/com/insanusmokrassar/krontab/SchedulerShortcuts.kt index 5688e24..ef05f6f 100644 --- a/src/commonMain/kotlin/com/insanusmokrassar/krontab/SchedulerShortcuts.kt +++ b/src/commonMain/kotlin/com/insanusmokrassar/krontab/SchedulerShortcuts.kt @@ -2,28 +2,50 @@ package com.insanusmokrassar.krontab import com.insanusmokrassar.krontab.builder.buildSchedule import com.insanusmokrassar.krontab.internal.CronDateTime +import com.insanusmokrassar.krontab.internal.CronDateTimeScheduler internal val anyCronDateTime by lazy { CronDateTime() } + +/** + * [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + */ val AnyTimeScheduler: KronScheduler by lazy { CronDateTimeScheduler(listOf(anyCronDateTime)) } -val EverySecondScheduler: KronScheduler - get() = AnyTimeScheduler +/** + * [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one second + */ +val EverySecondScheduler: KronScheduler by lazy { + buildSchedule { seconds { 0 every 1 } } +} + +/** + * [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one minute + */ val EveryMinuteScheduler: KronScheduler by lazy { buildSchedule { minutes { 0 every 1 } } } +/** + * [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one hour + */ val EveryHourScheduler: KronScheduler by lazy { buildSchedule { hours { 0 every 1 } } } +/** + * [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one day + */ val EveryDayOfMonthScheduler: KronScheduler by lazy { buildSchedule { dayOfMonth { 0 every 1 } } } +/** + * [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one month + */ val EveryMonthScheduler: KronScheduler by lazy { buildSchedule { months { 0 every 1 } } } \ No newline at end of file diff --git a/src/commonMain/kotlin/com/insanusmokrassar/krontab/StringParser.kt b/src/commonMain/kotlin/com/insanusmokrassar/krontab/StringParser.kt index 2b447d5..fffb1a3 100644 --- a/src/commonMain/kotlin/com/insanusmokrassar/krontab/StringParser.kt +++ b/src/commonMain/kotlin/com/insanusmokrassar/krontab/StringParser.kt @@ -5,27 +5,38 @@ import com.insanusmokrassar.krontab.internal.* /** * Parse [incoming] string and adapt according to next format: "* * * * *" where order of things: * - * seconds - * minutes - * hours - * dayOfMonth - * month + * * seconds + * * minutes + * * hours + * * dayOfMonth + * * month * * And each one have next format: * - * {number},{number},... + * `{number},{number},...` * - * and {number} here is one of {int}-{int} OR {int}/{int} OR *\/{int} OR {int}. + * and {number} here is one of * - * Seconds ranges can be found in [com.insanusmokrassar.krontab.internal.secondsRange]. - * Minutes ranges can be found in [com.insanusmokrassar.krontab.internal.minutesRange]. - * Hours ranges can be found in [com.insanusmokrassar.krontab.internal.hoursRange]. - * Days of month ranges can be found in [com.insanusmokrassar.krontab.internal.dayOfMonthRange]. - * Months ranges can be found in [com.insanusmokrassar.krontab.internal.monthRange]. + * * {int}-{int} + * * {int}/{int} + * * */{int} + * * {int} * - * @sample "0/5 * * * *" for every five seconds triggering - * @sample "0/15 30 * * *" for every 15th seconds in a half of each hour - * @sample "1 2 3 4 5" for triggering in near first second of second minute of third hour of fourth day of may + * Additional info about ranges can be found in follow accordance: + * + * * Seconds ranges can be found in [secondsRange] + * * Minutes ranges can be found in [minutesRange] + * * Hours ranges can be found in [hoursRange] + * * Days of month ranges can be found in [dayOfMonthRange] + * * Months ranges can be found in [monthRange] + * + * Examples: + * + * * "0/5 * * * *" for every five seconds triggering + * * "0/15 30 * * *" for every 15th seconds in a half of each hour + * * "1 2 3 4 5" for triggering in near first second of second minute of third hour of fourth day of may + * + * @see com.insanusmokrassar.krontab.internal.createKronScheduler */ fun createSimpleScheduler(incoming: String): KronScheduler { val (secondsSource, minutesSource, hoursSource, dayOfMonthSource, monthSource) = incoming.split(" ") @@ -36,27 +47,12 @@ fun createSimpleScheduler(incoming: String): KronScheduler { val dayOfMonthParsed = parseDaysOfMonth(dayOfMonthSource) val monthParsed = parseMonths(monthSource) - val resultCronDateTimes = mutableListOf(CronDateTime()) + return createKronScheduler( + secondsParsed, minutesParsed, hoursParsed, dayOfMonthParsed, monthParsed + ) +} - secondsParsed ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte -> - previousCronDateTime.copy(seconds = currentTime) - } - - minutesParsed ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte -> - previousCronDateTime.copy(minutes = currentTime) - } - - hoursParsed ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte -> - previousCronDateTime.copy(hours = currentTime) - } - - dayOfMonthParsed ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte -> - previousCronDateTime.copy(dayOfMonth = currentTime) - } - - monthParsed ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte -> - previousCronDateTime.copy(month = currentTime) - } - - return CronDateTimeScheduler(resultCronDateTimes.toList()) -} \ No newline at end of file +/** + * + */ +fun buildSchedule(incoming: String): KronScheduler = createSimpleScheduler(incoming) \ No newline at end of file diff --git a/src/commonMain/kotlin/com/insanusmokrassar/krontab/builder/SchedulerBuilder.kt b/src/commonMain/kotlin/com/insanusmokrassar/krontab/builder/SchedulerBuilder.kt index 7beb38f..57d4aec 100644 --- a/src/commonMain/kotlin/com/insanusmokrassar/krontab/builder/SchedulerBuilder.kt +++ b/src/commonMain/kotlin/com/insanusmokrassar/krontab/builder/SchedulerBuilder.kt @@ -1,10 +1,16 @@ package com.insanusmokrassar.krontab.builder -import com.insanusmokrassar.krontab.CronDateTimeScheduler import com.insanusmokrassar.krontab.KronScheduler +import com.insanusmokrassar.krontab.internal.* import com.insanusmokrassar.krontab.internal.CronDateTime +import com.insanusmokrassar.krontab.internal.CronDateTimeScheduler import com.insanusmokrassar.krontab.internal.fillWith +/** + * Will help to create an instance of [KronScheduler] + * + * @see com.insanusmokrassar.krontab.createSimpleScheduler + */ fun buildSchedule(settingsBlock: SchedulerBuilder.() -> Unit): KronScheduler { val builder = SchedulerBuilder() @@ -36,6 +42,9 @@ class SchedulerBuilder( } ?: builderValue } + /** + * Starts an seconds block + */ fun seconds(block: SecondsBuilder.() -> Unit) { seconds = callAndReturn( seconds, @@ -44,6 +53,9 @@ class SchedulerBuilder( ) } + /** + * Starts an minutes block + */ fun minutes(block: MinutesBuilder.() -> Unit) { minutes = callAndReturn( minutes, @@ -52,6 +64,9 @@ class SchedulerBuilder( ) } + /** + * Starts an hours block + */ fun hours(block: HoursBuilder.() -> Unit) { hours = callAndReturn( hours, @@ -60,6 +75,9 @@ class SchedulerBuilder( ) } + /** + * Starts an days of month block + */ fun dayOfMonth(block: DaysOfMonthBuilder.() -> Unit) { dayOfMonth = callAndReturn( dayOfMonth, @@ -68,6 +86,9 @@ class SchedulerBuilder( ) } + /** + * Starts an months block + */ fun months(block: MonthsBuilder.() -> Unit) { month = callAndReturn( month, @@ -76,29 +97,11 @@ class SchedulerBuilder( ) } - fun build(): KronScheduler { - val resultCronDateTimes = mutableListOf(CronDateTime()) - - seconds ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte -> - previousCronDateTime.copy(seconds = currentTime) - } - - minutes ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte -> - previousCronDateTime.copy(minutes = currentTime) - } - - hours ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte -> - previousCronDateTime.copy(hours = currentTime) - } - - dayOfMonth ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte -> - previousCronDateTime.copy(dayOfMonth = currentTime) - } - - month ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte -> - previousCronDateTime.copy(month = currentTime) - } - - return CronDateTimeScheduler(resultCronDateTimes.toList()) - } + /** + * @return Completely built and independent [KronScheduler] + * + * @see com.insanusmokrassar.krontab.createSimpleScheduler + * @see com.insanusmokrassar.krontab.internal.createKronScheduler + */ + fun build(): KronScheduler = createKronScheduler(seconds, minutes, hours, dayOfMonth, month) } diff --git a/src/commonMain/kotlin/com/insanusmokrassar/krontab/builder/TimeBuilder.kt b/src/commonMain/kotlin/com/insanusmokrassar/krontab/builder/TimeBuilder.kt index f060f92..f20a809 100644 --- a/src/commonMain/kotlin/com/insanusmokrassar/krontab/builder/TimeBuilder.kt +++ b/src/commonMain/kotlin/com/insanusmokrassar/krontab/builder/TimeBuilder.kt @@ -3,27 +3,53 @@ package com.insanusmokrassar.krontab.builder import com.insanusmokrassar.krontab.internal.* import com.insanusmokrassar.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] + */ sealed class TimeBuilder ( private val restrictionsRange: IntRange ) { private var result: Set? = null + /** + * After calling of this function this builder will allow any value of current time + */ + @Suppress("unused") fun allowAll() { result = null } + /** + * Will include all variations from this array inside of this timeline + */ + @Suppress("MemberVisibilityCanBePrivate") infix fun include(array: Array) { val clamped = array.map { it.clamp(restrictionsRange) } + (result ?: emptySet()) result = clamped.toSet() } + /** + * Add one [value] to current timeline + */ + @Suppress("unused") infix fun at(value: Int) { result = (result ?: emptySet()) + value.clamp(restrictionsRange) } + /** + * Just wrapper for more obvious writing something like "[from] 2 [every] 5". For example, for [SecondsBuilder] it + * will mean "[from] second second [every] 5 seconds", or "2, 7, 13, ..." + */ @Suppress("NOTHING_TO_INLINE") - inline fun from(value: Int) = value + inline infix fun from(value: Int) = value + /** + * Will create an sequence of times starting [from] [this] [every] [delay] times. For example, for [SecondsBuilder] it + * will mean "[from] second second [every] 5 seconds", or "2, 7, 13, ..." + * + * @see [from] + */ infix fun Int.every(delay: Int): Array { val progression = clamp(restrictionsRange) .. restrictionsRange.last step delay val result = progression.toSet().toTypedArray() @@ -32,8 +58,16 @@ sealed class TimeBuilder ( return result } - infix fun every(delay: Int): Array = 0 every delay + /** + * Shortcut for "[from] 0 [every] [delay]" + */ + infix fun every(delay: Int): Array = this from 0 every delay + + /** + * Will fill up this timeline from [this] up to [endIncluding] + */ + @Suppress("MemberVisibilityCanBePrivate") infix fun Int.upTo(endIncluding: Int): Array { val progression = clamp(restrictionsRange) .. endIncluding.clamp(restrictionsRange) val result = progression.toSet().toTypedArray() @@ -42,13 +76,17 @@ sealed class TimeBuilder ( return result } - infix fun upTo(endIncluding: Int): Array = 0 upTo endIncluding + /** + * Shortcut for "[from] 0 [upTo] [endIncluding]" + */ + @Suppress("unused") + infix fun upTo(endIncluding: Int): Array = this from 0 upTo endIncluding internal fun build() = result ?.map { it.toByte() } ?.toTypedArray() } class SecondsBuilder : TimeBuilder(secondsRange) class MinutesBuilder : TimeBuilder(minutesRange) -class HoursBuilder : TimeBuilder(com.insanusmokrassar.krontab.internal.hoursRange) -class DaysOfMonthBuilder : TimeBuilder(com.insanusmokrassar.krontab.internal.dayOfMonthRange) +class HoursBuilder : TimeBuilder(hoursRange) +class DaysOfMonthBuilder : TimeBuilder(dayOfMonthRange) class MonthsBuilder : TimeBuilder(monthRange) diff --git a/src/commonMain/kotlin/com/insanusmokrassar/krontab/internal/CronDateTime.kt b/src/commonMain/kotlin/com/insanusmokrassar/krontab/internal/CronDateTime.kt index 482053b..4a04474 100644 --- a/src/commonMain/kotlin/com/insanusmokrassar/krontab/internal/CronDateTime.kt +++ b/src/commonMain/kotlin/com/insanusmokrassar/krontab/internal/CronDateTime.kt @@ -1,15 +1,16 @@ package com.insanusmokrassar.krontab.internal +import com.insanusmokrassar.krontab.KronScheduler import com.insanusmokrassar.krontab.utils.clamp import com.soywiz.klock.DateTime import com.soywiz.klock.DateTimeSpan /** - * [month] 0-11 - * [dayOfMonth] 0-31 - * [hours] 0-23 - * [minutes] 0-59 - * [seconds] 0-59 + * @param month 0-11 + * @param dayOfMonth 0-31 + * @param hours 0-23 + * @param minutes 0-59 + * @param seconds 0-59 */ internal data class CronDateTime( val month: Byte? = null, @@ -19,16 +20,24 @@ internal data class CronDateTime( val seconds: Byte? = null ) { init { - check(month ?.let { it in com.insanusmokrassar.krontab.internal.monthRange } ?: true) - check(dayOfMonth ?.let { it in com.insanusmokrassar.krontab.internal.dayOfMonthRange } ?: true) - check(hours?.let { it in com.insanusmokrassar.krontab.internal.hoursRange } ?: true) - check(minutes?.let { it in com.insanusmokrassar.krontab.internal.minutesRange } ?: true) - check(seconds?.let { it in com.insanusmokrassar.krontab.internal.secondsRange } ?: true) + check(month ?.let { it in monthRange } ?: true) + check(dayOfMonth ?.let { it in dayOfMonthRange } ?: true) + check(hours?.let { it in hoursRange } ?: true) + check(minutes?.let { it in minutesRange } ?: true) + check(seconds?.let { it in secondsRange } ?: true) } internal val klockDayOfMonth = dayOfMonth ?.plus(1) companion object { + /** + * Using [clamp] extension for checking every parameter to be ensure that they are all correct + * @param month 0-11 + * @param dayOfMonth 0-31 + * @param hours 0-23 + * @param minutes 0-59 + * @param seconds 0-59 + */ fun create( month: Int? = null, dayOfMonth: Int? = null, @@ -36,15 +45,18 @@ internal data class CronDateTime( minutes: Int? = null, seconds: Int? = null ) = CronDateTime( - month ?.clamp(com.insanusmokrassar.krontab.internal.monthRange) ?.toByte(), - dayOfMonth ?.clamp(com.insanusmokrassar.krontab.internal.dayOfMonthRange) ?.toByte(), - hours ?.clamp(com.insanusmokrassar.krontab.internal.hoursRange) ?.toByte(), - minutes ?.clamp(com.insanusmokrassar.krontab.internal.minutesRange) ?.toByte(), - seconds ?.clamp(com.insanusmokrassar.krontab.internal.secondsRange) ?.toByte() + month ?.clamp(monthRange) ?.toByte(), + dayOfMonth ?.clamp(dayOfMonthRange) ?.toByte(), + hours ?.clamp(hoursRange) ?.toByte(), + minutes ?.clamp(minutesRange) ?.toByte(), + seconds ?.clamp(secondsRange) ?.toByte() ) } } +/** + * @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 @@ -75,3 +87,38 @@ internal fun CronDateTime.toNearDateTime(relativelyTo: DateTime = DateTime.now() return current } + +/** + * @return [KronScheduler] (in fact [CronDateTimeScheduler]) based on incoming data + */ +internal fun createKronScheduler( + seconds: Array? = null, + minutes: Array? = null, + hours: Array? = null, + dayOfMonth: Array? = null, + month: Array? = null +): KronScheduler { + val resultCronDateTimes = mutableListOf(CronDateTime()) + + seconds ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte -> + previousCronDateTime.copy(seconds = currentTime) + } + + minutes ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte -> + previousCronDateTime.copy(minutes = currentTime) + } + + hours ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte -> + previousCronDateTime.copy(hours = currentTime) + } + + dayOfMonth ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte -> + previousCronDateTime.copy(dayOfMonth = currentTime) + } + + month ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte -> + previousCronDateTime.copy(month = currentTime) + } + + return CronDateTimeScheduler(resultCronDateTimes.toList()) +} diff --git a/src/commonMain/kotlin/com/insanusmokrassar/krontab/internal/CronDateTimeScheduler.kt b/src/commonMain/kotlin/com/insanusmokrassar/krontab/internal/CronDateTimeScheduler.kt index b8595bb..8c96701 100644 --- a/src/commonMain/kotlin/com/insanusmokrassar/krontab/internal/CronDateTimeScheduler.kt +++ b/src/commonMain/kotlin/com/insanusmokrassar/krontab/internal/CronDateTimeScheduler.kt @@ -1,12 +1,30 @@ -package com.insanusmokrassar.krontab +package com.insanusmokrassar.krontab.internal -import com.insanusmokrassar.krontab.internal.CronDateTime -import com.insanusmokrassar.krontab.internal.toNearDateTime +import com.insanusmokrassar.krontab.KronScheduler +import com.insanusmokrassar.krontab.anyCronDateTime import com.soywiz.klock.DateTime +/** + * Cron-oriented realisation of [KronScheduler] + * + * @see com.insanusmokrassar.krontab.AnyTimeScheduler + * @see com.insanusmokrassar.krontab.EverySecondScheduler + * @see com.insanusmokrassar.krontab.EveryMinuteScheduler + * @see com.insanusmokrassar.krontab.EveryHourScheduler + * @see com.insanusmokrassar.krontab.EveryDayOfMonthScheduler + * @see com.insanusmokrassar.krontab.EveryMonthScheduler + * + * @see com.insanusmokrassar.krontab.builder.buildSchedule + * @see com.insanusmokrassar.krontab.builder.SchedulerBuilder + */ internal data class CronDateTimeScheduler internal constructor( internal val cronDateTimes: List ) : KronScheduler { + /** + * @return Near date using [cronDateTimes] list and getting the [Iterable.min] one + * + * @see toNearDateTime + */ override suspend fun next(relatively: DateTime): DateTime { return cronDateTimes.map { it.toNearDateTime(relatively) }.min() ?: anyCronDateTime.toNearDateTime(relatively) } diff --git a/src/commonMain/kotlin/com/insanusmokrassar/krontab/utils/Data.kt b/src/commonMain/kotlin/com/insanusmokrassar/krontab/utils/Data.kt index 305df46..5060708 100644 --- a/src/commonMain/kotlin/com/insanusmokrassar/krontab/utils/Data.kt +++ b/src/commonMain/kotlin/com/insanusmokrassar/krontab/utils/Data.kt @@ -1,4 +1,12 @@ package com.insanusmokrassar.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) diff --git a/src/commonTest/kotlin/com/insanusmokrassar/krontab/utils/StringParseTest.kt b/src/commonTest/kotlin/com/insanusmokrassar/krontab/utils/StringParseTest.kt new file mode 100644 index 0000000..406852a --- /dev/null +++ b/src/commonTest/kotlin/com/insanusmokrassar/krontab/utils/StringParseTest.kt @@ -0,0 +1,63 @@ +package com.insanusmokrassar.krontab.utils + +import com.insanusmokrassar.krontab.buildSchedule +import com.insanusmokrassar.krontab.builder.buildSchedule +import com.insanusmokrassar.krontab.createSimpleScheduler +import com.soywiz.klock.DateTime +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.takeWhile +import kotlin.test.Test +import kotlin.test.assertEquals + +@ExperimentalCoroutinesApi +@FlowPreview +class StringParseTest { + @Test + fun testThatFlowIsCorrectlyWorkEverySecondBuiltOnString() { + val kronScheduler = buildSchedule("*/1 * * * *") + + val flow = kronScheduler.asFlow() + + runTest { + val mustBeCollected = 10 + var collected = 0 + flow.takeWhile { + collected < mustBeCollected + }.collect { + collected++ + } + assertEquals(mustBeCollected, collected) + } + } + + @Test + fun testThatFlowIsCorrectlyWorkEverySecondWithMuchOfEmittersBuiltOnString() { + val kronScheduler = buildSchedule("*/1 * * * *") + + val flow = kronScheduler.asFlow() + + runTest { + val testsCount = 10 + val failJob = it.createFailJob((testsCount * 2) * 1000L) + val mustBeCollected = 10 + val answers = (0 until testsCount).map { _ -> + it.async { + var collected = 0 + flow.takeWhile { + collected < mustBeCollected + }.collect { + collected++ + } + collected + } + }.awaitAll() + + failJob.cancel() + + answers.forEach { + assertEquals(mustBeCollected, it) + } + } + } +}