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