/// Cron expressions: what they mean, when they fire, and when they never fire.
///
/// The expression language is Vixie cron's, the one every scheduler has copied.
/// This file reads expressions, asks whether an instant matches one, and walks
/// forward to the next few firings.
///
/// moon run src/examples/schedule
///|
/// The next `count` firings after `from`, one per line. `next` always returns a
/// moment strictly after the one you give it, so feeding the answer back in is
/// how you walk forward.
fn list_firings(expr : String, from : String, count : Int32) -> Unit raise {
let cron = @cron.parse(expr)
let mut at = @moondate.parse(from)
let mut n = 0
while n < count {
match cron.next(at) {
Some(fire) => {
println(" " + fire.to_string())
at = fire
n = n + 1
}
None => {
// An expression can name a day that never comes, such as 30 February.
// There is no next firing, and saying so is better than looping forever.
println(" never")
n = count
}
}
}
}
///|
fn main raise {
let start = "2026-09-22T14:30:00Z" // a Tuesday afternoon
// ---- the five fields ----------------------------------------------------
//
// minute, hour, day of month, month, day of week. Five fields, left to right.
// `*` is "every", a number is itself, `a-b` is a range, `a,b` is a list, and
// `*/n` steps through the field.
//
// 0 9 * * MON-FRI at 09:00, Monday through Friday
// │ │ │ │ └───────── day of week, names allowed
// │ │ │ └─────────── month, 1-12 or JAN-DEC
// │ │ └───────────── day of month, 1-31
// │ └─────────────── hour, 0-23
// └───────────────── minute, 0-59
println("0 9 * * MON-FRI")
list_firings("0 9 * * MON-FRI", start, 5)
println("")
// A range with a step: every fifteen minutes, on the hour and after.
println("*/15 * * * *")
list_firings("*/15 * * * *", start, 6)
println("")
// Lists are written with commas and can mix numbers with names.
println("0 0 1 JAN,JUL *")
list_firings("0 0 1 JAN,JUL *", start, 3)
println("")
// ---- the seconds field --------------------------------------------------
//
// Six fields means the first one is seconds, which is what makes a schedule
// that fires more than once a minute expressible at all.
println("*/20 * * * * *")
list_firings("*/20 * * * * *", start, 5)
println("")
// ---- the awkward rule ---------------------------------------------------
//
// When both day fields are restricted, Vixie cron fires when *either* matches,
// not when both do. The thirteenth of the month, or any Friday — which is how
// "Friday the 13th" is usually meant, and not how it reads.
println("0 0 13 * FRI")
list_firings("0 0 13 * FRI", start, 6)
println("")
// Star one of them and the other is the only condition, as you would expect.
println("0 0 * * FRI")
list_firings("0 0 * * FRI", start, 3)
println("")
// ---- matching -----------------------------------------------------------
//
// `matches` answers the question for one instant, which is what a scheduler
// running inside an event loop wants: it can wake up every minute and ask
// rather than compute.
let hourly = @cron.parse("0 * * * *")
let noon = @moondate.parse("2026-09-22T12:00:00Z")
let half_past = @moondate.parse("2026-09-22T12:30:00Z")
println("at 12:00 " + hourly.matches(noon).to_string())
println("at 12:30 " + hourly.matches(half_past).to_string())
println("")
// ---- refusals -----------------------------------------------------------
//
// Four fields, or 61 minutes, or a month called FOO: each one is refused with
// the field and the range that failed.
let bad = ["0 9 * *", "61 * * * *", "0 0 * FOO *"]
for expr in bad {
try {
let _ = @cron.parse(expr)
println(expr + " -> accepted")
} catch {
err => println(expr + " -> " + err.to_string())
}
}
}