Forwards & Backwards in Time
Matching is OK but it’s not exactly what Cron expressions are made for. They have been created to be able to calculate the following or previous moment in time to a given one. To see is in action, let’s start with our own basic imports:
import java.time._
import cron4s._
import cron4s.lib.javatime._
And an already parsed CRON expression:
val cron = Cron.unsafeParse("10-35 2,4,6 * ? * *")
// cron: CronExpr = CronExpr(10-35, 2,4,6, *, ?, *, *)
Now the next
operation is able to return to us the next moment in time according
to the CRON expression:
val now = LocalDateTime.of(2016, 12, 1, 0, 4, 34)
// now: LocalDateTime = 2016-12-01T00:04:34
cron.next(now)
// res0: Option[LocalDateTime] = Some(2016-12-01T00:04:35)
And of course, we can also get the previous moment in time to a given one:
cron.prev(now)
// res1: Option[LocalDateTime] = Some(2016-12-01T00:04:33)
Let’s try this with the sub-expressions too:
cron.datePart.next(now)
// res2: Option[LocalDateTime] = Some(2016-12-02T00:04:34)
cron.timePart.prev(now)
// res3: Option[LocalDateTime] = Some(2016-12-01T00:04:33)
If for some reason we do not want the next one, but the following to the next one,
then we could recursively invoke the next
operation in any subsequent generated
time; or we can get it more efficiently using the operation step
and telling it
how big is the step size that we want to make:
cron.step(now, 2)
// res4: Option[LocalDateTime] = Some(2016-12-01T00:06:10)
cron.timePart.step(now, 4)
// res5: Option[LocalDateTime] = Some(2016-12-01T00:06:12)
cron.datePart.step(now, -3)
// res6: Option[LocalDateTime] = None
Individual fields
The same type of operations are also available on the individual fields of the CRON expression:
cron.seconds.nextIn(now)
// res7: Option[LocalDateTime] = Some(2016-12-01T00:04:35)
cron.minutes.prevIn(now)
// res8: Option[LocalDateTime] = Some(2016-12-01T00:02:34)
Why so many Option[...]
?
As you must have noticed, all the methods that operate on date times have an Option[...]
return type. The reason for that type is to be able to express the fact that sometimes
you can not obtain a meaningful result out of the standard operations. Let’s see it in an
example:
val today = LocalDate.of(2017, 5, 12)
// today: LocalDate = 2017-05-12
cron.next(today)
// res9: Option[LocalDate] = None
So in this case the next
method returns None
instead of giving us the next datetime to
the given local date according to the cron expression. The reason for this is because
Cron4s can not give you a LocalDate
according the full cron expression (since it can’t
express the time values with it). The next
and prev
methods have been designed to reply
with the same type that has been given as a parameter (if possible), so in this case None
is being used to signal the fact that it’s not possible.
The are two different ways to workaround this, one of them is using a subexpression such that
we can get our desired LocalDate
:
cron.datePart.next(today)
// res10: Option[LocalDate] = Some(2017-05-13)
Now we get a LocalDate
but this may not still be what we want, since all the constrains
defined for the time fields are being ignored.
If what we are looking for is a LocalDateTime
relative to the LocalDate
we can easily get
one with the following code:
cron.next(today.atStartOfDay())
// res11: Option[LocalDateTime] = Some(2017-05-12T00:02:10)
The same applies when working with LocalTime
instances:
val before = LocalTime.of(0, 4, 34)
// before: LocalTime = 00:04:34
cron.next(before)
// res12: Option[LocalTime] = None
cron.timePart.next(before)
// res13: Option[LocalTime] = Some(00:04:35)
cron.next(before.atDate(today))
// res14: Option[LocalDateTime] = Some(2017-05-12T00:04:35)
And of course, same happens when dealing with the individual fields of the cron expression:
cron.seconds.nextIn(today)
// res15: Option[LocalDate] = None
cron.months.nextIn(today)
// res16: Option[LocalDate] = Some(2017-06-12)
cron.minutes.nextIn(before)
// res17: Option[LocalTime] = Some(00:06:34)
cron.daysOfMonth.nextIn(before)
// res18: Option[LocalTime] = None