#![cfg(any(feature = "chrono", feature = "jiff"))]
use croner::parser::CronParser;
use croner::Cron;
use std::str::FromStr;
fn custom_parse(pattern: &str, dom_and_dow: bool) -> Result<Cron, croner::errors::CronError> {
CronParser::builder()
.dom_and_dow(dom_and_dow)
.build()
.parse(pattern)
}
#[cfg(test)]
mod ocps_1_0_tests {
use super::*;
#[test]
fn test_5_field_baseline() {
assert!(
Cron::from_str("15 10 1 10 *").is_ok(),
"Should parse a 5-field pattern."
);
}
#[test]
fn test_special_chars_wildcard_list_range_step() {
assert!(
Cron::from_str("*/15 0-4,8-12 * JAN-MAR,DEC MON-FRI").is_ok(),
"Should handle *, /, -, and , correctly."
);
}
#[cfg(feature = "chrono")]
#[test]
fn chrono_test_logical_or_for_date_fields() {
use chrono::{Local, TimeZone};
let cron = Cron::from_str("0 12 1 * MON").unwrap();
let first_of_month = Local.with_ymd_and_hms(2025, 7, 1, 12, 0, 0).unwrap();
let a_monday = Local.with_ymd_and_hms(2025, 7, 14, 12, 0, 0).unwrap();
assert!(cron.is_time_matching(&first_of_month).unwrap());
assert!(cron.is_time_matching(&a_monday).unwrap());
}
#[cfg(feature = "jiff")]
#[test]
fn jiff_test_logical_or_for_date_fields() {
use jiff::civil::date;
let cron = Cron::from_str("0 12 1 * MON").unwrap();
let first_of_month: jiff::civil::DateTime = date(2025, 7, 1).at(12, 0, 0, 0);
let a_monday: jiff::civil::DateTime = date(2025, 7, 14).at(12, 0, 0, 0);
assert!(cron.is_time_matching(&first_of_month).unwrap());
assert!(cron.is_time_matching(&a_monday).unwrap());
}
}
#[cfg(test)]
mod ocps_1_1_tests {
use super::*;
#[test]
fn test_nicknames() {
assert_eq!(
custom_parse("@yearly", false)
.unwrap()
.pattern
.to_string()
.to_uppercase(),
"0 0 1 1 *"
);
assert_eq!(
custom_parse("@monthly", false)
.unwrap()
.pattern
.to_string()
.to_uppercase(),
"0 0 1 * *"
);
assert_eq!(
custom_parse("@weekly", false)
.unwrap()
.pattern
.to_string()
.to_uppercase(),
"0 0 * * 0"
);
assert_eq!(
custom_parse("@daily", false)
.unwrap()
.pattern
.to_string()
.to_uppercase(),
"0 0 * * *"
);
assert_eq!(
custom_parse("@hourly", false)
.unwrap()
.pattern
.to_string()
.to_uppercase(),
"0 * * * *"
);
}
#[test]
#[ignore] fn test_reboot_nickname() {
assert!(custom_parse("@reboot", false).is_ok());
}
}
#[cfg(test)]
mod ocps_1_2_tests {
use super::*;
#[test]
fn test_6_field_with_seconds() {
let cron = custom_parse("30 15 10 1 10 *", false).unwrap();
assert!(cron.pattern.seconds.is_bit_set(30, 1).unwrap());
assert!(!cron.pattern.seconds.is_bit_set(0, 1).unwrap());
}
#[test]
fn test_7_field_with_year() {
let cron = custom_parse("0 0 12 1 1 * 2025", false).unwrap();
assert!(cron.pattern.years.is_bit_set(2025, 1).unwrap());
}
}
#[cfg(test)]
mod ocps_1_3_tests {
use super::*;
#[cfg(feature = "chrono")]
mod chrono_tests {
use super::*;
use chrono::{Datelike, Local, TimeZone};
#[test]
fn test_last_day_of_month() {
let cron = Cron::from_str("0 0 L * *").unwrap();
let last_of_july = Local.with_ymd_and_hms(2025, 7, 31, 0, 0, 0).unwrap();
let not_last_of_july = Local.with_ymd_and_hms(2025, 7, 30, 0, 0, 0).unwrap();
assert!(cron.is_time_matching(&last_of_july).unwrap());
assert!(!cron.is_time_matching(¬_last_of_july).unwrap());
}
#[test]
fn test_last_weekday_of_month() {
let cron = Cron::from_str("0 0 * * 5L").unwrap();
let last_friday = Local.with_ymd_and_hms(2025, 7, 25, 0, 0, 0).unwrap();
let not_last_friday = Local.with_ymd_and_hms(2025, 7, 18, 0, 0, 0).unwrap();
assert!(cron.is_time_matching(&last_friday).unwrap());
assert!(!cron.is_time_matching(¬_last_friday).unwrap());
}
#[test]
fn test_nth_weekday_of_month() {
let cron = Cron::from_str("0 0 * * 2#2").unwrap();
let second_tuesday = Local.with_ymd_and_hms(2025, 7, 8, 0, 0, 0).unwrap();
let first_tuesday = Local.with_ymd_and_hms(2025, 7, 1, 0, 0, 0).unwrap();
assert!(cron.is_time_matching(&second_tuesday).unwrap());
assert!(!cron.is_time_matching(&first_tuesday).unwrap());
}
#[test]
fn test_closest_weekday() {
let cron = Cron::from_str("0 0 5W 7 *").unwrap();
let closest_weekday = Local.with_ymd_and_hms(2025, 7, 4, 0, 0, 0).unwrap();
assert!(cron.is_time_matching(&closest_weekday).unwrap());
}
#[test]
fn test_last_weekday_lw() {
let cron = Cron::from_str("0 0 LW * *").unwrap();
let feb_last_weekday = Local.with_ymd_and_hms(2025, 2, 28, 0, 0, 0).unwrap();
assert!(
cron.is_time_matching(&feb_last_weekday).unwrap(),
"Should match Feb 28 (Friday) as last weekday"
);
let aug_last_weekday = Local.with_ymd_and_hms(2025, 8, 29, 0, 0, 0).unwrap();
assert!(
cron.is_time_matching(&aug_last_weekday).unwrap(),
"Should match Aug 29 (Friday) as last weekday"
);
let aug_31 = Local.with_ymd_and_hms(2025, 8, 31, 0, 0, 0).unwrap();
assert!(
!cron.is_time_matching(&aug_31).unwrap(),
"Should NOT match Aug 31 (Sunday)"
);
let nov_last_weekday = Local.with_ymd_and_hms(2025, 11, 28, 0, 0, 0).unwrap();
assert!(
cron.is_time_matching(&nov_last_weekday).unwrap(),
"Should match Nov 28 (Friday) as last weekday"
);
let nov_30 = Local.with_ymd_and_hms(2025, 11, 30, 0, 0, 0).unwrap();
assert!(
!cron.is_time_matching(&nov_30).unwrap(),
"Should NOT match Nov 30 (Sunday)"
);
}
#[test]
fn test_31w_only_triggers_if_31st_exists() {
let cron = Cron::from_str("0 0 31W * *").unwrap();
let jan_31 = Local.with_ymd_and_hms(2025, 1, 31, 0, 0, 0).unwrap();
assert!(
cron.is_time_matching(&jan_31).unwrap(),
"Should match Jan 31 (Friday)"
);
let feb_28 = Local.with_ymd_and_hms(2025, 2, 28, 0, 0, 0).unwrap();
assert!(
!cron.is_time_matching(&feb_28).unwrap(),
"Should NOT match in February (no 31st)"
);
let apr_30 = Local.with_ymd_and_hms(2025, 4, 30, 0, 0, 0).unwrap();
assert!(
!cron.is_time_matching(&apr_30).unwrap(),
"Should NOT match in April (no 31st)"
);
let may_30 = Local.with_ymd_and_hms(2025, 5, 30, 0, 0, 0).unwrap();
assert!(
cron.is_time_matching(&may_30).unwrap(),
"Should match May 30 (Friday) for 31W"
);
let aug_29 = Local.with_ymd_and_hms(2025, 8, 29, 0, 0, 0).unwrap();
assert!(
cron.is_time_matching(&aug_29).unwrap(),
"Should match Aug 29 (Friday) for 31W"
);
}
#[test]
fn test_lw_find_next_occurrences() {
let cron = Cron::from_str("0 0 LW * *").expect("Failed to parse LW pattern");
let start_date = Local.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let expected = vec![
(2025, 1, 31), (2025, 2, 28), (2025, 3, 31), (2025, 4, 30), (2025, 5, 30), (2025, 6, 30), (2025, 7, 31), (2025, 8, 29), (2025, 9, 30), (2025, 10, 31), (2025, 11, 28), (2025, 12, 31), ];
let mut current = start_date;
for (i, (year, month, day)) in expected.iter().enumerate() {
match cron.find_next_occurrence(¤t, false) {
Ok(next) => {
assert_eq!(next.year(), *year, "Year mismatch at index {}", i);
assert_eq!(next.month(), *month, "Month mismatch at index {}", i);
assert_eq!(next.day(), *day, "Day mismatch at index {}", i);
current = next;
}
Err(e) => panic!("Error finding occurrence at index {}: {:?}", i, e),
}
}
} }
#[cfg(feature = "jiff")]
mod jiff_tests {
use super::*;
use jiff::civil::date;
#[test]
fn test_last_day_of_month() {
let cron = Cron::from_str("0 0 L * *").unwrap();
let last_of_july: jiff::civil::DateTime = date(2025, 7, 31).at(0, 0, 0, 0);
let not_last_of_july: jiff::civil::DateTime = date(2025, 7, 30).at(0, 0, 0, 0);
assert!(cron.is_time_matching(&last_of_july).unwrap());
assert!(!cron.is_time_matching(¬_last_of_july).unwrap());
}
#[test]
fn test_last_weekday_of_month() {
let cron = Cron::from_str("0 0 * * 5L").unwrap();
let last_friday: jiff::civil::DateTime = date(2025, 7, 25).at(0, 0, 0, 0);
let not_last_friday: jiff::civil::DateTime = date(2025, 7, 18).at(0, 0, 0, 0);
assert!(cron.is_time_matching(&last_friday).unwrap());
assert!(!cron.is_time_matching(¬_last_friday).unwrap());
}
#[test]
fn test_nth_weekday_of_month() {
let cron = Cron::from_str("0 0 * * 2#2").unwrap();
let second_tuesday: jiff::civil::DateTime = date(2025, 7, 8).at(0, 0, 0, 0);
let first_tuesday: jiff::civil::DateTime = date(2025, 7, 1).at(0, 0, 0, 0);
assert!(cron.is_time_matching(&second_tuesday).unwrap());
assert!(!cron.is_time_matching(&first_tuesday).unwrap());
}
#[test]
fn test_closest_weekday() {
let cron = Cron::from_str("0 0 5W 7 *").unwrap();
let closest_weekday: jiff::civil::DateTime = date(2025, 7, 4).at(0, 0, 0, 0);
assert!(cron.is_time_matching(&closest_weekday).unwrap());
}
#[test]
fn test_last_weekday_lw() {
let cron = Cron::from_str("0 0 LW * *").unwrap();
let feb_last: jiff::civil::DateTime = date(2025, 2, 28).at(0, 0, 0, 0);
assert!(cron.is_time_matching(&feb_last).unwrap(), "Feb 28 (Friday) as last weekday");
let aug_last: jiff::civil::DateTime = date(2025, 8, 29).at(0, 0, 0, 0);
assert!(cron.is_time_matching(&aug_last).unwrap(), "Aug 29 (Friday) as last weekday");
let aug_31: jiff::civil::DateTime = date(2025, 8, 31).at(0, 0, 0, 0);
assert!(!cron.is_time_matching(&aug_31).unwrap(), "Should NOT match Aug 31 (Sunday)");
let nov_last: jiff::civil::DateTime = date(2025, 11, 28).at(0, 0, 0, 0);
assert!(cron.is_time_matching(&nov_last).unwrap(), "Nov 28 (Friday) as last weekday");
let nov_30: jiff::civil::DateTime = date(2025, 11, 30).at(0, 0, 0, 0);
assert!(!cron.is_time_matching(&nov_30).unwrap(), "Should NOT match Nov 30 (Sunday)");
}
#[test]
fn test_31w_only_triggers_if_31st_exists() {
let cron = Cron::from_str("0 0 31W * *").unwrap();
let jan_31: jiff::civil::DateTime = date(2025, 1, 31).at(0, 0, 0, 0);
assert!(cron.is_time_matching(&jan_31).unwrap(), "Should match Jan 31 (Friday)");
let feb_28: jiff::civil::DateTime = date(2025, 2, 28).at(0, 0, 0, 0);
assert!(!cron.is_time_matching(&feb_28).unwrap(), "Should NOT match in February");
let apr_30: jiff::civil::DateTime = date(2025, 4, 30).at(0, 0, 0, 0);
assert!(!cron.is_time_matching(&apr_30).unwrap(), "Should NOT match in April");
let may_30: jiff::civil::DateTime = date(2025, 5, 30).at(0, 0, 0, 0);
assert!(cron.is_time_matching(&may_30).unwrap(), "Should match May 30 for 31W");
let aug_29: jiff::civil::DateTime = date(2025, 8, 29).at(0, 0, 0, 0);
assert!(cron.is_time_matching(&aug_29).unwrap(), "Should match Aug 29 for 31W");
}
#[test]
fn test_lw_find_next_occurrences() {
use jiff::civil::DateTime as CivilDateTime;
let cron = Cron::from_str("0 0 LW * *").expect("Failed to parse LW pattern");
let start_date: CivilDateTime = date(2025, 1, 1).at(0, 0, 0, 0);
let expected = vec![
(2025, 1, 31), (2025, 2, 28), (2025, 3, 31), (2025, 4, 30),
(2025, 5, 30), (2025, 6, 30), (2025, 7, 31), (2025, 8, 29),
(2025, 9, 30), (2025, 10, 31), (2025, 11, 28), (2025, 12, 31),
];
let mut current = start_date;
for (i, (year, month, day)) in expected.iter().enumerate() {
match cron.find_next_occurrence(¤t, false) {
Ok(next) => {
assert_eq!(next.year(), *year, "Year mismatch at index {i}");
assert_eq!(next.month(), *month, "Month mismatch at index {i}");
assert_eq!(next.day(), *day, "Day mismatch at index {i}");
current = next;
}
Err(e) => panic!("Error finding occurrence at index {i}: {e:?}"),
}
}
}
} }
#[cfg(test)]
mod ocps_1_4_tests {
use super::*;
#[test]
fn test_question_mark_is_alias_for_wildcard() {
let cron_star = Cron::from_str("0 0 1 * *").unwrap();
let cron_q = Cron::from_str("0 0 1 * ?").unwrap();
assert_eq!(cron_star, cron_q);
}
#[cfg(feature = "chrono")]
#[test]
fn chrono_test_and_modifier() {
use chrono::{Local, TimeZone};
let cron = custom_parse("0 12 1 * +MON", false).unwrap();
let first_is_monday = Local.with_ymd_and_hms(2025, 9, 1, 12, 0, 0).unwrap();
let first_is_not_monday = Local.with_ymd_and_hms(2025, 7, 1, 12, 0, 0).unwrap();
assert!(cron.is_time_matching(&first_is_monday).unwrap());
assert!(!cron.is_time_matching(&first_is_not_monday).unwrap());
}
#[cfg(feature = "jiff")]
#[test]
fn jiff_test_and_modifier() {
use jiff::civil::date;
let cron = custom_parse("0 12 1 * +MON", false).unwrap();
let first_is_monday: jiff::civil::DateTime = date(2025, 9, 1).at(12, 0, 0, 0);
let first_is_not_monday: jiff::civil::DateTime = date(2025, 7, 1).at(12, 0, 0, 0);
assert!(cron.is_time_matching(&first_is_monday).unwrap());
assert!(!cron.is_time_matching(&first_is_not_monday).unwrap());
}
#[cfg(feature = "chrono")]
#[test]
fn chrono_test_global_and_mode() {
use chrono::{Local, TimeZone};
let cron = custom_parse("0 12 1 * MON", true).unwrap();
let first_is_monday = Local.with_ymd_and_hms(2025, 9, 1, 12, 0, 0).unwrap();
let first_is_not_monday = Local.with_ymd_and_hms(2025, 7, 1, 12, 0, 0).unwrap();
let a_monday_not_first = Local.with_ymd_and_hms(2025, 7, 14, 12, 0, 0).unwrap();
assert!(cron.is_time_matching(&first_is_monday).unwrap());
assert!(!cron.is_time_matching(&first_is_not_monday).unwrap());
assert!(!cron.is_time_matching(&a_monday_not_first).unwrap());
}
#[cfg(feature = "jiff")]
#[test]
fn jiff_test_global_and_mode() {
use jiff::civil::date;
let cron = custom_parse("0 12 1 * MON", true).unwrap();
let first_is_monday: jiff::civil::DateTime = date(2025, 9, 1).at(12, 0, 0, 0);
let first_is_not_monday: jiff::civil::DateTime = date(2025, 7, 1).at(12, 0, 0, 0);
let a_monday_not_first: jiff::civil::DateTime = date(2025, 7, 14).at(12, 0, 0, 0);
assert!(cron.is_time_matching(&first_is_monday).unwrap());
assert!(!cron.is_time_matching(&first_is_not_monday).unwrap());
assert!(!cron.is_time_matching(&a_monday_not_first).unwrap());
}
#[test]
fn test_plus_modifier_invalid_field() {
let result = custom_parse("0 0 +1 * *", false);
assert!(matches!(
result,
Err(croner::errors::CronError::IllegalCharacters(_))
));
}
#[test]
fn test_invalid_step_syntax_single_number() {
let result = Cron::from_str("0/10 * * * *");
assert!(
result.is_err(),
"Pattern '0/10 * * * *' should be rejected (invalid step syntax)"
);
let result = Cron::from_str("30/10 * * * *");
assert!(
result.is_err(),
"Pattern '30/10 * * * *' should be rejected (invalid step syntax)"
);
let result = Cron::from_str("* 5/15 * * *");
assert!(
result.is_err(),
"Pattern '* 5/15 * * *' should be rejected (invalid step syntax)"
);
let result = Cron::from_str("* * 15/5 * *");
assert!(
result.is_err(),
"Pattern '* * 15/5 * *' should be rejected (invalid step syntax)"
);
let result = Cron::from_str("* * * 6/2 *");
assert!(
result.is_err(),
"Pattern '* * * 6/2 *' should be rejected (invalid step syntax)"
);
let result = Cron::from_str("* * * * 1/2");
assert!(
result.is_err(),
"Pattern '* * * * 1/2' should be rejected (invalid step syntax)"
);
let result = Cron::from_str("/10 * * * *");
assert!(
result.is_err(),
"Pattern '/10 * * * *' should be rejected (omitting starting point)"
);
let result = Cron::from_str("* /5 * * *");
assert!(
result.is_err(),
"Pattern '* /5 * * *' should be rejected (omitting starting point)"
);
}
#[test]
fn test_valid_step_syntax() {
assert!(
Cron::from_str("*/10 * * * *").is_ok(),
"Pattern '*/10 * * * *' should be accepted (valid wildcard step)"
);
assert!(
Cron::from_str("0-30/10 * * * *").is_ok(),
"Pattern '0-30/10 * * * *' should be accepted (valid range step)"
);
assert!(
Cron::from_str("10-50/5 * * * *").is_ok(),
"Pattern '10-50/5 * * * *' should be accepted (valid range step)"
);
}
#[test]
fn test_sloppy_ranges_enabled() {
let parser = CronParser::builder().sloppy_ranges(true).build();
let result = parser.parse("0/10 * * * *");
assert!(
result.is_ok(),
"Pattern '0/10 * * * *' should be accepted with sloppy_ranges enabled"
);
let result = parser.parse("30/10 * * * *");
assert!(
result.is_ok(),
"Pattern '30/10 * * * *' should be accepted with sloppy_ranges enabled"
);
let result = parser.parse("/10 * * * *");
assert!(
result.is_ok(),
"Pattern '/10 * * * *' should be accepted with sloppy_ranges enabled"
);
let result = parser.parse("* 5/15 * * *");
assert!(
result.is_ok(),
"Pattern '* 5/15 * * *' should be accepted with sloppy_ranges enabled"
);
let result = parser.parse("* * /5 * *");
assert!(
result.is_ok(),
"Pattern '* * /5 * *' should be accepted with sloppy_ranges enabled"
);
}
#[test]
fn test_sloppy_ranges_disabled_by_default() {
let parser = CronParser::new();
assert!(
parser.parse("0/10 * * * *").is_err(),
"Pattern '0/10 * * * *' should be rejected by default"
);
assert!(
parser.parse("/10 * * * *").is_err(),
"Pattern '/10 * * * *' should be rejected by default"
);
}
#[test]
fn test_sloppy_ranges_behavior() {
let parser = CronParser::builder().sloppy_ranges(true).build();
let cron = parser.parse("0/10 * * * *").unwrap();
assert!(cron.pattern.minutes.is_bit_set(0, 1).unwrap());
assert!(cron.pattern.minutes.is_bit_set(10, 1).unwrap());
assert!(cron.pattern.minutes.is_bit_set(20, 1).unwrap());
assert!(!cron.pattern.minutes.is_bit_set(5, 1).unwrap());
let cron = parser.parse("/10 * * * *").unwrap();
assert!(cron.pattern.minutes.is_bit_set(0, 1).unwrap());
assert!(cron.pattern.minutes.is_bit_set(10, 1).unwrap());
assert!(cron.pattern.minutes.is_bit_set(50, 1).unwrap());
}
}