Last active
May 14, 2020 10:29
-
-
Save afsalthaj/ef760acf3fc964b32e01019532e0757a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** Usage: | |
{{{ | |
val expr = CronExpr.mk("15 9 * * *") | |
expr flatMap (_.durationToNextExecution(ZonedDateTime.now())) | |
expr flatMap (_.durationFromPreviousExecution(ZonedDateTime.now()) | |
expr flatMap (_.nextExecutionTimeFrom(ZonedDateTime.now()) | |
expr flatMap (_.previousExecutionTimeFrom(ZonedDateTime.now()) | |
expr flatMap (_.travel(numberOfSteps = 10, ZonedDateTime.now(), Direction.Forward)) | |
expr flatMap (_.travel(numberOfSteps = 10, ZonedDateTime.now(), Direction.Backward)) | |
}}} | |
*/ | |
import com.cronutils.model._ | |
import com.cronutils.model.definition._ | |
import java.time._ | |
import com.cronutils.parser.CronParser | |
import com.cronutils.model.time.ExecutionTime | |
import CronExpr._ | |
import java.util.Optional | |
import CronError._ | |
import CronExpr.Direction, Direction._ | |
import scala.util.Try | |
/** | |
* A very simple scala wrapper over cron. | |
* Every other libraries in scala is either buggy or doesn't have step logic. | |
*/ | |
class CronExpr(val cron: Cron) { | |
private val executionTime: Either[CronError, ExecutionTime] = | |
Try(ExecutionTime.forCron(cron)).toEither.swap.map(FailedExecutionTimeForCron.apply).swap | |
def travel(numberOfSteps: Int, currentTime: ZonedDateTime, direction: Direction): Either[CronError, ZonedDateTime] = | |
(0 until numberOfSteps).foldLeft(Right[CronError, ZonedDateTime](currentTime): Either[CronError, ZonedDateTime]) { | |
(localTime, _) => | |
for { | |
now <- localTime | |
exec <- executionTime | |
r <- Try(exec.nextStep(now, direction)).toEither.swap.map(t => StepException(t, now, direction)).swap | |
time <- Try(r.get()).toEither.swap.map(t => StepException(new RuntimeException(t.getMessage), now, direction)).swap | |
} yield time | |
} | |
private def timeToNextStep(now: ZonedDateTime, direction: Direction): Either[CronError, Duration] = { | |
val fn: ExecutionTime => Optional[Duration] = | |
t => direction match { | |
case Direction.Forward => t.timeToNextExecution(now) | |
case Direction.Backward => t.timeFromLastExecution(now) | |
} | |
executionTime.flatMap(t => { | |
val result = fn(t) | |
if (result.isPresent) Right(result.get()) else Left(InvalidDuration(now)) | |
}) | |
} | |
def nextExecutionTimeFrom(now: ZonedDateTime): Either[CronError, ZonedDateTime] = | |
travel(1, now, Direction.Forward) | |
def previousExecutionTimeFrom(now: ZonedDateTime): Either[CronError, ZonedDateTime] = | |
for { | |
time <- executionTime | |
res <- if (time.isMatch(now)) Right(now) else travel(1, now, Direction.Backward) | |
} yield res | |
def durationFromPreviousExecution(now: ZonedDateTime): Either[CronError, Duration] = | |
for { | |
time <- executionTime | |
res <- if (time.isMatch(now)) Right(java.time.Duration.of(0, ChronoUnit.MILLIS)) else timeToNextStep(now, Backward) | |
} yield res | |
def durationToNextExecution(now: ZonedDateTime): Either[CronError, Duration] = | |
timeToNextStep(now, Forward) | |
} | |
object CronExpr extends ExecutionTimeSyntax { | |
import CronError._ | |
val standardCron: Either[FailedCronDef, CronDefinition] = | |
Try( | |
CronDefinitionBuilder | |
.defineCron() | |
.withMinutes() | |
.and() | |
.withHours() | |
.and() | |
.withDayOfMonth() | |
.supportsL() | |
.supportsW() | |
.supportsLW() | |
.supportsQuestionMark() | |
.and() | |
.withMonth() | |
.and() | |
.withDayOfWeek() | |
.withValidRange(1, 7) | |
.withMondayDoWValue(2) | |
.supportsHash() | |
.supportsL() | |
.supportsQuestionMark() | |
.and() | |
.instance() | |
).toEither.swap.map(FailedCronDef.apply).swap // coz there are some casts inside java lib - unless you are sure they are safe. | |
def mk(str: String): Either[CronError, CronExpr] = | |
standardCron.flatMap(t => Try(new CronExpr(new CronParser(t).parse(str))).toEither.swap.map(CronParseException(str, _)).swap) | |
sealed trait Direction | |
object Direction { | |
case object Forward extends Direction | |
case object Backward extends Direction | |
} | |
abstract sealed class CronError private (val underlying: Throwable) { self => | |
def throwable: RuntimeException = self match { | |
case FailedCronDef(t) => new RuntimeException("FailedCronDef", t) | |
case CronParseException(input, t) => new RuntimeException(s"Failed to parse cron expression $input", t) | |
case FailedExecutionTimeForCron(t) => new RuntimeException("Failed to get execution time from cronj", t) | |
case StepException(t, currentTime, direction) => new RuntimeException(s"Cron Step Error: Failed to calculate step from $currentTime in ${direction} direction", t) | |
case InvalidDuration(now) => new RuntimeException(s"CronError: Failed to get duration from previous execution time to current time ${now}") | |
} | |
} | |
object CronError { | |
final case class FailedCronDef(t: Throwable) extends CronError(t) | |
final case class CronParseException(input: String, t: Throwable) extends CronError(t) | |
final case class FailedExecutionTimeForCron(t: Throwable) extends CronError(t) | |
final case class StepException(t: Throwable, currentTime: ZonedDateTime, direction: Direction) extends CronError(t) | |
final case class InvalidDuration(now: ZonedDateTime) extends CronError(new RuntimeException(s"Invalid duration compared to current time ${now.toString}")) | |
} | |
} | |
trait ExecutionTimeSyntax { | |
implicit class ExecutionTimeOps(exec: ExecutionTime) { | |
def nextStep(currentTime: ZonedDateTime, direction: Direction): Optional[ZonedDateTime] = | |
direction match { | |
case Direction.Forward => | |
exec.nextExecution(currentTime) | |
case Direction.Backward => | |
exec.lastExecution(currentTime) | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment