The Cron4s AST

After successfully parsing a CRON expression, the CronExpr resulting type represents the previously parsed expression as an AST, in which we can access all the expression fields individually. Let’s start by declaring a cron expression:

val cron = Cron.unsafeParse("10-35 2,4,6 * ? * *")
// cron: CronExpr = CronExpr(10-35, 2,4,6, *, ?, *, *)

And now with that out of the way, we can access the different components of the AST using the field accessors:

cron.seconds
// res0: expr.package.SecondsNode = 10-35
cron.minutes
// res1: expr.package.MinutesNode = 2,4,6
cron.months
// res2: expr.package.MonthsNode = *

We can also take the date or time parts only of the expression using either timePart or datePart:

val time = cron.timePart
// time: expr.TimeCronExpr = TimeCronExpr(10-35, 2,4,6, *)
val date = cron.datePart
// date: expr.DateCronExpr = DateCronExpr(?, *, *)

And similarly as with the main expression type, we can get individual fields out of the date and time sub expressions:

time.seconds
// res3: expr.package.SecondsNode = 10-35
time.minutes
// res4: expr.package.MinutesNode = 2,4,6

date.daysOfMonth
// res5: expr.package.DaysOfMonthNode = ?

Or by means of the field method in CronExpr and passing the cron field type.

cron.field[CronField.Minute]
// res6: expr.FieldSelector.MinutesFromCronExpr.Out[CronField.Minute] = 2,4,6

Some other basic operations at the CronExpr level are asking for the list of supported fields of the actual value ranges for all the fields in the form of a map:

cron.supportedFields
// res7: List[CronField] = List(
//   Second,
//   Minute,
//   Hour,
//   DayOfMonth,
//   Month,
//   DayOfWeek
// )
cron.ranges
// res8: Map[CronField, IndexedSeq[Int]] = HashMap(
//   Second -> Range(
//     10,
//     11,
//     12,
//     13,
//     14,
//     15,
//     16,
//     17,
//     18,
//     19,
//     20,
//     21,
//     22,
//     23,
//     24,
//     25,
//     26,
//     27,
//     28,
//     29,
//     30,
//     31,
//     32,
//     33,
//     34,
//     35
//   ),
//   DayOfWeek -> Range(0, 1, 2, 3, 4, 5, 6),
//   Hour -> Range(
//     0,
//     1,
//     2,
//     3,
//     4,
//     5,
//     6,
//     7,
//     8,
//     9,
//     10,
//     11,
//     12,
//     13,
//     14,
//     15,
//     16,
//     17,
// ...

To convert an AST back into the original string expression we simply use the toString method:

cron.toString
// res9: String = "10-35 2,4,6 * ? * *"

Sub-expressions

All the operations possible on a CronExpr are also possible in any of its subexpressions (either time or date) so you can use them in exactly the same way. For example:

cron.supportedFields
// res10: List[CronField] = List(
//   Second,
//   Minute,
//   Hour,
//   DayOfMonth,
//   Month,
//   DayOfWeek
// )
cron.timePart.supportedFields
// res11: List[CronField] = List(Second, Minute, Hour)
cron.datePart.supportedFields
// res12: List[CronField] = List(DayOfMonth, Month, DayOfWeek)

supportedFields is not super-interesting at CronExpr (we expect it to support all the fields anyway) but when is part of the sub-expressions gives us a more particular piece of information about the actual expression itself. The field method is also interesting, it can return the field node expression given a specific field type:

cron.field[CronField.DayOfMonth]
// res13: expr.FieldSelector.DayOfMonthFromCronExpr.Out[CronField.DayOfMonth] = ?
cron.timePart.field[CronField.Hour]
// res14: expr.FieldSelector.HoursFromTimeExpr.Out[CronField.Hour] = *
cron.datePart.field[CronField.DayOfWeek]
// res15: expr.FieldSelector.DayOfWeekFromDateExpr.Out[CronField.DayOfWeek] = *

It’s important to note that when we pass a field type that is not supported by the given expression, we get a compile error:

cron.timePart.field[CronField.DayOfMonth]
// error: Field cron4s.CronField.DayOfMonth is not a member of expression cron4s.expr.TimeCronExpr
// cron.timePart.field[CronField.DayOfMonth]
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

This is just a teaser, we will see much more interesting operations on cron expressions later but it’s good to know that all operations possible on a CronExpr, are also possible on it’s subexpressions.

Field nodes

