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]
).