///|
/// Cron expression parser.
/// Parses standard 5-field and extended 6-field (with seconds) cron expressions,
/// plus common aliases.
///|
/// Parse a full cron expression string into an array of CronField.
/// Supports 5-field format, 6-field format (seconds first), and named aliases.
pub fn parse_cron_expression(
raw : String,
) -> Result[Array[CronField], CronError] {
let expression = raw.trim().to_owned()
if expression == "" {
return Err({ kind: EmptyExpression, expression: "" })
}
// Try alias first
match resolve_alias(expression) {
Some(expanded) => parse_fields(expanded, expression)
None => parse_fields(expression, expression)
}
}
///|
/// Determine the field types based on the number of fields.
/// Returns the appropriate field type array.
fn field_types_for_count(count : Int) -> Array[FieldType]? {
match count {
5 => Some([Minute, Hour, DayOfMonth, Month, DayOfWeek])
6 => Some([Second, Minute, Hour, DayOfMonth, Month, DayOfWeek])
_ => None
}
}
///|
/// Parse a cron expression with 5 or 6 fields.
fn parse_fields(
expression : String,
original : String,
) -> Result[Array[CronField], CronError] {
let fields = split_cron_fields(expression)
let count = fields.length()
let field_types = match field_types_for_count(count) {
Some(types) => types
None => return Err({ kind: WrongFieldCount(count), expression: original })
}
let result : Array[CronField] = []
for i = 0; i < count; i = i + 1 {
let raw = fields[i].trim().to_owned()
if raw == "" {
return Err({
kind: InvalidFieldValue(field_key(field_types[i])),
expression: original,
})
}
match parse_field(field_types[i], raw) {
Some(field) => result.push(field)
None =>
return Err({
kind: InvalidFieldValue(field_key(field_types[i])),
expression: original,
})
}
}
Ok(result)
}
///|
/// Resolve a named alias to its standard 5-field form.
fn resolve_alias(raw : String) -> String? {
match raw {
"@hourly" => Some("0 * * * *")
"@daily" | "@midnight" => Some("0 0 * * *")
"@weekly" => Some("0 0 * * 0")
"@monthly" => Some("0 0 1 * *")
"@yearly" | "@annually" => Some("0 0 1 1 *")
_ => None
}
}
///|
/// Check if a parsed expression uses the 6-field (seconds) format.
pub fn is_six_field(fields : Array[CronField]) -> Bool {
fields.length() == 6
}
///|
/// Check if a parsed expression uses the 5-field (standard) format.
pub fn is_five_field(fields : Array[CronField]) -> Bool {
fields.length() == 5
}
///|
/// Split a cron expression into 5 whitespace-separated fields.
fn split_cron_fields(s : String) -> Array[String] {
let result : Array[String] = []
let mut start = 0
let mut i = 0
// Skip leading whitespace
while i < s.length() && is_space_code(s[i].to_int()) {
i = i + 1
}
start = i
while i < s.length() {
if is_space_code(s[i].to_int()) {
if start < i {
result.push(s[start:i].to_owned())
}
// Skip consecutive whitespace
while i < s.length() && is_space_code(s[i].to_int()) {
i = i + 1
}
start = i
} else {
i = i + 1
}
}
if start < s.length() {
result.push(s[start:s.length()].to_owned())
}
result
}
///|
/// Check if an integer character code is whitespace.
fn is_space_code(code : Int) -> Bool {
code == 32 || code == 9 || code == 13 || code == 10
}
///|
/// Get the list of known alias names.
pub fn alias_names() -> Array[String] {
[
"@hourly", "@daily", "@midnight", "@weekly", "@monthly", "@yearly", "@annually",
]
}
///|
/// Check if a string is a known alias.
pub fn is_alias(s : String) -> Bool {
match resolve_alias(s) {
Some(_) => true
None => false
}
}