Skip to content

Instantly share code, notes, and snippets.

@afsalthaj
Last active May 14, 2020 10:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save afsalthaj/ef760acf3fc964b32e01019532e0757a to your computer and use it in GitHub Desktop.
Save afsalthaj/ef760acf3fc964b32e01019532e0757a to your computer and use it in GitHub Desktop.
/** 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