///|
priv enum FieldKind {
Numeric
Month
Weekday
}
///|
pub fn parse(cron_str : String) -> Result[CronExpr, String] {
let parts = split_spaces(cron_str)
if parts.length() != 5 {
return Err("Invalid cron expression: must have exactly 5 fields")
}
let minute = match parse_field(parts[0], 0, 59, Numeric) {
Ok(v) => v
Err(e) => return Err("Failed to parse minute: \{e}")
}
let hour = match parse_field(parts[1], 0, 23, Numeric) {
Ok(v) => v
Err(e) => return Err("Failed to parse hour: \{e}")
}
let day_of_month = match parse_field(parts[2], 1, 31, Numeric) {
Ok(v) => v
Err(e) => return Err("Failed to parse day_of_month: \{e}")
}
let month = match parse_field(parts[3], 1, 12, Month) {
Ok(v) => v
Err(e) => return Err("Failed to parse month: \{e}")
}
let day_of_week = match parse_field(parts[4], 0, 6, Weekday) {
Ok(v) => v
Err(e) => return Err("Failed to parse day_of_week: \{e}")
}
Ok({ minute, hour, day_of_month, month, day_of_week })
}
///|
fn split_spaces(s : String) -> Array[String] {
let res = []
let mut start = -1
for i = 0; i < s.length(); i = i + 1 {
let c = s[i]
if c == ' ' || c == '\t' {
if start != -1 {
res.push(s[start:i].to_owned())
start = -1
}
} else if start == -1 {
start = i
}
}
if start != -1 {
res.push(s[start:s.length()].to_owned())
}
res
}
///|
fn split_comma(s : String) -> Array[String] {
let res = []
let mut start = 0
for i = 0; i < s.length(); i = i + 1 {
let c = s[i]
if c == ',' {
res.push(s[start:i].to_owned())
start = i + 1
}
}
res.push(s[start:s.length()].to_owned())
res
}
///|
fn parse_number(s : String, kind : FieldKind) -> Result[Int, String] {
if s.length() == 0 {
return Err("Empty numeric value")
}
match named_value(s, kind) {
Some(value) => return Ok(value)
None => ()
}
let mut num = 0
for i = 0; i < s.length(); i = i + 1 {
let c = s[i]
if c >= '0' && c <= '9' {
num = num * 10 + (c.to_int() - '0'.to_int())
if num > 1000 {
return Err("Numeric value is too large")
}
} else {
return Err("Invalid numeric value: \{s}")
}
}
Ok(num)
}
///|
fn named_value(s : String, kind : FieldKind) -> Int? {
match kind {
Numeric => None
Month =>
match s {
"JAN" => Some(1)
"FEB" => Some(2)
"MAR" => Some(3)
"APR" => Some(4)
"MAY" => Some(5)
"JUN" => Some(6)
"JUL" => Some(7)
"AUG" => Some(8)
"SEP" => Some(9)
"OCT" => Some(10)
"NOV" => Some(11)
"DEC" => Some(12)
_ => None
}
Weekday =>
match s {
"SUN" => Some(0)
"MON" => Some(1)
"TUE" => Some(2)
"WED" => Some(3)
"THU" => Some(4)
"FRI" => Some(5)
"SAT" => Some(6)
_ => None
}
}
}
///|
fn parse_field(
s : String,
min : Int,
max : Int,
kind : FieldKind,
) -> Result[CronField, String] {
let mut has_comma = false
for i = 0; i < s.length(); i = i + 1 {
if s[i] == ',' {
has_comma = true
break
}
}
if has_comma {
let parts = split_comma(s)
let list = []
for i = 0; i < parts.length(); i = i + 1 {
let part = parts[i]
let field = match parse_single_item(part, min, max, kind) {
Ok(v) => v
Err(e) => return Err(e)
}
list.push(field)
}
return Ok(List(list))
}
parse_single_item(s, min, max, kind)
}
///|
fn parse_single_item(
s : String,
min : Int,
max : Int,
kind : FieldKind,
) -> Result[CronField, String] {
// Check for step '/'
let mut slash_idx = -1
for i = 0; i < s.length(); i = i + 1 {
if s[i] == '/' {
slash_idx = i
break
}
}
if slash_idx != -1 {
let base_str = s[0:slash_idx].to_owned()
let step_str = s[slash_idx + 1:s.length()].to_owned()
let step_num = match parse_number(step_str, Numeric) {
Ok(v) => v
Err(e) => return Err(e)
}
let base_field = match parse_single_item(base_str, min, max, kind) {
Ok(v) => v
Err(e) => return Err(e)
}
if step_num <= 0 {
return Err("Invalid step size: must be > 0")
}
return Ok(Step(base_field, step_num))
}
if s == "*" {
return Ok(All)
}
// Check for range '-'
let mut dash_idx = -1
for i = 0; i < s.length(); i = i + 1 {
if s[i] == '-' {
dash_idx = i
break
}
}
if dash_idx != -1 {
let start_str = s[0:dash_idx].to_owned()
let end_str = s[dash_idx + 1:s.length()].to_owned()
let start_num = match parse_number(start_str, kind) {
Ok(v) => v
Err(e) => return Err(e)
}
let end_num = match parse_number(end_str, kind) {
Ok(v) => v
Err(e) => return Err(e)
}
if start_num < min || start_num > max || end_num < min || end_num > max {
return Err("Range value out of bounds: expected \{min}-\{max}")
}
if start_num > end_num {
return Err("Invalid range: start > end")
}
return Ok(Range(start_num, end_num))
}
// Try to parse as integer
let num = match parse_number(s, kind) {
Ok(v) => v
Err(e) => return Err(e)
}
if num < min || num > max {
return Err("Value out of bounds: expected \{min}-\{max}")
}
Ok(Value(num))
}