All field nodes have their own type, which is parameterized in the actual field type they operate on. We can access that field type definition via the unit of field expression:

cron.seconds.unit.field
// res17: CronField.Second = Second

The expression unit can be used to give us information about what values are valid for that specific field:

cron.seconds.unit.range
// res18: IndexedSeq[Int] = Range(
//   0,
//   1,
//   2,
//   3,
//   4,
//   5,
//   6,
//   7,
//   8,
//   9,
//   10,
//   11,
//   12,
//   13,
//   14,
//   15,
//   16,
//   17,
//   18,
//   19,
//   20,
//   21,
//   22,
//   23,
//   24,
//   25,
//   26,
//   27,
//   28,
//   29,
//   30,
//   31,
//   32,
//   33,
//   34,
//   35,
//   36,
//   37,
//   38,
//   39,
//   40,
//   41,
//   42,
//   43,
//   44,
//   45,
//   46,
//   47,
// ...

Which is different than the range of values accepted by the expression at that given field:

cron.seconds.range
// res19: IndexedSeq[Int] = Range(
//   10,
//   11,
//   12,
//   13,
//   14,
//   15,
//   16,
//   17,
//   18,
//   19,
//   20,
//   21,
//   22,
//   23,
//   24,
//   25,
//   26,
//   27,
//   28,
//   29,
//   30,
//   31,
//   32,
//   33,
//   34,
//   35
// )

We can also obtain a field expression

To obtain the string representation of individual fields we use the same toString method:

cron.seconds.toString
// res20: String = "10-35"
cron.field[CronField.Minute].toString
// res21: String = "2,4,6"

Other interesting operations are the ones that can be used to test if a given value matches the field expression:

cron.seconds.matches(5)
// res22: Boolean = false
cron.seconds.matches(15)
// res23: Boolean = true
cron.minutes.matches(4)
// res24: Boolean = true
cron.minutes.matches(5)
// res25: Boolean = false

Or to test if a given field expression is implied by another one (that is also parameterized by the same field type). To show this, let’s work with some simple field expressions:

import cron4s.expr._

val eachSecond = EachNode[CronField.Second]
// eachSecond: EachNode[CronField.Second] = *
val fixedSecond = ConstNode[CronField.Second](30)
// fixedSecond: ConstNode[CronField.Second] = 30

fixedSecond.implies(eachSecond)
// res26: Boolean = false
fixedSecond.impliedBy(eachSecond)
// res27: Boolean = true
eachSecond.implies(fixedSecond)
// res28: Boolean = true

val minutesRange = BetweenNode[CronField.Minute](ConstNode(2), ConstNode(10))
// minutesRange: BetweenNode[CronField.Minute] = 2-10
val fixedMinute = ConstNode[CronField.Minute](7)
// fixedMinute: ConstNode[CronField.Minute] = 7

fixedMinute.implies(minutesRange)
// res29: Boolean = false
fixedMinute.impliedBy(minutesRange)
// res30: Boolean = true

These two operations allways hold the following property (it looks obvious, but it’s important):

assert(minutesRange.implies(fixedMinute) == fixedMinute.impliedBy(minutesRange))

It’s important to notice that when using either the implies or impliedBy operation, if the two nodes are not parameterized by the same field type, the code won’t compile:

minutesRange.implies(eachSecond)
// error: no type parameters for method implies: (ee: EE[cron4s.CronField.Minute])(implicit EE: cron4s.expr.FieldExpr[EE,cron4s.CronField.Minute]): Boolean exist so that it can be applied to arguments (cron4s.expr.EachNode[cron4s.CronField.Second])
//  --- because ---
// argument expression's type is not compatible with formal parameter type;
//  found   : cron4s.expr.EachNode[cron4s.CronField.Second]
//  required: ?0EE[cron4s.CronField.Minute]
// minutesRange.implies(eachSecond)
// ^^^^^^^^^^^^^^^^^^^^
// error: type mismatch;
//  found   : cron4s.expr.EachNode[cron4s.CronField.Second]
//  required: EE[cron4s.CronField.Minute]
// minutesRange.implies(eachSecond)
//                      ^^^^^^^^^^
// error: could not find implicit value for parameter EE: cron4s.expr.FieldExpr[EE,cron4s.CronField.Minute]
// minutesRange.implies(eachSecond)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The error looks a bit scary, but in essence is saying to us that the implies method was expecting any kind of expression as long as it was for the Minute field (expressed as EE[cron4s.CronField.Minute]).