///|
/// Cron field representation and expansion.
/// Each of the 5 cron fields (minute, hour, day, month, weekday)
/// is parsed and expanded into a set of matching integer values.
///|
/// Cron field types. Supports both 5-field (standard) and 6-field (with seconds) formats.
pub enum FieldType {
Second // 0-59 (6-field format only)
Minute // 0-59
Hour // 0-23
DayOfMonth // 1-31
Month // 1-12
DayOfWeek // 0-7 (0 and 7 both = Sunday)
}
///|
/// A parsed and expanded cron field.
pub struct CronField {
field_type : FieldType
values : Array[Int] // sorted, deduplicated matching values
raw : String // original field text
}
///|
/// Get the allowed value range for a field type.
pub fn field_range(field_type : FieldType) -> (Int, Int) {
match field_type {
Second => (0, 59)
Minute => (0, 59)
Hour => (0, 23)
DayOfMonth => (1, 31)
Month => (1, 12)
DayOfWeek => (0, 7)
}
}
///|
/// Get the display name for a field type.
pub fn field_name(field_type : FieldType) -> String {
match field_type {
Second => "second"
Minute => "minute"
Hour => "hour"
DayOfMonth => "day of month"
Month => "month"
DayOfWeek => "day of week"
}
}
///|
/// Get a short key name for a field type (used in error messages).
pub fn field_key(field_type : FieldType) -> String {
match field_type {
Second => "second"
Minute => "minute"
Hour => "hour"
DayOfMonth => "day"
Month => "month"
DayOfWeek => "weekday"
}
}
///|
/// Parse a single cron field expression into a CronField.
/// Returns None if the field has invalid syntax or values.
pub fn parse_field(field_type : FieldType, raw : String) -> CronField? {
let (min, max) = field_range(field_type)
let expanded = expand_field(raw, min, max)
match expanded {
Some(vals) => {
let sorted = sort_and_dedup(vals)
Some({ field_type, values: sorted, raw })
}
None => None
}
}
///|
/// Expand a single field string into an array of matching integer values.
/// Supports: *, numbers, lists (1,3,5), ranges (1-5), steps (*/10, 1-10/2).
fn expand_field(raw : String, min : Int, max : Int) -> Array[Int]? {
if raw == "*" {
return Some(range(min, max, 1))
}
// Handle step syntax: value/step
if contains_char(raw, '/') {
return expand_step(raw, min, max)
}
// Handle list: a,b,c
if contains_char(raw, ',') {
return expand_list(raw, min, max)
}
// Handle range: a-b
if contains_char(raw, '-') {
return expand_range_item(raw, min, max)
}
// Simple number
match parse_int_safe(raw) {
Some(value) => Some([value])
None => None
}
}
///|
/// Expand a step expression (e.g., */5, 1-30/5, 5/10).
fn expand_step(raw : String, min : Int, max : Int) -> Array[Int]? {
let parts = split_once(raw, "/")
let left = parts.0
let right = parts.1
// Step must be positive
let step_opt = parse_int_safe(right)
match step_opt {
Some(step) => {
if step == 0 {
return None
}
if left == "*" {
return Some(range(min, max, step))
}
if contains_char(left, '-') {
let combined = left + "/" + right
return expand_range_item(combined, min, max)
}
// Single start value with step (e.g., 5/10 = 5,15,25,...)
match parse_int_safe(left) {
Some(start) => {
if start < min || start > max {
return None
}
let result : Array[Int] = []
let mut value = start
while value <= max {
result.push(value)
value = value + step
}
Some(result)
}
None => None
}
}
None => None
}
}
///|
/// Expand a list expression (e.g., 1,3,5 or 1-3,7-9).
fn expand_list(raw : String, min : Int, max : Int) -> Array[Int]? {
let items = split(raw, ",")
let result : Array[Int] = []
for i = 0; i < items.length(); i = i + 1 {
let item = items[i]
if item == "" {
return None
}
match expand_field(item, min, max) {
Some(expanded) =>
for j = 0; j < expanded.length(); j = j + 1 {
result.push(expanded[j])
}
None => return None
}
}
Some(result)
}
///|
/// Expand a range expression with optional step (e.g., 1-5, 1-30/5).
fn expand_range_item(raw : String, min : Int, max : Int) -> Array[Int]? {
let step = if contains_char(raw, '/') {
let parts = split_once(raw, "/")
match parse_int_safe(parts.1) {
Some(s) => {
if s == 0 {
return None
}
s
}
None => return None
}
} else {
1
}
let range_part = if contains_char(raw, '/') {
split_once(raw, "/").0
} else {
raw
}
let parts = split_once(range_part, "-")
if parts.0 == "" || parts.1 == "" {
return None
}
let start_opt = parse_int_safe(parts.0)
let end_opt = parse_int_safe(parts.1)
match (start_opt, end_opt) {
(Some(start), Some(end)) => {
if start < min || end > max || start > end {
return None
}
let result : Array[Int] = []
let mut value = start
while value <= end {
result.push(value)
value = value + step
}
Some(result)
}
_ => None
}
}
///|
/// Generate a range of values [min, max] with step.
fn range(min : Int, max : Int, step : Int) -> Array[Int] {
let result : Array[Int] = []
let mut value = min
while value <= max {
result.push(value)
value = value + step
}
result
}
///|
/// Sort and deduplicate an array of integers.
fn sort_and_dedup(arr : Array[Int]) -> Array[Int] {
if arr.length() <= 1 {
return arr
}
let sorted = sort_ints(arr)
let result : Array[Int] = []
let mut prev : Int? = None
for i = 0; i < sorted.length(); i = i + 1 {
let curr = sorted[i]
match prev {
Some(p) =>
if curr != p {
result.push(curr)
prev = Some(curr)
} else {
()
}
None => {
result.push(curr)
prev = Some(curr)
}
}
}
result
}
///|
/// Simple insertion sort for integers.
fn sort_ints(arr : Array[Int]) -> Array[Int] {
let result : Array[Int] = []
for i = 0; i < arr.length(); i = i + 1 {
result.push(arr[i])
}
for i = 1; i < result.length(); i = i + 1 {
let key = result[i]
let mut j = i
while j > 0 && result[j - 1] > key {
result[j] = result[j - 1]
j = j - 1
}
result[j] = key
}
result
}
///|
/// Split a string by a delimiter character.
fn split(s : String, delim : String) -> Array[String] {
let result : Array[String] = []
let mut start = 0
let mut i = 0
let delim_code = delim[0].to_int()
while i < s.length() {
if s[i].to_int() == delim_code {
result.push(s[start:i].to_owned())
start = i + 1
}
i = i + 1
}
result.push(s[start:s.length()].to_owned())
result
}
///|
/// Split a string on the first occurrence of a delimiter.
fn split_once(s : String, delim : String) -> (String, String) {
let delim_code = delim[0].to_int()
let mut i = 0
while i < s.length() {
if s[i].to_int() == delim_code {
return (s[0:i].to_owned(), s[i + 1:s.length()].to_owned())
}
i = i + 1
}
(s, "")
}
///|
/// Check if a string contains a specific character.
fn contains_char(s : String, ch : Char) -> Bool {
let code = ch.to_int()
for i = 0; i < s.length(); i = i + 1 {
if s[i].to_int() == code {
return true
}
}
false
}
///|
/// Parse a string into an integer, returning None on failure.
fn parse_int_safe(s : String) -> Int? {
if s == "" {
return None
}
let mut value = 0
for i = 0; i < s.length(); i = i + 1 {
let code = s[i].to_int()
if code < 48 || code > 57 {
return None
}
value = value * 10 + (code - 48)
}
Some(value)
}