Skip to content

Instantly share code, notes, and snippets.

@afsalthaj
Last active April 28, 2020 02:25
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/c3c99ea13174da1fb3453a85ec53f815 to your computer and use it in GitHub Desktop.
Save afsalthaj/c3c99ea13174da1fb3453a85ec53f815 to your computer and use it in GitHub Desktop.
import com.cronutils.model._
import com.cronutils.model.definition._
import java.time._
import com.cronutils.parser.CronParser
import com.cronutils.model.time.ExecutionTime
import CronOps._
import StepDirection._
import java.util.Optional
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.
*/
trait CronOps {
val standardCron: Either[FailedCronDef, CronDefinition] =
Try(
CronDefinitionBuilder
.defineCron()
.withSeconds()
.and()
.withMinutes()
.and()
.withHours()
.and()
.withDayOfMonth()
.supportsL()
.supportsW()
.supportsLW()
.supportsQuestionMark()
.and()
.withMonth()
.and()
.withDayOfWeek()
.withValidRange(1, 7)
.withMondayDoWValue(2)
.supportsHash()
.supportsL()
.supportsQuestionMark()
.and()
.withYear()
.withValidRange(1970, 2099)
.optional()
.and()
.withCronValidation(
CronConstraintsFactory.ensureEitherDayOfWeekOrDayOfMonth()
)
.instance()
).toEither.swap.map(FailedCronDef.apply).swap // coz there are some casts inside java lib - unless you are sure they are safe.
private def parseCron(string: String): Either[CronOpError, Cron] =
standardCron.flatMap(t => Try(new CronParser(t).parse(string)).toEither.swap.map(CronParseException.apply).swap)
private def executionTime(string: String): Either[CronOpError, ExecutionTime] =
parseCron(string.toUpperCase()).flatMap(cron => Try(ExecutionTime.forCron(cron)).toEither.swap.map(FailedExecutionTimeForCron.apply).swap)
private def oneStep(t: ExecutionTime, now: LocalDateTime, dir: StepDirection): Either[CronOpError, LocalDateTime] =
for {
r <- Try(dir.nextTime(t)(now)).toEither.swap.map(t => StepException(t, now, dir)).swap
v <- Try(r.get()).toEither.swap.map(t => StepException(new RuntimeException(t.getMessage), now, dir)).swap
} yield v.toLocalDateTime
// A better type driven step calculation.
def step(t: ExecutionTime, now: LocalDateTime, dir: StepDirection): Either[CronOpError, LocalDateTime] = {
(0 to dir.step).foldLeft(Right(now): Either[CronOpError, LocalDateTime]) {
(acc, a) => acc.flatMap(l => oneStep(t, l, dir))
}
}
}
object CronOps extends CronOps {
abstract sealed class CronOpError(val underlying: Throwable)
final case class FailedCronDef(t: Throwable) extends CronOpError(t)
final case class CronParseException(t: Throwable) extends CronOpError(t)
final case class FailedExecutionTimeForCron(t: Throwable) extends CronOpError(t)
final case class StepException(t: Throwable, currentTime: LocalDateTime, direction: StepDirection) extends CronOpError(t)
abstract sealed class StepDirection(val step: Int) {
override def toString = this match {
case Forward(n) => s"step forward by ${n}"
case Backward(n) => s"step backward by ${n}"
}
val nextTime: ExecutionTime => LocalDateTime => Optional[ZonedDateTime] =
a => b => a.nextExecution(ZonedDateTime.of(b, ZoneId.systemDefault()))
}
object StepDirection {
final case class Forward(n: Int) extends StepDirection(n)
final case class Backward(n: Int) extends StepDirection(n)
}
}
////////////////
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment