diff --git a/src/commonMain/kotlin/dev/inmo/krontab/KronScheduler.kt b/src/commonMain/kotlin/dev/inmo/krontab/KronScheduler.kt index dcd2eef..efd89ef 100644 --- a/src/commonMain/kotlin/dev/inmo/krontab/KronScheduler.kt +++ b/src/commonMain/kotlin/dev/inmo/krontab/KronScheduler.kt @@ -21,9 +21,7 @@ interface KronScheduler { */ suspend fun next(relatively: DateTime = DateTime.now()): DateTime? - suspend fun next(relatively: DateTimeTz): DateTimeTz? { - return next(relatively.local) ?.toOffsetUnadjusted(relatively.offset) - } + suspend fun next(relatively: DateTimeTz): DateTimeTz? } suspend fun KronScheduler.nextOrRelative(relatively: DateTime = DateTime.now()): DateTime = next(relatively) ?: getAnyNext(relatively) diff --git a/src/commonMain/kotlin/dev/inmo/krontab/SchedulerShortcuts.kt b/src/commonMain/kotlin/dev/inmo/krontab/SchedulerShortcuts.kt index 4b7945c..aa5500b 100644 --- a/src/commonMain/kotlin/dev/inmo/krontab/SchedulerShortcuts.kt +++ b/src/commonMain/kotlin/dev/inmo/krontab/SchedulerShortcuts.kt @@ -11,6 +11,7 @@ internal val anyCronDateTime by lazy { CronDateTime() } internal fun getAnyNext(relatively: DateTime) = anyCronDateTime.toNearDateTime(relatively)!! +internal fun getAnyNext(relatively: DateTimeTz) = anyCronDateTime.toNearDateTime(relatively)!! /** * [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] @@ -18,7 +19,6 @@ internal fun getAnyNext(relatively: DateTime) = anyCronDateTime.toNearDateTime(r val AnyTimeScheduler: KronScheduler by lazy { CronDateTimeScheduler(listOf(anyCronDateTime)) } -internal suspend fun getAnyNext(relatively: DateTimeTz) = AnyTimeScheduler.next(relatively)!! /** * [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one second diff --git a/src/commonMain/kotlin/dev/inmo/krontab/StringParser.kt b/src/commonMain/kotlin/dev/inmo/krontab/StringParser.kt index d41c5b4..0eb868f 100644 --- a/src/commonMain/kotlin/dev/inmo/krontab/StringParser.kt +++ b/src/commonMain/kotlin/dev/inmo/krontab/StringParser.kt @@ -17,6 +17,7 @@ typealias KrontabTemplate = String * * dayOfMonth * * month * * (optional) year + * * (optional) (can be placed before year) offset * * And each one have next format: * @@ -39,6 +40,7 @@ typealias KrontabTemplate = String * * Days of month ranges can be found in [dayOfMonthRange] * * Months ranges can be found in [monthRange] * * Years ranges can be found in [yearRange] (in fact - any [Int]) + * * Offset (timezone) ranges can be found in [offsetRange] * * Examples: * @@ -46,14 +48,31 @@ typealias KrontabTemplate = String * * "0/5,L * * * *" for every five seconds triggering and on 59 second * * "0/15 30 * * *" for every 15th seconds in a half of each hour * * "1 2 3 F,4,L 5" for triggering in near first second of second minute of third hour of fourth day of may + * * "1 2 3 F,4,L 5 60o" for triggering in near first second of second minute of third hour of fourth day of may with timezone UTC+01:00 * * "1 2 3 F,4,L 5 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 * * @see dev.inmo.krontab.internal.createKronScheduler */ fun createSimpleScheduler(incoming: KrontabTemplate): KronScheduler { - val yearSource: String? + var offsetParsed: Int? = null + var yearParsed: Array? = null val (secondsSource, minutesSource, hoursSource, dayOfMonthSource, monthSource) = incoming.split(" ").also { - yearSource = it.getOrNull(5) + listOfNotNull( + it.getOrNull(5), + it.getOrNull(6) + ).forEach { + val parsedOffset = parseOffset(it) + offsetParsed = offsetParsed ?: parsedOffset + when { + parsedOffset == null && yearParsed == null -> { + yearParsed = parseYears(it) + } + parsedOffset != null && offsetParsed == null -> { + offsetParsed = parsedOffset + } + } + } } val secondsParsed = parseSeconds(secondsSource) @@ -61,10 +80,9 @@ fun createSimpleScheduler(incoming: KrontabTemplate): KronScheduler { val hoursParsed = parseHours(hoursSource) val dayOfMonthParsed = parseDaysOfMonth(dayOfMonthSource) val monthParsed = parseMonths(monthSource) - val yearParsed = parseYears(yearSource) return createKronScheduler( - secondsParsed, minutesParsed, hoursParsed, dayOfMonthParsed, monthParsed, yearParsed + secondsParsed, minutesParsed, hoursParsed, dayOfMonthParsed, monthParsed, yearParsed, offsetParsed ) } diff --git a/src/commonMain/kotlin/dev/inmo/krontab/builder/SchedulerBuilder.kt b/src/commonMain/kotlin/dev/inmo/krontab/builder/SchedulerBuilder.kt index acc1334..2f477d0 100644 --- a/src/commonMain/kotlin/dev/inmo/krontab/builder/SchedulerBuilder.kt +++ b/src/commonMain/kotlin/dev/inmo/krontab/builder/SchedulerBuilder.kt @@ -1,7 +1,10 @@ package dev.inmo.krontab.builder +import com.soywiz.klock.TimeSpan +import com.soywiz.klock.TimezoneOffset import dev.inmo.krontab.KronScheduler import dev.inmo.krontab.internal.createKronScheduler +import dev.inmo.krontab.utils.Minutes /** * Will help to create an instance of [KronScheduler] @@ -22,7 +25,8 @@ class SchedulerBuilder( private var hours: Array? = null, private var dayOfMonth: Array? = null, private var month: Array? = null, - private var year: Array? = null + private var year: Array? = null, + var offset: Minutes? = null ) { private fun > callAndReturn( initial: Array?, @@ -106,6 +110,27 @@ class SchedulerBuilder( ) ?.toTypedArray() } + /** + * Setter of [offset] property + */ + fun offset(offset: Minutes?) { + this.offset = offset + } + + /** + * Setter of [offset] property + */ + fun offset(offset: TimeSpan?) { + this.offset = offset ?.minutes ?.toInt() + } + + /** + * Setter of [offset] property + */ + fun offset(offset: TimezoneOffset?) { + this.offset = offset ?.totalMinutesInt + } + /** * @return Completely built and independent [KronScheduler] * diff --git a/src/commonMain/kotlin/dev/inmo/krontab/collection/CollectionKronScheduler.kt b/src/commonMain/kotlin/dev/inmo/krontab/collection/CollectionKronScheduler.kt index 1f552b2..62fbf8c 100644 --- a/src/commonMain/kotlin/dev/inmo/krontab/collection/CollectionKronScheduler.kt +++ b/src/commonMain/kotlin/dev/inmo/krontab/collection/CollectionKronScheduler.kt @@ -1,6 +1,7 @@ package dev.inmo.krontab.collection import com.soywiz.klock.DateTime +import com.soywiz.klock.DateTimeTz import dev.inmo.krontab.* import dev.inmo.krontab.internal.* import dev.inmo.krontab.internal.CronDateTimeScheduler @@ -48,4 +49,8 @@ data class CollectionKronScheduler internal constructor( override suspend fun next(relatively: DateTime): DateTime { return schedulers.mapNotNull { it.next(relatively) }.minOrNull() ?: getAnyNext(relatively) } + + override suspend fun next(relatively: DateTimeTz): DateTimeTz { + return schedulers.mapNotNull { it.next(relatively) }.minOrNull() ?: getAnyNext(relatively) + } } diff --git a/src/commonMain/kotlin/dev/inmo/krontab/internal/CronDateTime.kt b/src/commonMain/kotlin/dev/inmo/krontab/internal/CronDateTime.kt index 269cd9b..4b1d312 100644 --- a/src/commonMain/kotlin/dev/inmo/krontab/internal/CronDateTime.kt +++ b/src/commonMain/kotlin/dev/inmo/krontab/internal/CronDateTime.kt @@ -1,8 +1,8 @@ package dev.inmo.krontab.internal -import com.soywiz.klock.DateTime -import com.soywiz.klock.DateTimeSpan +import com.soywiz.klock.* import dev.inmo.krontab.KronScheduler +import dev.inmo.krontab.utils.Minutes /** * @param month 0-11 @@ -12,6 +12,7 @@ import dev.inmo.krontab.KronScheduler * @param seconds 0-59 */ internal data class CronDateTime( + val offset: Int? = null, val year: Int? = null, val month: Byte? = null, val dayOfMonth: Byte? = null, @@ -29,9 +30,13 @@ internal data class CronDateTime( } internal val klockDayOfMonth = dayOfMonth ?.plus(1) + internal val klockOffset = offset ?.minutes ?.offset } /** + * THIS METHOD WILL NOT TAKE CARE ABOUT [offset] PARAMETER. It was decided due to the fact that we unable to get + * real timezone offset from simple [DateTime] + * * @return The near [DateTime] which happens after [relativelyTo] or will be equal to [relativelyTo] */ internal fun CronDateTime.toNearDateTime(relativelyTo: DateTime = DateTime.now()): DateTime? { @@ -77,6 +82,23 @@ internal fun CronDateTime.toNearDateTime(relativelyTo: DateTime = DateTime.now() return current } +/** + * THIS METHOD WILL TAKE CARE ABOUT [offset] PARAMETER. It was decided due to the fact that we unable to get + * real timezone offset from simple [DateTime] + * + * @return The near [DateTime] which happens after [relativelyTo] or will be equal to [relativelyTo] + */ +internal fun CronDateTime.toNearDateTime( + relativelyTo: DateTimeTz +): DateTimeTz? { + val klockOffset = klockOffset + return if (klockOffset != null) { + toNearDateTime(relativelyTo.toOffset(klockOffset).local) ?.toOffsetUnadjusted(klockOffset) ?.toOffset(relativelyTo.offset) + } else { + toNearDateTime(relativelyTo.local) ?.toOffsetUnadjusted(relativelyTo.offset) + } +} + /** * @return [KronScheduler] (in fact [CronDateTimeScheduler]) based on incoming data */ @@ -86,7 +108,8 @@ internal fun createKronScheduler( hours: Array? = null, dayOfMonth: Array? = null, month: Array? = null, - years: Array? = null + years: Array? = null, + offset: Minutes? = null ): KronScheduler { val resultCronDateTimes = mutableListOf(CronDateTime()) @@ -114,5 +137,13 @@ internal fun createKronScheduler( previousCronDateTime.copy(year = currentTime) } + years ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Int -> + previousCronDateTime.copy(year = currentTime) + } + + offset ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Int -> + previousCronDateTime.copy(year = currentTime) + } + return CronDateTimeScheduler(resultCronDateTimes.toList()) } diff --git a/src/commonMain/kotlin/dev/inmo/krontab/internal/CronDateTimeScheduler.kt b/src/commonMain/kotlin/dev/inmo/krontab/internal/CronDateTimeScheduler.kt index 23de605..ede3fca 100644 --- a/src/commonMain/kotlin/dev/inmo/krontab/internal/CronDateTimeScheduler.kt +++ b/src/commonMain/kotlin/dev/inmo/krontab/internal/CronDateTimeScheduler.kt @@ -1,6 +1,7 @@ package dev.inmo.krontab.internal import com.soywiz.klock.DateTime +import com.soywiz.klock.DateTimeTz import dev.inmo.krontab.* import dev.inmo.krontab.collection.plus @@ -29,6 +30,10 @@ internal data class CronDateTimeScheduler internal constructor( override suspend fun next(relatively: DateTime): DateTime { return cronDateTimes.mapNotNull { it.toNearDateTime(relatively) }.minOrNull() ?: getAnyNext(relatively) } + + override suspend fun next(relatively: DateTimeTz): DateTimeTz { + return cronDateTimes.mapNotNull { it.toNearDateTime(relatively) }.minOrNull() ?: getAnyNext(relatively) + } } internal fun mergeCronDateTimeSchedulers(schedulers: List) = CronDateTimeScheduler( diff --git a/src/commonMain/kotlin/dev/inmo/krontab/internal/Parser.kt b/src/commonMain/kotlin/dev/inmo/krontab/internal/Parser.kt index f2f8648..871a60f 100644 --- a/src/commonMain/kotlin/dev/inmo/krontab/internal/Parser.kt +++ b/src/commonMain/kotlin/dev/inmo/krontab/internal/Parser.kt @@ -38,6 +38,7 @@ private fun createSimpleScheduler(from: String, dataRange: IntRange, dataCon return results.map(dataConverter) } +internal fun parseOffset(from: String?) = from ?.let { if (it.endsWith("o")) it.removeSuffix("o").toIntOrNull() else null } internal fun parseYears(from: String?) = from ?.let { createSimpleScheduler(from, yearRange, intToIntConverter) ?.toTypedArray() } internal fun parseMonths(from: String) = createSimpleScheduler(from, monthRange, intToByteConverter) ?.toTypedArray() internal fun parseDaysOfMonth(from: String) = createSimpleScheduler(from, dayOfMonthRange, intToByteConverter) ?.toTypedArray() @@ -60,3 +61,16 @@ internal fun Array.fillWith( } } +internal fun T.fillWith( + whereToPut: MutableList, + createFactory: (CronDateTime, T) -> CronDateTime +) { + val previousValues = whereToPut.toList() + + whereToPut.clear() + + previousValues.forEach { previousValue -> + whereToPut.add(createFactory(previousValue, this)) + } +} + diff --git a/src/commonMain/kotlin/dev/inmo/krontab/internal/Ranges.kt b/src/commonMain/kotlin/dev/inmo/krontab/internal/Ranges.kt index ec4359b..232cf13 100644 --- a/src/commonMain/kotlin/dev/inmo/krontab/internal/Ranges.kt +++ b/src/commonMain/kotlin/dev/inmo/krontab/internal/Ranges.kt @@ -6,3 +6,8 @@ internal val dayOfMonthRange = 0 .. 30 internal val hoursRange = 0 .. 23 internal val minutesRange = 0 .. 59 internal val secondsRange = minutesRange + +/** + * From 0 - 1439 minutes (1440 == 24 hours, that is the same as 0 in terms of timezones) + */ +internal val offsetRange = 0 until (hoursRange.count() * minutesRange.count()) diff --git a/src/commonMain/kotlin/dev/inmo/krontab/utils/Typealiases.kt b/src/commonMain/kotlin/dev/inmo/krontab/utils/Typealiases.kt new file mode 100644 index 0000000..5cb9dc9 --- /dev/null +++ b/src/commonMain/kotlin/dev/inmo/krontab/utils/Typealiases.kt @@ -0,0 +1,3 @@ +package dev.inmo.krontab.utils + +typealias Minutes = Int diff --git a/src/commonTest/kotlin/dev/inmo/krontab/utils/StringParseTest.kt b/src/commonTest/kotlin/dev/inmo/krontab/utils/StringParseTest.kt index fe37fc2..fdc0146 100644 --- a/src/commonTest/kotlin/dev/inmo/krontab/utils/StringParseTest.kt +++ b/src/commonTest/kotlin/dev/inmo/krontab/utils/StringParseTest.kt @@ -1,6 +1,8 @@ package dev.inmo.krontab.utils +import com.soywiz.klock.DateTimeTz import dev.inmo.krontab.buildSchedule +import dev.inmo.krontab.internal.offsetRange import kotlinx.coroutines.* import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.takeWhile @@ -76,4 +78,16 @@ class StringParseTest { assertEquals(expectedCollects, collected) } } + @Test + fun testThatTimezoneCorrectlyDeserialized() { + val now = DateTimeTz.nowLocal() + + runTest { + for (i in offsetRange) { + val kronScheduler = buildSchedule("* * 10 * * ${i}o") + val next = kronScheduler.next(now) + + } + } + } }