///|
/// Next execution time calculator.
/// Given a cron expression and a reference time, compute the next matching time(s).
///|
/// Find the next execution time after a given reference time.
/// Returns None if no matching time exists within a reasonable search range.
pub fn next_after(fields : Array[CronField], after : DateTime) -> DateTime? {
next_matching_time(fields, after, after.year + 10)
}
///|
/// Find the next N execution times after a given reference time.
pub fn next_n(
fields : Array[CronField],
after : DateTime,
n : Int,
) -> Array[DateTime] {
let result : Array[DateTime] = []
let mut current = after
for i = 0; i < n; i = i + 1 {
match next_after(fields, current) {
Some(next_time) => {
result.push(next_time)
current = next_time
}
None => break
}
}
result
}
///|
/// Internal: find the next matching time using optimized field-based jumping.
fn next_matching_time(
fields : Array[CronField],
after : DateTime,
max_year : Int,
) -> DateTime? {
let has_seconds = fields.length() == 6
let mut current = if has_seconds {
add_second(after)
} else {
add_minute(after)
}
let mut iterations = 0
// Field-based jumps keep this bound practical even for sparse schedules.
let max_iterations = if has_seconds { 315360000 } else { 5256000 }
while current.year <= max_year && iterations < max_iterations {
iterations = iterations + 1
// Check if current time matches all fields
if matches_time(fields, current) {
return Some(current)
}
// Jump optimization: skip to next potentially matching time
current = skip_to_next_match(fields, current)
}
None
}
///|
/// Skip to the next possibly matching time.
/// This optimizes the search by jumping forward in larger increments
/// when a field doesn't match.
fn skip_to_next_match(fields : Array[CronField], dt : DateTime) -> DateTime {
let offset = if fields.length() == 6 { 1 } else { 0 }
// Check month
let month_field = fields[offset + 3]
if !value_in_field(month_field, dt.month) {
return skip_month(fields, offset, dt)
}
// Check day
let dom_field = fields[offset + 2]
let dow_field = fields[offset + 4]
let dom_ok = value_in_field_or_all(dom_field, dt.day)
let dow_ok = matches_dow(dow_field, dt)
let dom_restricted = !field_is_all(dom_field)
let dow_restricted = !field_is_all(dow_field)
let day_ok = if dom_restricted && dow_restricted {
dom_ok || dow_ok
} else if dom_restricted {
dom_ok
} else if dow_restricted {
dow_ok
} else {
true
}
if !day_ok {
return skip_day(dt)
}
// Check hour
let hour_field = fields[offset + 1]
if !value_in_field(hour_field, dt.hour) {
return skip_hour(dt)
}
// Check minute
let minute_field = fields[offset]
if !value_in_field(minute_field, dt.minute) {
return skip_minute(dt)
}
if offset == 1 {
let second_field = fields[0]
if !value_in_field(second_field, dt.second) {
return skip_second(dt)
}
add_second(dt)
} else {
add_minute(dt)
}
}
///|
/// Skip to the next matching month.
fn skip_month(
fields : Array[CronField],
offset : Int,
dt : DateTime,
) -> DateTime {
let month_field = fields[offset + 3]
match next_value_in_field(month_field, dt.month) {
Some(m) =>
if m <= 12 {
{ ..dt, month: m, day: 1, hour: 0, minute: 0, second: 0 }
} else {
// Next year
match next_value_in_field(month_field, 0) {
Some(nm) =>
{
year: dt.year + 1,
month: nm,
day: 1,
hour: 0,
minute: 0,
second: 0,
}
None =>
{
year: dt.year + 1,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
}
}
}
None =>
{ year: dt.year + 1, month: 1, day: 1, hour: 0, minute: 0, second: 0 }
}
}
///|
/// Skip to the next day.
fn skip_day(dt : DateTime) -> DateTime {
let dim = days_in_month(dt.year, dt.month)
if dt.day + 1 <= dim {
{ ..dt, day: dt.day + 1, hour: 0, minute: 0, second: 0 }
// Next month
} else if dt.month + 1 <= 12 {
{ ..dt, month: dt.month + 1, day: 1, hour: 0, minute: 0, second: 0 }
} else {
{ year: dt.year + 1, month: 1, day: 1, hour: 0, minute: 0, second: 0 }
}
}
///|
/// Skip to the next matching hour.
fn skip_hour(dt : DateTime) -> DateTime {
if dt.hour + 1 < 24 {
{ ..dt, hour: dt.hour + 1, minute: 0, second: 0 }
} else {
skip_day(dt)
}
}
///|
/// Skip to the next matching minute.
fn skip_minute(dt : DateTime) -> DateTime {
if dt.minute + 1 < 60 {
{ ..dt, minute: dt.minute + 1, second: 0 }
} else {
skip_hour(dt)
}
}
///|
/// Advance by one second after a seconds-field mismatch.
fn skip_second(dt : DateTime) -> DateTime {
if dt.second + 1 < 60 {
{ ..dt, second: dt.second + 1 }
} else {
skip_minute(dt)
}
}
///|
/// Check if a value is in a field's expanded values.
fn value_in_field(field : CronField, value : Int) -> Bool {
for i = 0; i < field.values.length(); i = i + 1 {
if field.values[i] == value {
return true
}
}
false
}
///|
/// Check if a value is in a field, or the field is "all values".
fn value_in_field_or_all(field : CronField, value : Int) -> Bool {
field_is_all(field) || value_in_field(field, value)
}
///|
/// Check if a field represents all values.
fn field_is_all(field : CronField) -> Bool {
let (min, max) = field_range(field.field_type)
if field.values.length() != max - min + 1 {
return false
}
let mut expected = min
for i = 0; i < field.values.length(); i = i + 1 {
if field.values[i] != expected {
return false
}
expected = expected + 1
}
true
}
///|
/// Find the next value in a field greater than the given value.
fn next_value_in_field(field : CronField, current : Int) -> Int? {
let mut found : Int? = None
for i = 0; i < field.values.length(); i = i + 1 {
if field.values[i] > current {
match found {
Some(f) =>
if field.values[i] < f {
found = Some(field.values[i])
} else {
()
}
None => found = Some(field.values[i])
}
}
}
found
}
///|
/// Check if a DateTime's day matches the day-of-week field.
fn matches_dow(field : CronField, dt : DateTime) -> Bool {
let dow = day_of_week(dt.year, dt.month, dt.day)
for i = 0; i < field.values.length(); i = i + 1 {
let v = field.values[i]
if v == dow || (v == 7 && dow == 0) {
return true
}
}
false
}
///|
/// Get all execution times in a date range (inclusive).
pub fn between(
fields : Array[CronField],
start : DateTime,
end : DateTime,
) -> Array[DateTime] {
let result : Array[DateTime] = []
let mut current = start
// If start matches, include it
if matches_time(fields, start) {
result.push(start)
}
while true {
match next_after(fields, current) {
Some(next) => {
if next.gt(end) {
break
}
result.push(next)
current = next
}
None => break
}
}
result
}
///|
/// Count how many times an expression fires in a date range.
pub fn count_between(
fields : Array[CronField],
start : DateTime,
end : DateTime,
) -> Int {
let mut count = 0
if matches_time(fields, start) {
count = count + 1
}
let mut current = start
while true {
match next_after(fields, current) {
Some(next) => {
if next.gt(end) {
break
}
count = count + 1
current = next
}
None => break
}
}
count
}
///|
/// Check if there are any execution times in a date range.
pub fn is_any_in_range(
fields : Array[CronField],
start : DateTime,
end : DateTime,
) -> Bool {
if matches_time(fields, start) {
return true
}
match next_after(fields, start) {
Some(next) => !next.gt(end)
None => false
}
}
///|
/// Find the previous execution time before a given reference time.
pub fn prev_before(fields : Array[CronField], before : DateTime) -> DateTime? {
// Search backwards by iterating forward from a much earlier point
// and finding the last execution before the reference time.
let mut search_start = DateTime::new(before.year - 2, 1, 1, 0, 0)
let mut last_match : DateTime? = None
while true {
match next_after(fields, search_start) {
Some(next) => {
if next.ge(before) {
// We've passed the reference time
break
}
last_match = Some(next)
search_start = next
}
None => break
}
}
last_match
}
///|
/// Estimate the next N execution times with a maximum search bound.
/// Returns fewer than N if not enough matches exist within the bound.
pub fn next_n_bounded(
fields : Array[CronField],
after : DateTime,
n : Int,
max_bound : DateTime,
) -> Array[DateTime] {
let result : Array[DateTime] = []
let mut current = after
for i = 0; i < n; i = i + 1 {
match next_after(fields, current) {
Some(next_time) => {
if next_time.gt(max_bound) {
break
}
result.push(next_time)
current = next_time
}
None => break
}
}
result
}