rewrite system with offsets

This commit is contained in:
InsanusMokrassar 2021-04-10 00:28:08 +06:00
parent a07027399c
commit 5844859369
13 changed files with 174 additions and 86 deletions

View File

@ -13,22 +13,15 @@ import dev.inmo.krontab.internal.toNearDateTime
* @see dev.inmo.krontab.internal.CronDateTimeScheduler * @see dev.inmo.krontab.internal.CronDateTimeScheduler
*/ */
interface KronScheduler { interface KronScheduler {
/** /**
* @return Next [DateTime] when some action must be triggered according to settings of this instance * @return Next [DateTime] when some action must be triggered according to settings of this instance
* *
* @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 next(relatively: DateTimeTz): DateTimeTz?
} }
suspend fun KronScheduler.nextOrRelative(relatively: DateTime = DateTime.now()): DateTime = next(relatively) ?: getAnyNext(relatively) suspend fun KronScheduler.nextOrRelative(relatively: DateTime = DateTime.now()): DateTime = next(relatively) ?: getAnyNext(relatively)
suspend fun KronScheduler.nextOrRelative(relatively: DateTimeTz): DateTimeTz = next(relatively) ?: getAnyNext(relatively)
suspend fun KronScheduler.nextOrNow(): DateTime = DateTime.now().let { suspend fun KronScheduler.nextOrNow(): DateTime = DateTime.now().let {
next(it) ?: getAnyNext(it) next(it) ?: getAnyNext(it)
} }
suspend fun KronScheduler.nextOrNowWithOffset(): DateTimeTz = DateTimeTz.nowLocal().let {
next(it) ?: getAnyNext(it)
}

View File

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

View File

@ -1,8 +1,12 @@
package dev.inmo.krontab package dev.inmo.krontab
import com.soywiz.klock.TimezoneOffset
import dev.inmo.krontab.collection.CollectionKronScheduler import dev.inmo.krontab.collection.CollectionKronScheduler
import dev.inmo.krontab.collection.includeAll
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
import dev.inmo.krontab.internal.CronDateTimeSchedulerTz
/** /**
* 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 +14,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 +41,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

@ -11,7 +11,6 @@ internal val anyCronDateTime by lazy {
CronDateTime() CronDateTime()
} }
internal fun getAnyNext(relatively: DateTime) = anyCronDateTime.toNearDateTime(relatively)!! 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] * [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now]

View File

@ -1,6 +1,8 @@
package dev.inmo.krontab package dev.inmo.krontab
import com.soywiz.klock.*
import dev.inmo.krontab.internal.* import dev.inmo.krontab.internal.*
import dev.inmo.krontab.utils.Minutes
/** /**
* @see createSimpleScheduler * @see createSimpleScheduler
@ -52,9 +54,15 @@ typealias KrontabTemplate = String
* * "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" 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
* *
* @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(
incoming: KrontabTemplate
): KronScheduler {
var offsetParsed: Int? = null var offsetParsed: Int? = null
var yearParsed: Array<Int>? = null var yearParsed: Array<Int>? = null
val (secondsSource, minutesSource, hoursSource, dayOfMonthSource, monthSource) = incoming.split(" ").also { val (secondsSource, minutesSource, hoursSource, dayOfMonthSource, monthSource) = incoming.split(" ").also {
@ -81,22 +89,53 @@ fun createSimpleScheduler(incoming: KrontabTemplate): KronScheduler {
val dayOfMonthParsed = parseDaysOfMonth(dayOfMonthSource) val dayOfMonthParsed = parseDaysOfMonth(dayOfMonthSource)
val monthParsed = parseMonths(monthSource) val monthParsed = parseMonths(monthSource)
return createKronScheduler( return offsetParsed ?.let { offset ->
secondsParsed, minutesParsed, hoursParsed, dayOfMonthParsed, monthParsed, yearParsed, offsetParsed createKronSchedulerWithOffset(
secondsParsed, minutesParsed, hoursParsed, dayOfMonthParsed, monthParsed, yearParsed, TimezoneOffset(offset.minutes)
)
} ?: createKronScheduler(
secondsParsed, minutesParsed, hoursParsed, dayOfMonthParsed, monthParsed, yearParsed
) )
} }
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,9 +1,10 @@
package dev.inmo.krontab.builder package dev.inmo.krontab.builder
import com.soywiz.klock.TimeSpan import com.soywiz.klock.*
import com.soywiz.klock.TimezoneOffset
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 import dev.inmo.krontab.utils.Minutes
/** /**
@ -19,6 +20,22 @@ 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,
@ -26,7 +43,7 @@ class SchedulerBuilder(
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,
var offset: Minutes? = 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>?,
@ -110,32 +127,13 @@ class SchedulerBuilder(
) ?.toTypedArray() ) ?.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] * @return Completely built and independent [KronScheduler]
* *
* @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, TimezoneOffset(it.minutes))
} ?: createKronScheduler(seconds, minutes, hours, dayOfMonth, month, year)
} }

View File

@ -12,7 +12,7 @@ import dev.inmo.krontab.internal.toNearDateTime
*/ */
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())
/** /**
@ -51,6 +51,6 @@ data class CollectionKronScheduler internal constructor(
} }
override suspend fun next(relatively: DateTimeTz): DateTimeTz { override suspend fun next(relatively: DateTimeTz): DateTimeTz {
return schedulers.mapNotNull { it.next(relatively) }.minOrNull() ?: getAnyNext(relatively) return schedulers.mapNotNull { it.next(relatively) }.minOrNull() ?: getAnyNext(relatively.local).toOffsetUnadjusted(relatively.offset)
} }
} }

View File

@ -12,7 +12,6 @@ import dev.inmo.krontab.utils.Minutes
* @param seconds 0-59 * @param seconds 0-59
*/ */
internal data class CronDateTime( internal data class CronDateTime(
val offset: Int? = 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,
@ -30,7 +29,6 @@ internal data class CronDateTime(
} }
internal val klockDayOfMonth = dayOfMonth ?.plus(1) internal val klockDayOfMonth = dayOfMonth ?.plus(1)
internal val klockOffset = offset ?.minutes ?.offset
} }
/** /**
@ -82,35 +80,14 @@ internal fun CronDateTime.toNearDateTime(relativelyTo: DateTime = DateTime.now()
return current return current
} }
/** internal fun createCronDateTimeList(
* 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
*/
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
offset: Minutes? = null ): List<CronDateTime> {
): KronScheduler {
val resultCronDateTimes = mutableListOf(CronDateTime()) val resultCronDateTimes = mutableListOf(CronDateTime())
seconds ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte -> seconds ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
@ -141,9 +118,29 @@ internal fun createKronScheduler(
previousCronDateTime.copy(year = currentTime) previousCronDateTime.copy(year = currentTime)
} }
offset ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Int -> return resultCronDateTimes.toList()
previousCronDateTime.copy(year = currentTime)
}
return CronDateTimeScheduler(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
): KronScheduler = CronDateTimeScheduler(createCronDateTimeList(seconds, minutes, hours, dayOfMonth, month, years))
/**
* @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,
offset: TimezoneOffset
): KronScheduler = CronDateTimeSchedulerTz(createCronDateTimeList(seconds, minutes, hours, dayOfMonth, month, years), offset)

View File

@ -1,7 +1,6 @@
package dev.inmo.krontab.internal package dev.inmo.krontab.internal
import com.soywiz.klock.DateTime import com.soywiz.klock.*
import com.soywiz.klock.DateTimeTz
import dev.inmo.krontab.* import dev.inmo.krontab.*
import dev.inmo.krontab.collection.plus import dev.inmo.krontab.collection.plus
@ -30,10 +29,6 @@ internal data class CronDateTimeScheduler internal constructor(
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() ?: getAnyNext(relatively)
} }
override suspend fun next(relatively: DateTimeTz): DateTimeTz {
return cronDateTimes.mapNotNull { it.toNearDateTime(relatively) }.minOrNull() ?: getAnyNext(relatively)
}
} }
internal fun mergeCronDateTimeSchedulers(schedulers: List<CronDateTimeScheduler>) = CronDateTimeScheduler( internal fun mergeCronDateTimeSchedulers(schedulers: List<CronDateTimeScheduler>) = CronDateTimeScheduler(

View File

@ -0,0 +1,30 @@
package dev.inmo.krontab.internal
import com.soywiz.klock.*
import dev.inmo.krontab.*
import dev.inmo.krontab.collection.plus
/**
* 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.utc.toOffset(offset).local
return cronDateTimes.mapNotNull {
it.toNearDateTime(dateTimeWithActualOffset)
}.minOrNull() ?.toOffset(relatively.offset)
}
}
internal fun mergeCronDateTimeSchedulers(
schedulers: List<CronDateTimeSchedulerTz>
) = schedulers.groupBy {
it.offset
}.map { (offset, schedulers) ->
CronDateTimeSchedulerTz(schedulers.flatMap { it.cronDateTimes }, offset)
}

View File

@ -6,8 +6,3 @@ internal val dayOfMonthRange = 0 .. 30
internal val hoursRange = 0 .. 23 internal val hoursRange = 0 .. 23
internal val minutesRange = 0 .. 59 internal val minutesRange = 0 .. 59
internal val secondsRange = minutesRange 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())

View File

@ -1,6 +1,7 @@
package dev.inmo.krontab.utils package dev.inmo.krontab.utils
import com.soywiz.klock.DateTimeTz import com.soywiz.klock.DateTimeTz
import dev.inmo.krontab.KronSchedulerTz
import dev.inmo.krontab.buildSchedule import dev.inmo.krontab.buildSchedule
import dev.inmo.krontab.internal.offsetRange import dev.inmo.krontab.internal.offsetRange
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -84,9 +85,8 @@ class StringParseTest {
runTest { runTest {
for (i in offsetRange) { for (i in offsetRange) {
val kronScheduler = buildSchedule("* * 10 * * ${i}o") val kronScheduler = buildSchedule("* * 10 * * ${i}o") as KronSchedulerTz
val next = kronScheduler.next(now) val next = kronScheduler.next(now)
} }
} }
} }

View File

@ -2,6 +2,7 @@ package dev.inmo.krontab.utils
import com.soywiz.klock.* import com.soywiz.klock.*
import dev.inmo.krontab.builder.buildSchedule import dev.inmo.krontab.builder.buildSchedule
import dev.inmo.krontab.next
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals