use crate::{
pattern::{Pattern, PatternItem, PatternType, PatternValueType},
utils, CronError, Result,
};
use chrono::{offset::LocalResult, DateTime, Datelike, TimeDelta, TimeZone, Timelike};
#[cfg(feature = "tz")]
use chrono_tz::Tz;
use std::{fmt::Display, str::FromStr};
pub const MIN_YEAR: u16 = 1970;
pub const MAX_YEAR: u16 = 2099;
pub(crate) const MIN_YEAR_STR: &str = "1970";
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "String"))]
#[cfg_attr(feature = "serde", serde(into = "String"))]
pub struct Schedule {
year: Pattern,
month: Pattern,
dom: Pattern,
dow: Pattern,
hour: Pattern,
minute: Pattern,
second: Pattern,
#[cfg(feature = "tz")]
tz: Option<Tz>,
}
impl Schedule {
pub fn new(pattern: impl Into<String>) -> Result<Self> {
let pattern = pattern.into();
let mut elements: Vec<&str> = pattern.split_whitespace().collect();
#[cfg(feature = "tz")]
let mut tz = None;
#[cfg(feature = "tz")]
if elements.len() >= 2 {
let tz_elements: Vec<&str> = elements[0].split('=').collect();
if tz_elements.len() == 2 && tz_elements[0].to_uppercase() == "TZ" {
let tz_str = tz_elements[1];
if let Ok(tz_value) = Tz::from_str(tz_str) {
tz = Some(tz_value);
elements.remove(0);
} else {
return Err(CronError::InvalidTimeZone(tz_str.to_string()));
}
}
}
if elements.len() == 1 {
match elements[0] {
"@yearly" | "@annually" => elements = vec!["0", "0", "0", "1", "1", "?", "*"],
"@monthly" => elements = vec!["0", "0", "0", "1", "*", "?", "*"],
"@weekly" => elements = vec!["0", "0", "0", "?", "*", "0", "*"],
"@daily" | "@midnight" => elements = vec!["0", "0", "0", "*", "*", "*", "*"],
"@hourly" => elements = vec!["0", "0", "*", "*", "*", "*", "*"],
_ => return Err(CronError::InvalidCronSchedule(pattern)),
}
} else if elements.len() == 5 {
elements.insert(0, "0");
elements.insert(6, "*");
} else if elements.len() == 6 {
elements.insert(6, "*");
} else if elements.len() != 7 {
return Err(CronError::InvalidCronSchedule(pattern));
}
let schedule = Self {
second: Pattern::parse(PatternType::Seconds, elements[0])?,
minute: Pattern::parse(PatternType::Minutes, elements[1])?,
hour: Pattern::parse(PatternType::Hours, elements[2])?,
dom: Pattern::parse(PatternType::Doms, elements[3])?,
month: Pattern::parse(PatternType::Months, elements[4])?,
dow: Pattern::parse(PatternType::Dows, elements[5])?,
year: Pattern::parse(PatternType::Years, elements[6])?,
#[cfg(feature = "tz")]
tz,
};
match (schedule.dom.pattern(), schedule.dow.pattern()) {
(PatternItem::Any, PatternItem::Any) => return Err(CronError::InvalidDaysPattern(pattern)),
(PatternItem::All, _) | (_, PatternItem::All) | (PatternItem::Any, _) | (_, PatternItem::Any) => {}
(_, _) => {
return Err(CronError::InvalidDaysPattern(pattern));
}
}
Ok(schedule)
}
#[cfg(not(feature = "tz"))]
#[inline]
pub fn upcoming<T: TimeZone>(&self, current: &DateTime<T>) -> Option<DateTime<T>> {
self.upcoming_impl(current)
}
#[cfg(feature = "tz")]
pub fn upcoming<Tz: TimeZone>(&self, current: &DateTime<Tz>) -> Option<DateTime<Tz>> {
if let Some(schedule_tz) = &self.tz {
let current_tz = current.timezone();
let current = current.with_timezone(schedule_tz);
let result = self.upcoming_impl(¤t);
result.map(|dt| dt.with_timezone(¤t_tz))
} else {
self.upcoming_impl(current)
}
}
fn upcoming_impl<Tz: TimeZone>(&self, current: &DateTime<Tz>) -> Option<DateTime<Tz>> {
let mut current = if current.nanosecond() > 0 {
current
.with_nanosecond(0)
.unwrap()
.checked_add_signed(TimeDelta::seconds(1))
.unwrap()
} else {
current.clone()
};
let mut year = Some(current.year() as PatternValueType);
let mut month = Some(current.month() as PatternValueType);
let mut dom = Some(current.day() as PatternValueType);
let mut hour = Some(current.hour() as PatternValueType);
let mut minute = Some(current.minute() as PatternValueType);
let mut second = Some(current.second() as PatternValueType);
let mut first_iteration = true;
while year.is_none()
|| month.is_none()
|| dom.is_none()
|| hour.is_none()
|| minute.is_none()
|| second.is_none()
|| first_iteration
{
first_iteration = false;
if year.is_none() {
return None;
} else if month.is_none() {
inc_year(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second)?;
} else if dom.is_none() {
inc_month(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second)?;
} else if hour.is_none() {
inc_dom(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second)?;
} else if minute.is_none() {
inc_hour(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second)?;
} else if second.is_none() {
inc_minute(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second)?;
}
current = match current.timezone().with_ymd_and_hms(
year? as i32,
month? as u32,
dom? as u32,
hour? as u32,
minute? as u32,
second? as u32,
) {
LocalResult::Single(updated_current) => updated_current,
LocalResult::Ambiguous(earliest, _latest) => earliest,
LocalResult::None => {
minute = None;
continue;
}
};
year = self.year.next(&mut current);
if year.is_some() {
month = self.month.next(&mut current);
year = Some(current.year() as PatternValueType);
if month.is_some() {
dom = match (self.dom.pattern(), self.dow.pattern()) {
(PatternItem::All, PatternItem::All) => self.dom.next(&mut current),
(PatternItem::All, PatternItem::Any) => self.dom.next(&mut current),
(PatternItem::All, _) => self.dow.next(&mut current),
(PatternItem::Any, PatternItem::All) => self.dow.next(&mut current),
(PatternItem::Any, PatternItem::Any) => unreachable!(),
(PatternItem::Any, _) => self.dow.next(&mut current),
(_, PatternItem::All) => self.dom.next(&mut current),
(_, PatternItem::Any) => self.dom.next(&mut current),
(_, _) => unreachable!(),
};
year = Some(current.year() as PatternValueType);
month = Some(current.month() as PatternValueType);
if dom.is_some() {
hour = self.hour.next(&mut current);
year = Some(current.year() as PatternValueType);
month = Some(current.month() as PatternValueType);
dom = Some(current.day() as PatternValueType);
if hour.is_some() {
minute = self.minute.next(&mut current);
year = Some(current.year() as PatternValueType);
month = Some(current.month() as PatternValueType);
dom = Some(current.day() as PatternValueType);
hour = Some(current.hour() as PatternValueType);
if minute.is_some() {
second = self.second.next(&mut current);
year = Some(current.year() as PatternValueType);
month = Some(current.month() as PatternValueType);
dom = Some(current.day() as PatternValueType);
hour = Some(current.hour() as PatternValueType);
minute = Some(current.minute() as PatternValueType);
}
}
}
}
}
}
match current.timezone().with_ymd_and_hms(
year? as i32,
month? as u32,
dom? as u32,
hour? as u32,
minute? as u32,
second? as u32,
) {
LocalResult::Single(current) => Some(current),
LocalResult::Ambiguous(earliest, _latest) => Some(earliest),
LocalResult::None => None,
}
}
#[inline]
pub fn iter<Tz: TimeZone>(&self, current: &DateTime<Tz>) -> impl Iterator<Item = DateTime<Tz>> {
ScheduleIterator {
schedule: self.clone(),
next: self.upcoming(current),
}
}
#[inline]
pub fn into_iter<Tz: TimeZone>(self, current: &DateTime<Tz>) -> impl Iterator<Item = DateTime<Tz>> {
let next = self.upcoming(current);
ScheduleIterator { schedule: self, next }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct ScheduleIterator<Tz: TimeZone> {
pub(crate) schedule: Schedule,
pub(crate) next: Option<DateTime<Tz>>,
}
impl<Tz: TimeZone> Iterator for ScheduleIterator<Tz> {
type Item = DateTime<Tz>;
fn next(&mut self) -> Option<Self::Item> {
let current = self.next.take()?;
self.next = self
.schedule
.upcoming(¤t.clone().checked_add_signed(TimeDelta::seconds(1))?);
Some(current)
}
}
impl From<Schedule> for String {
fn from(value: Schedule) -> Self {
value.to_string()
}
}
impl From<&Schedule> for String {
fn from(value: &Schedule) -> Self {
value.to_string()
}
}
impl TryFrom<String> for Schedule {
type Error = CronError;
fn try_from(value: String) -> Result<Self> {
Self::new(value)
}
}
impl TryFrom<&String> for Schedule {
type Error = CronError;
fn try_from(value: &String) -> Result<Self> {
Self::new(value)
}
}
impl TryFrom<&str> for Schedule {
type Error = CronError;
fn try_from(value: &str) -> Result<Self> {
Self::new(value)
}
}
impl FromStr for Schedule {
type Err = CronError;
fn from_str(s: &str) -> Result<Self> {
Self::new(s)
}
}
impl Display for Schedule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
#[cfg(not(feature = "tz"))]
{
write!(
f,
"{} {} {} {} {} {} {}",
self.second, self.minute, self.hour, self.dom, self.month, self.dow, self.year
)
}
#[cfg(feature = "tz")]
if let Some(tz) = self.tz {
write!(
f,
"TZ={} {} {} {} {} {} {} {}",
tz, self.second, self.minute, self.hour, self.dom, self.month, self.dow, self.year
)
} else {
write!(
f,
"{} {} {} {} {} {} {}",
self.second, self.minute, self.hour, self.dom, self.month, self.dow, self.year
)
}
}
}
#[inline]
fn inc_year(
year: &mut Option<PatternValueType>,
month: &mut Option<PatternValueType>,
dom: &mut Option<PatternValueType>,
hour: &mut Option<PatternValueType>,
minute: &mut Option<PatternValueType>,
second: &mut Option<PatternValueType>,
) -> Option<PatternValueType> {
if (*year)? < MAX_YEAR {
*year = Some((*year)? + 1);
*month = Some(1);
*dom = Some(1);
*hour = Some(0);
*minute = Some(0);
*second = Some(0);
*year
} else {
*year = None;
None
}
}
#[inline]
fn inc_month(
year: &mut Option<PatternValueType>,
month: &mut Option<PatternValueType>,
dom: &mut Option<PatternValueType>,
hour: &mut Option<PatternValueType>,
minute: &mut Option<PatternValueType>,
second: &mut Option<PatternValueType>,
) -> Option<PatternValueType> {
if (*month)? < 12 {
*month = Some((*month)? + 1);
*dom = Some(1);
*hour = Some(0);
*minute = Some(0);
*second = Some(0);
*month
} else {
inc_year(year, month, dom, hour, minute, second)?;
*month
}
}
#[inline]
fn inc_dom(
year: &mut Option<PatternValueType>,
month: &mut Option<PatternValueType>,
dom: &mut Option<PatternValueType>,
hour: &mut Option<PatternValueType>,
minute: &mut Option<PatternValueType>,
second: &mut Option<PatternValueType>,
) -> Option<PatternValueType> {
if (*dom)? < utils::days_in_month((*year)?, (*month)?) {
*dom = Some((*dom)? + 1);
*hour = Some(0);
*minute = Some(0);
*second = Some(0);
*dom
} else {
inc_month(year, month, dom, hour, minute, second)?;
*dom
}
}
#[inline]
fn inc_hour(
year: &mut Option<PatternValueType>,
month: &mut Option<PatternValueType>,
dom: &mut Option<PatternValueType>,
hour: &mut Option<PatternValueType>,
minute: &mut Option<PatternValueType>,
second: &mut Option<PatternValueType>,
) -> Option<PatternValueType> {
if (*hour)? < 23 {
*hour = Some((*hour)? + 1);
*minute = Some(0);
*second = Some(0);
*hour
} else {
inc_dom(year, month, dom, hour, minute, second)?;
*hour
}
}
#[inline]
fn inc_minute(
year: &mut Option<PatternValueType>,
month: &mut Option<PatternValueType>,
dom: &mut Option<PatternValueType>,
hour: &mut Option<PatternValueType>,
minute: &mut Option<PatternValueType>,
second: &mut Option<PatternValueType>,
) -> Option<PatternValueType> {
if (*minute)? < 59 {
*minute = Some((*minute)? + 1);
*second = Some(0);
*minute
} else {
inc_hour(year, month, dom, hour, minute, second)?;
*minute
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::DateTime;
use rstest::rstest;
use rstest_reuse::{apply, template};
use std::time::Duration;
#[rstest]
#[case("* 0 0 1 1 *", "2024-01-01T00:00:21Z", "2024-01-01T00:00:21+00:00")]
#[case("* 0 0 1 1 *", "2024-01-01T01:00:25Z", "2025-01-01T00:00:00+00:00")]
#[case("*/5 * * * * *", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00+00:00")]
#[case("*/5 * * * * *", "2024-01-01T00:00:01Z", "2024-01-01T00:00:05+00:00")]
#[case("0 */15 * * * *", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00+00:00")]
#[case("0 */15 * * * *", "2024-01-01T00:01:00Z", "2024-01-01T00:15:00+00:00")]
#[case("0 */30 9-17 * * 1-5", "2024-01-01T09:00:00Z", "2024-01-01T09:00:00+00:00")]
#[case("0 */30 9-17 * * 1-5", "2024-01-01T09:15:00Z", "2024-01-01T09:30:00+00:00")]
#[case("0 */5 * * * *", "2024-01-01T00:01:00Z", "2024-01-01T00:05:00+00:00")]
#[case("0 0 */2 * * *", "2024-01-01T01:00:00Z", "2024-01-01T02:00:00+00:00")]
#[case("0 0 0 ? * 1-5", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00+00:00")]
#[case("0 0 0 ? * 1-5", "2024-01-01T00:00:01Z", "2024-01-02T00:00:00+00:00")]
#[case("0 0 0 ? * 1-5", "2024-01-05T00:00:01Z", "2024-01-08T00:00:00+00:00")]
#[case("0 0 0 * * 1#1", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00+00:00")]
#[case("0 0 0 * * 1#1", "2024-01-02T00:00:00Z", "2024-02-05T00:00:00+00:00")]
#[case("0 0 0 * * 5L", "2024-01-01T00:00:00Z", "2024-01-26T00:00:00+00:00")]
#[case("0 0 0 * * 5L", "2024-01-26T00:00:01Z", "2024-02-23T00:00:00+00:00")]
#[case("0 0 0 * * 5L", "2024-02-23T00:00:00Z", "2024-02-23T00:00:00+00:00")]
#[case("0 0 0 * * 6,0", "2024-01-01T00:00:00Z", "2024-01-06T00:00:00+00:00")]
#[case("0 0 0 * * 6,0", "2024-01-06T00:00:01Z", "2024-01-07T00:00:00+00:00")]
#[case("0 0 0 * * 6,0", "2024-01-07T00:00:00Z", "2024-01-07T00:00:00+00:00")]
#[case("0 0 0 * * 6,0", "2024-01-07T00:00:01Z", "2024-01-13T00:00:00+00:00")]
#[case("0 0 0 * * MON", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00+00:00")]
#[case("0 0 0 * * SUN", "2024-01-01T00:00:00Z", "2024-01-07T00:00:00+00:00")]
#[case("0 0 0 1 */2 *", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00+00:00")]
#[case("0 0 0 1 */2 *", "2024-02-01T00:00:00Z", "2024-03-01T00:00:00+00:00")]
#[case("0 0 0 1 */3 * 1999", "1999-01-01T00:00:00Z", "1999-01-01T00:00:00+00:00")]
#[case("0 0 0 1 */3 * 1999", "1999-02-01T00:00:00Z", "1999-04-01T00:00:00+00:00")]
#[case("0 0 0 1 */3 *", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00+00:00")]
#[case("0 0 0 1 */3 *", "2024-02-01T00:00:00Z", "2024-04-01T00:00:00+00:00")]
#[case("0 0 0 1 1 * *", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00+00:00")]
#[case("0 0 0 1 1 * *", "2024-01-01T00:00:01Z", "2025-01-01T00:00:00+00:00")]
#[case("0 0 0 1 1 * 1970", "2024-01-01T00:00:00Z", "None")]
#[case("0 0 0 1 1 * 1999", "1999-01-01T00:00:00Z", "1999-01-01T00:00:00+00:00")]
#[case("0 0 0 1 1 * 1999", "1999-01-01T00:00:01Z", "None")]
#[case("0 0 0 1 1 * 2024-2025", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00+00:00")]
#[case("0 0 0 1 1 * 2024-2025", "2025-01-01T00:00:00Z", "2025-01-01T00:00:00+00:00")]
#[case("0 0 0 1 1 * 2024-2025", "2026-01-01T00:00:00Z", "None")]
#[case("0 0 0 1 1,6,12 *", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00+00:00")]
#[case("0 0 0 1 1,6,12 *", "2024-02-01T00:00:00Z", "2024-06-01T00:00:00+00:00")]
#[case("0 0 0 1,15 * ?", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00+00:00")]
#[case("0 0 0 1,15 * ?", "2024-01-01T00:00:01Z", "2024-01-15T00:00:00+00:00")]
#[case("0 0 0 1,15 * ?", "2024-01-15T00:00:01Z", "2024-02-01T00:00:00+00:00")]
#[case("0 0 0 1,15,L * ?", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00+00:00")]
#[case("0 0 0 1,15,L * ?", "2024-01-15T00:00:01Z", "2024-01-31T00:00:00+00:00")]
#[case("0 0 0 1,15,L * ?", "2024-01-31T00:00:01Z", "2024-02-01T00:00:00+00:00")]
#[case("0 0 0 1W * *", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00+00:00")]
#[case("0 0 0 1W * *", "2024-04-01T00:00:00Z", "2024-04-01T00:00:00+00:00")]
#[case("0 0 0 28-31 2 *", "2024-02-28T00:00:00Z", "2024-02-28T00:00:00+00:00")]
#[case("0 0 0 28-31 2 *", "2024-02-28T00:00:01Z", "2024-02-29T00:00:00+00:00")]
#[case("0 0 0 28-31 2 *", "2025-02-28T00:00:00Z", "2025-02-28T00:00:00+00:00")]
#[case("0 0 0 28-31 2 *", "2025-02-28T00:00:01Z", "2026-02-28T00:00:00+00:00")]
#[case("0 0 0 28,29,30,31 2 *", "2024-02-28T00:00:00Z", "2024-02-28T00:00:00+00:00")]
#[case("0 0 0 28,29,30,31 2 *", "2024-02-28T00:00:01Z", "2024-02-29T00:00:00+00:00")]
#[case("0 0 0 28,29,30,31 2 *", "2025-02-28T00:00:00Z", "2025-02-28T00:00:00+00:00")]
#[case("0 0 0 28,29,30,31 2 *", "2025-02-28T00:00:01Z", "2026-02-28T00:00:00+00:00")]
#[case("0 0 0 29 2 * 1999", "1999-01-01T00:00:00Z", "None")]
#[case("0 0 0 29 2 * 1999/3", "1999-01-01T00:00:00Z", "2008-02-29T00:00:00+00:00")]
#[case("0 0 0 29 2 *", "2024-01-01T00:00:00Z", "2024-02-29T00:00:00+00:00")]
#[case("0 0 0 29 2 *", "2024-03-01T00:00:00Z", "2028-02-29T00:00:00+00:00")]
#[case("0 0 0 29-31 2 *", "2024-02-29T00:00:00Z", "2024-02-29T00:00:00+00:00")]
#[case("0 0 0 29-31 2 *", "2024-02-29T00:00:01Z", "2028-02-29T00:00:00+00:00")]
#[case("0 0 0 29-31 2 *", "2025-02-01T00:00:00Z", "2028-02-29T00:00:00+00:00")]
#[case("0 0 0 29,30,31 2 *", "2024-02-29T00:00:00Z", "2024-02-29T00:00:00+00:00")]
#[case("0 0 0 29,30,31 2 *", "2024-02-29T00:00:01Z", "2028-02-29T00:00:00+00:00")]
#[case("0 0 0 29,30,31 2 *", "2025-02-01T00:00:00Z", "2028-02-29T00:00:00+00:00")]
#[case("0 0 0 31 */2 * 1999", "1999-01-01T00:00:00Z", "1999-01-31T00:00:00+00:00")]
#[case("0 0 0 31 */2 *", "2024-01-01T00:00:00Z", "2024-01-31T00:00:00+00:00")]
#[case("0 0 0 31 */2 *", "2024-02-01T00:00:00Z", "2024-03-31T00:00:00+00:00")]
#[case("0 0 0 L * * 1999", "1999-01-15T00:00:00Z", "1999-01-31T00:00:00+00:00")]
#[case("0 0 0 L * * 1999", "1999-02-15T00:00:00Z", "1999-02-28T00:00:00+00:00")]
#[case("0 0 0 L * *", "2024-01-15T00:00:00Z", "2024-01-31T00:00:00+00:00")]
#[case("0 0 0 L * *", "2024-02-15T00:00:00Z", "2024-02-29T00:00:00+00:00")]
#[case("0 0 0 L * *", "2024-04-15T00:00:00Z", "2024-04-30T00:00:00+00:00")]
#[case("0 0 1 1 *", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00+00:00")]
#[case("0 0 1 1 *", "2024-01-01T00:00:01Z", "2025-01-01T00:00:00+00:00")]
#[case("0 0 12 ? * 2-6", "2024-01-01T00:00:00Z", "2024-01-02T12:00:00+00:00")]
#[case("0 0 12 ? * 2-6", "2024-01-06T12:00:00Z", "2024-01-06T12:00:00+00:00")]
#[case("0 0 12 ? * 2-6", "2024-01-06T12:00:01Z", "2024-01-09T12:00:00+00:00")]
#[case("0 0 12 * * MON-FRI 1999", "1999-01-01T00:00:00Z", "1999-01-01T12:00:00+00:00")]
#[case("0 0 12 * * MON-FRI 1999", "1999-01-01T12:00:01Z", "1999-01-04T12:00:00+00:00")]
#[case("0 0 12 * * MON-FRI", "2024-01-01T00:00:00Z", "2024-01-01T12:00:00+00:00")]
#[case("0 0 12 * * MON-FRI", "2024-01-06T00:00:00Z", "2024-01-08T12:00:00+00:00")]
#[case("0 0 12 1-7 * *", "2024-01-01T00:00:00Z", "2024-01-01T12:00:00+00:00")]
#[case("0 0 12 1-7 * *", "2024-01-07T12:00:00Z", "2024-01-07T12:00:00+00:00")]
#[case("0 0 12 1-7 * *", "2024-01-07T12:00:01Z", "2024-02-01T12:00:00+00:00")]
#[case("0 0 12 1,15 * ?", "2024-01-01T00:00:00Z", "2024-01-01T12:00:00+00:00")]
#[case("0 0 12 1,15 * ?", "2024-01-01T12:00:01Z", "2024-01-15T12:00:00+00:00")]
#[case("0 0 6 * * 1-5", "2024-01-01T00:00:00Z", "2024-01-01T06:00:00+00:00")]
#[case("0 0 6 * * 1-5", "2024-01-06T00:00:00Z", "2024-01-08T06:00:00+00:00")]
#[case("0 0 9 * * 1", "2024-01-01T00:00:00Z", "2024-01-01T09:00:00+00:00")]
#[case("0 0 9 * * 1", "2024-01-01T09:00:01Z", "2024-01-08T09:00:00+00:00")]
#[case("0 0 9 * * 1#1", "2024-04-12T00:00:00Z", "2024-05-06T09:00:00+00:00")]
#[case("0 0 9 * * 6#4", "2024-11-30T09:00:00Z", "2024-12-28T09:00:00+00:00")]
#[case("0 0 9-17 * * 1-5", "2024-01-01T08:00:00Z", "2024-01-01T09:00:00+00:00")]
#[case("0 0 9-17 * * 1-5", "2024-01-01T17:00:01Z", "2024-01-02T09:00:00+00:00")]
#[case("0 15,45 9-17 * * 1-5", "2024-01-01T09:00:00Z", "2024-01-01T09:15:00+00:00")]
#[case("0 15,45 9-17 * * 1-5", "2024-01-01T09:15:01Z", "2024-01-01T09:45:00+00:00")]
#[case("0 30 0 1 * *", "2024-01-01T00:00:00Z", "2024-01-01T00:30:00+00:00")]
#[case("30 0 0 1 * *", "2024-01-01T00:00:00Z", "2024-01-01T00:00:30+00:00")]
#[case("30 0 0 1 * *", "2024-01-01T00:00:30Z", "2024-01-01T00:00:30+00:00")]
#[case("30 0 0 1 * *", "2024-01-01T00:00:30.001Z", "2024-02-01T00:00:30+00:00")]
#[case("25 * * * *", "2024-01-01T00:21:21Z", "2024-01-01T00:25:00+00:00")]
#[case("1 2 29-31 * *", "2024-01-01T00:00:21Z", "2024-01-29T02:01:00+00:00")]
#[case("1 2 29-31 * *", "2024-01-31T00:00:21Z", "2024-01-31T02:01:00+00:00")]
#[case("1 2 29-31 * *", "2024-02-01T00:00:21Z", "2024-02-29T02:01:00+00:00")]
#[case("1 2 29-31 * *", "2024-03-31T00:00:21Z", "2024-03-31T02:01:00+00:00")]
#[case("1 2 29-31 * *", "2025-01-01T00:00:21Z", "2025-01-29T02:01:00+00:00")]
#[case("1 2 29-31 * *", "2025-02-01T00:00:21Z", "2025-03-29T02:01:00+00:00")]
#[case("1 2 29-31 * *", "2025-03-31T00:00:21Z", "2025-03-31T02:01:00+00:00")]
#[case("@yearly", "2025-03-31T00:00:21Z", "2026-01-01T00:00:00+00:00")]
#[case("@annually", "2025-03-31T00:00:21Z", "2026-01-01T00:00:00+00:00")]
#[case("@monthly", "2025-03-31T00:00:21Z", "2025-04-01T00:00:00+00:00")]
#[case("@weekly", "2025-03-31T00:00:21Z", "2025-04-06T00:00:00+00:00")]
#[case("@daily", "2025-03-31T00:00:21Z", "2025-04-01T00:00:00+00:00")]
#[case("@midnight", "2025-03-31T00:00:21Z", "2025-04-01T00:00:00+00:00")]
#[case("@hourly", "2025-03-31T00:00:21Z", "2025-03-31T01:00:00+00:00")]
#[timeout(Duration::from_secs(1))]
fn test_schedule_upcoming(#[case] pattern: &str, #[case] current: &str, #[case] expected: &str) {
let schedule = Schedule::new(pattern).unwrap();
let current = DateTime::parse_from_rfc3339(current).unwrap();
let next = schedule.upcoming(¤t);
if expected == "None" {
assert!(
next.is_none(),
"pattern = {pattern}, schedule = {schedule:?}, current = {current}, next = {next:?}"
);
} else {
assert!(
next.is_some(),
"pattern = {pattern}, schedule = {schedule:?}, current = {current}, next = {next:?}"
);
assert_eq!(
next.unwrap().to_rfc3339(),
expected,
"pattern = {pattern}, schedule = {schedule:?}, current = {current}, next = {next:?}"
);
}
}
#[test]
fn test_inc_year() {
let mut year = Some(2024);
let mut month = Some(1);
let mut dom = Some(1);
let mut hour = Some(0);
let mut minute = Some(0);
let mut second = Some(0);
assert_eq!(
inc_year(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
Some(2025)
);
assert_eq!(year, Some(2025));
assert_eq!(month, Some(1));
assert_eq!(dom, Some(1));
assert_eq!(hour, Some(0));
assert_eq!(minute, Some(0));
assert_eq!(second, Some(0));
year = Some(MAX_YEAR);
assert_eq!(
inc_year(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
None
);
assert_eq!(year, None);
}
#[test]
fn test_inc_month() {
let mut year = Some(2024);
let mut month = Some(1);
let mut dom = Some(1);
let mut hour = Some(0);
let mut minute = Some(0);
let mut second = Some(0);
assert_eq!(
inc_month(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
Some(2)
);
assert_eq!(year, Some(2024));
assert_eq!(month, Some(2));
assert_eq!(dom, Some(1));
assert_eq!(hour, Some(0));
assert_eq!(minute, Some(0));
assert_eq!(second, Some(0));
year = Some(2024);
month = Some(12);
assert_eq!(
inc_month(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
Some(1)
);
assert_eq!(year, Some(2025));
assert_eq!(month, Some(1));
assert_eq!(dom, Some(1));
assert_eq!(hour, Some(0));
assert_eq!(minute, Some(0));
assert_eq!(second, Some(0));
year = Some(MAX_YEAR);
month = Some(12);
assert_eq!(
inc_month(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
None
);
assert_eq!(year, None);
}
#[test]
fn test_inc_dom() {
let mut year = Some(2024);
let mut month = Some(1);
let mut dom = Some(1);
let mut hour = Some(0);
let mut minute = Some(0);
let mut second = Some(0);
assert_eq!(
inc_dom(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
Some(2)
);
assert_eq!(year, Some(2024));
assert_eq!(month, Some(1));
assert_eq!(dom, Some(2));
assert_eq!(hour, Some(0));
assert_eq!(minute, Some(0));
assert_eq!(second, Some(0));
year = Some(2024);
month = Some(1);
dom = Some(31);
assert_eq!(
inc_dom(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
Some(1)
);
assert_eq!(year, Some(2024));
assert_eq!(month, Some(2));
assert_eq!(dom, Some(1));
assert_eq!(hour, Some(0));
assert_eq!(minute, Some(0));
assert_eq!(second, Some(0));
year = Some(2024);
month = Some(12);
dom = Some(31);
assert_eq!(
inc_dom(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
Some(1)
);
assert_eq!(year, Some(2025));
assert_eq!(month, Some(1));
assert_eq!(dom, Some(1));
assert_eq!(hour, Some(0));
assert_eq!(minute, Some(0));
assert_eq!(second, Some(0));
year = Some(2024);
month = Some(2);
dom = Some(28);
assert_eq!(
inc_dom(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
Some(29)
);
assert_eq!(year, Some(2024));
assert_eq!(month, Some(2));
assert_eq!(dom, Some(29));
assert_eq!(hour, Some(0));
assert_eq!(minute, Some(0));
assert_eq!(second, Some(0));
year = Some(2025);
month = Some(2);
dom = Some(28);
assert_eq!(
inc_dom(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
Some(1)
);
assert_eq!(year, Some(2025));
assert_eq!(month, Some(3));
assert_eq!(dom, Some(1));
assert_eq!(hour, Some(0));
assert_eq!(minute, Some(0));
assert_eq!(second, Some(0));
year = Some(MAX_YEAR);
month = Some(12);
dom = Some(31);
assert_eq!(
inc_dom(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
None
);
assert_eq!(year, None);
}
#[test]
fn test_inc_hour() {
let mut year = Some(2024);
let mut month = Some(1);
let mut dom = Some(1);
let mut hour = Some(0);
let mut minute = Some(0);
let mut second = Some(0);
assert_eq!(
inc_hour(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
Some(1)
);
assert_eq!(year, Some(2024));
assert_eq!(month, Some(1));
assert_eq!(dom, Some(1));
assert_eq!(hour, Some(1));
assert_eq!(minute, Some(0));
assert_eq!(second, Some(0));
year = Some(2024);
month = Some(1);
dom = Some(1);
hour = Some(23);
assert_eq!(
inc_hour(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
Some(0)
);
assert_eq!(year, Some(2024));
assert_eq!(month, Some(1));
assert_eq!(dom, Some(2));
assert_eq!(hour, Some(0));
assert_eq!(minute, Some(0));
assert_eq!(second, Some(0));
year = Some(2024);
month = Some(12);
dom = Some(31);
hour = Some(23);
assert_eq!(
inc_hour(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
Some(0)
);
assert_eq!(year, Some(2025));
assert_eq!(month, Some(1));
assert_eq!(dom, Some(1));
assert_eq!(hour, Some(0));
assert_eq!(minute, Some(0));
assert_eq!(second, Some(0));
year = Some(MAX_YEAR);
month = Some(12);
dom = Some(31);
hour = Some(23);
assert_eq!(
inc_hour(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
None
);
assert_eq!(year, None);
}
#[test]
fn test_inc_minute() {
let mut year = Some(2024);
let mut month = Some(1);
let mut dom = Some(1);
let mut hour = Some(0);
let mut minute = Some(0);
let mut second = Some(0);
assert_eq!(
inc_minute(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
Some(1)
);
assert_eq!(year, Some(2024));
assert_eq!(month, Some(1));
assert_eq!(dom, Some(1));
assert_eq!(hour, Some(0));
assert_eq!(minute, Some(1));
assert_eq!(second, Some(0));
year = Some(2024);
month = Some(1);
dom = Some(1);
hour = Some(23);
minute = Some(59);
assert_eq!(
inc_minute(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
Some(0)
);
assert_eq!(year, Some(2024));
assert_eq!(month, Some(1));
assert_eq!(dom, Some(2));
assert_eq!(hour, Some(0));
assert_eq!(minute, Some(0));
assert_eq!(second, Some(0));
year = Some(2024);
month = Some(12);
dom = Some(31);
hour = Some(23);
minute = Some(59);
assert_eq!(
inc_minute(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
Some(0)
);
assert_eq!(year, Some(2025));
assert_eq!(month, Some(1));
assert_eq!(dom, Some(1));
assert_eq!(hour, Some(0));
assert_eq!(minute, Some(0));
assert_eq!(second, Some(0));
year = Some(MAX_YEAR);
month = Some(12);
dom = Some(31);
hour = Some(23);
minute = Some(59);
assert_eq!(
inc_minute(&mut year, &mut month, &mut dom, &mut hour, &mut minute, &mut second),
None
);
assert_eq!(year, None);
}
#[template]
#[rstest]
#[case("* * * * * * *", "* * * * * * *")]
#[case("* * * * * *", "* * * * * * *")]
#[case("* * * * *", "0 * * * * * *")]
#[case("*/5 * * * *", "0 0/5 * * * * *")]
#[case("0 */15 */6 * * *", "0 0/15 0/6 * * * *")]
#[case("0 0 ? 1 0", "0 0 0 ? 1 0 *")]
#[case("0 0 * * SUN", "0 0 0 * * 0 *")]
#[case("0 0 * 1 0", "0 0 0 * 1 0 *")]
#[case("0 0 1 1 ?", "0 0 0 1 1 ? *")]
#[case("0 0 1 1 *", "0 0 0 1 1 * *")]
#[case("0 0 12 * * MON", "0 0 12 * * 1 *")]
#[case("0 0 22 * * 1-5", "0 0 22 * * 1-5 *")]
#[case("0 0/5 14,18 * * *", "0 0/5 14,18 * * * *")]
#[case("0 15 10 ? * MON-FRI", "0 15 10 ? * 1-5 *")]
#[case("1,22,45 5/2 0-15 1-6/2 */6 * 2000", "1,22,45 5/2 0-15 1-6/2 1/6 * 2000")]
#[case("23 0-20/2 * * *", "0 23 0-20/2 * * * *")]
#[case("30 0 1 1 * *", "30 0 1 1 * * *")]
#[case("5,10,15,20 * * * *", "0 5,10,15,20 * * * * *")]
#[case("@yearly", "0 0 0 1 1 ? *")]
#[case("@annually", "0 0 0 1 1 ? *")]
#[case("@monthly", "0 0 0 1 * ? *")]
#[case("@weekly", "0 0 0 ? * 0 *")]
#[case("@daily", "0 0 0 * * * *")]
#[case("@midnight", "0 0 0 * * * *")]
#[case("@hourly", "0 0 * * * * *")]
fn valid_schedules_to_test(#[case] input: &str) {}
#[apply(valid_schedules_to_test)]
fn test_schedule_display_and_new(#[case] input: &str, #[case] expected: &str) {
assert_eq!(Schedule::new(input).unwrap().to_string(), expected);
}
#[apply(valid_schedules_to_test)]
fn test_try_from_string(#[case] input: &str, #[case] _expected: &str) {
let schedule1 = Schedule::new(input).unwrap();
let schedule2 = Schedule::try_from(input).unwrap();
assert_eq!(schedule1, schedule2);
let tst_string = String::from(input);
let schedule2 = Schedule::try_from(&tst_string).unwrap();
assert_eq!(schedule1, schedule2);
let schedule2 = Schedule::try_from(tst_string).unwrap();
assert_eq!(schedule1, schedule2);
let schedule2 = Schedule::from_str(input).unwrap();
assert_eq!(schedule1, schedule2);
}
#[rstest]
#[timeout(Duration::from_secs(1))]
fn test_schedule_iter() {
let schedule = Schedule::new("0 0 12 * 1 MON 2024").unwrap();
let mut iter = schedule.iter(&DateTime::parse_from_rfc3339("2024-01-01T00:00:00+00:00").unwrap());
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T12:00:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-08T12:00:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-15T12:00:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-22T12:00:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-29T12:00:00+00:00");
assert_eq!(iter.next(), None);
}
#[rstest]
#[timeout(Duration::from_secs(1))]
fn test_schedule_iter_every_second() {
let schedule = Schedule::new("* * * * * *").unwrap();
let mut iter = schedule.iter(&DateTime::parse_from_rfc3339("2024-01-01T00:00:01+00:00").unwrap());
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T00:00:01+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T00:00:02+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T00:00:03+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T00:00:04+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T00:00:05+00:00");
}
#[rstest]
#[timeout(Duration::from_secs(1))]
fn test_schedule_iter_every_minute() {
let schedule = Schedule::new("* * * * *").unwrap();
let mut iter = schedule.iter(&DateTime::parse_from_rfc3339("2024-01-01T00:00:01+00:00").unwrap());
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T00:01:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T00:02:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T00:03:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T00:04:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T00:05:00+00:00");
}
#[rstest]
#[timeout(Duration::from_secs(1))]
fn test_schedule_iter_every_hour() {
let schedule = Schedule::new("13 * * * *").unwrap();
let mut iter = schedule.iter(&DateTime::parse_from_rfc3339("2024-01-01T07:01:01+00:00").unwrap());
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T07:13:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T08:13:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T09:13:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T10:13:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T11:13:00+00:00");
}
#[rstest]
#[timeout(Duration::from_secs(1))]
fn test_schedule_iter_every_day() {
let schedule = Schedule::new("22 5 * * *").unwrap();
let mut iter = schedule.iter(&DateTime::parse_from_rfc3339("2024-01-01T04:01:01+00:00").unwrap());
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T05:22:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-02T05:22:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-03T05:22:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-04T05:22:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-05T05:22:00+00:00");
}
#[rstest]
#[timeout(Duration::from_secs(1))]
fn test_schedule_iter_every_month() {
let schedule = Schedule::new("13 13 12 * *").unwrap();
let mut iter = schedule.iter(&DateTime::parse_from_rfc3339("2024-01-12T13:13:01+00:00").unwrap());
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-02-12T13:13:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-03-12T13:13:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-04-12T13:13:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-05-12T13:13:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-06-12T13:13:00+00:00");
}
#[rstest]
#[timeout(Duration::from_secs(1))]
fn test_schedule_iter_every_weekday() {
let schedule = Schedule::new("13 13 ? * *").unwrap();
let mut iter = schedule.iter(&DateTime::parse_from_rfc3339("2024-01-12T13:13:01+00:00").unwrap());
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-13T13:13:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-14T13:13:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-15T13:13:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-16T13:13:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-17T13:13:00+00:00");
}
#[rstest]
#[timeout(Duration::from_secs(1))]
fn test_schedule_iter_every_year() {
let schedule = Schedule::new("30 12 22 6 ?").unwrap();
let mut iter = schedule.iter(&DateTime::parse_from_rfc3339("2021-01-12T13:13:01+00:00").unwrap());
assert_eq!(iter.next().unwrap().to_rfc3339(), "2021-06-22T12:30:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2022-06-22T12:30:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2023-06-22T12:30:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-06-22T12:30:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2025-06-22T12:30:00+00:00");
}
#[rstest]
#[timeout(Duration::from_secs(1))]
fn test_schedule_into_iter() {
let schedule = Schedule::new("0 0 12 * 1 MON 2024").unwrap();
let mut iter = schedule.into_iter(&DateTime::parse_from_rfc3339("2024-01-01T00:00:00+00:00").unwrap());
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-01T12:00:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-08T12:00:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-15T12:00:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-22T12:00:00+00:00");
assert_eq!(iter.next().unwrap().to_rfc3339(), "2024-01-29T12:00:00+00:00");
assert_eq!(iter.next(), None);
}
#[template]
#[rstest]
#[case("* * * *")]
#[case("* * ? * ?")]
#[case("* * 10 * 1")]
#[case("* * * * 2/2")]
#[case("0 1 2 3 * * 1969")]
#[case("0 0 0 ? * 6-1")]
#[case("@minutely")]
fn invalid_schedules_to_test(#[case] input: &str) {}
#[apply(invalid_schedules_to_test)]
fn test_invalid_schedule_constructor(#[case] input: &str) {
assert!(Schedule::new(input).is_err(), "input = {input}");
}
#[apply(invalid_schedules_to_test)]
fn test_try_from_invalid_string(#[case] input: &str) {
assert!(Schedule::try_from(input).is_err(), "input = {input}");
assert!(Schedule::from_str(input).is_err(), "input = {input}");
}
#[rstest]
#[case("0 0 0 29 2 ? 2024", "2024-02-28T23:59:59Z", "2024-02-29T00:00:00+00:00")]
#[case("0 0 0 29 2 ? 2024-2099", "2024-03-01T23:59:59Z", "2028-02-29T00:00:00+00:00")]
#[case("0 0 0 29 2 ? 2025-2099", "2024-02-01T23:59:59Z", "2028-02-29T00:00:00+00:00")]
#[case("0 0 0 29 2 ? 1971/7", "1970-02-01T23:59:59Z", "1992-02-29T00:00:00+00:00")]
#[case("0 0 0 29 2 ? 2024-2027", "2024-02-29T00:00:01Z", "None")]
#[case("0 0 0 1 * ? 2024", "2024-01-31T23:59:59Z", "2024-02-01T00:00:00+00:00")]
#[case("0 0 0 1 * ? 2024", "2024-02-28T23:59:59Z", "2024-03-01T00:00:00+00:00")]
#[case("0 0 0 1 * ? 2024", "2024-02-29T23:59:59Z", "2024-03-01T00:00:00+00:00")]
#[case("0 0 0 1 * ? 2024", "2024-03-31T23:59:59Z", "2024-04-01T00:00:00+00:00")]
#[case("0 0 0 1 * ? 2024", "2024-04-30T23:59:59Z", "2024-05-01T00:00:00+00:00")]
#[case("0 0 0 1 * ? 2024", "2024-11-30T23:59:59Z", "2024-12-01T00:00:00+00:00")]
#[case("0 0 0 1 1 ? 2025", "2024-12-31T23:59:59Z", "2025-01-01T00:00:00+00:00")]
#[case("0 0 0 L 2 ? 2024", "2024-02-28T23:59:59Z", "2024-02-29T00:00:00+00:00")]
#[case("0 0 0 L 2 ? 2025", "2024-02-28T23:59:59Z", "2025-02-28T00:00:00+00:00")]
#[case("0 0 0 ? 2 4L 2024", "2024-02-28T23:59:59Z", "2024-02-29T00:00:00+00:00")]
#[case("0 0 0 15W * ? 2024", "2024-01-14T23:59:59Z", "2024-01-15T00:00:00+00:00")]
#[case("0 0 0 ? * 1#3 2024", "2024-01-20T23:59:59Z", "2024-02-19T00:00:00+00:00")]
#[timeout(Duration::from_secs(1))]
fn test_edge_cases(#[case] pattern: &str, #[case] current: &str, #[case] expected: &str) {
let schedule = Schedule::new(pattern).unwrap();
let current = DateTime::parse_from_rfc3339(current).unwrap();
let next = schedule.upcoming(¤t);
if expected == "None" {
assert!(
next.is_none(),
"pattern = {pattern}, schedule = {schedule:?}, current = {current}, next = {next:?}"
);
} else {
assert!(
next.is_some(),
"pattern = {pattern}, schedule = {schedule:?}, current = {current}, next = {next:?}"
);
assert_eq!(
next.unwrap().to_rfc3339(),
expected,
"pattern = {pattern}, schedule = {schedule:?}, current = {current}, next = {next:?}"
);
}
}
#[apply(valid_schedules_to_test)]
fn test_schedule_to_string(#[case] input: &str, #[case] expected: &str) {
let schedule = Schedule::new(input).unwrap();
let string: String = (&schedule).into();
assert_eq!(string, expected);
let string: String = schedule.into();
assert_eq!(string, expected);
}
#[cfg(feature = "tz")]
mod tz {
use super::super::*;
use chrono::{Local, Utc};
use rstest::rstest;
use rstest_reuse::{apply, template};
use std::{fmt::Debug, time::Duration};
#[template]
#[rstest]
#[case("TZ=Europe/Kyiv * * * * * * *", "TZ=Europe/Kyiv * * * * * * *")]
#[case("TZ=Europe/London * * * * * *", "TZ=Europe/London * * * * * * *")]
#[case("TZ=UTC * * * * *", "TZ=UTC 0 * * * * * *")]
#[case("TZ=US/Pacific */5 * * * *", "TZ=US/Pacific 0 0/5 * * * * *")]
#[case("TZ=EET 0 */15 */6 * * *", "TZ=EET 0 0/15 0/6 * * * *")]
#[case("TZ=Asia/Tokyo @yearly", "TZ=Asia/Tokyo 0 0 0 1 1 ? *")]
#[case("Tz=Asia/Tokyo @yearly", "TZ=Asia/Tokyo 0 0 0 1 1 ? *")]
#[case("tz=Asia/Tokyo @yearly", "TZ=Asia/Tokyo 0 0 0 1 1 ? *")]
#[case("tz=Europe/Paris @yearly", "TZ=Europe/Paris 0 0 0 1 1 ? *")]
fn valid_schedules_to_test(#[case] input: &str, #[case] expected: &str) {}
#[apply(valid_schedules_to_test)]
fn test_schedule_display_and_new(#[case] input: &str, #[case] expected: &str) {
assert_eq!(Schedule::new(input).unwrap().to_string(), expected);
}
#[apply(valid_schedules_to_test)]
fn test_try_from_string(#[case] input: &str, #[case] _expected: &str) {
let schedule1 = Schedule::new(input).unwrap();
let schedule2 = Schedule::try_from(input).unwrap();
assert_eq!(schedule1, schedule2);
let tst_string = String::from(input);
let schedule2 = Schedule::try_from(&tst_string).unwrap();
assert_eq!(schedule1, schedule2);
let schedule2 = Schedule::try_from(tst_string).unwrap();
assert_eq!(schedule1, schedule2);
let schedule2 = Schedule::from_str(input).unwrap();
assert_eq!(schedule1, schedule2);
}
#[template]
#[rstest]
#[case("TZ * * * *")]
#[case("TZ= * * ? * ?")]
#[case("tz=UTC * * 10 * 1")]
#[case("TZ=Aaa/Bbb * * * * 2/2")]
#[case("TZ=Aaa/Bbb * * * * *")]
#[case("TZ =UTC * * * * *")]
#[case("TZ= UTC * * * * *")]
#[case("TZ = UTC * * * * *")]
#[case("TZ= 0 0 0 ? * 1-6")]
#[case("tz= @hourly")]
fn invalid_schedules_to_test(#[case] input: &str) {}
#[apply(invalid_schedules_to_test)]
fn test_invalid_schedule_constructor(#[case] input: &str) {
assert!(Schedule::new(input).is_err(), "input = {input}");
}
#[apply(invalid_schedules_to_test)]
fn test_try_from_invalid_string(#[case] input: &str) {
assert!(Schedule::try_from(input).is_err(), "input = {input}");
assert!(Schedule::from_str(input).is_err(), "input = {input}");
}
#[rstest]
#[case("TZ=Europe/Kyiv @monthly", "2025-03-31T00:00:21Z", "2025-03-31T21:00:00+00:00")]
#[case("TZ=Europe/Kyiv @monthly", "2025-03-31T00:00:21+02:00", "2025-03-31T23:00:00+02:00")]
#[case("TZ=Europe/Kyiv @monthly", "2025-11-30T00:00:21Z", "2025-11-30T22:00:00+00:00")]
#[case("TZ=EET @hourly", "2000-03-25T23:00:01Z", "2000-03-26T00:00:00+00:00")]
#[case("TZ=EET @hourly", "2000-03-26T00:00:01Z", "2000-03-26T01:00:00+00:00")]
#[case("TZ=EET @hourly", "2000-03-26T01:00:01Z", "2000-03-26T02:00:00+00:00")]
#[case("TZ=EET @hourly", "2000-10-28T22:00:01Z", "2000-10-28T23:00:00+00:00")]
#[case("TZ=EET @hourly", "2000-10-28T23:00:01Z", "2000-10-29T00:00:00+00:00")] #[case("TZ=EET @hourly", "2000-10-29T00:00:01Z", "2000-10-29T02:00:00+00:00")] #[case("TZ=EET @hourly", "2000-10-29T01:00:01Z", "2000-10-29T02:00:00+00:00")] #[case("TZ=EET @hourly", "2000-10-29T02:00:01Z", "2000-10-29T03:00:00+00:00")]
#[timeout(Duration::from_secs(1))]
fn test_schedule_upcoming(#[case] pattern: &str, #[case] current: &str, #[case] expected: &str) {
let schedule = Schedule::new(pattern).unwrap();
let current = DateTime::parse_from_rfc3339(current).unwrap();
let next = schedule.upcoming(¤t);
if expected == "None" {
assert!(
next.is_none(),
"pattern = {pattern}, schedule = {schedule:?}, current = {current}, next = {next:?}"
);
} else {
assert!(
next.is_some(),
"pattern = {pattern}, schedule = {schedule:?}, current = {current}, next = {next:?}"
);
assert_eq!(
next.unwrap().to_rfc3339(),
expected,
"pattern = {pattern}, schedule = {schedule:?}, current = {current}, next = {next:?}"
);
}
}
#[rstest]
#[timeout(Duration::from_secs(1))]
fn test_schedule_iter() {
let schedule = Schedule::new("TZ=UTC 0 0 12 * 1 MON 2024").unwrap();
let mut iter = schedule.iter(&DateTime::parse_from_rfc3339("2024-01-01T00:00:00+00:00").unwrap());
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-01T12:00:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-08T12:00:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-15T12:00:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-22T12:00:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-29T12:00:00+00:00",
"schedule = {schedule}"
);
assert_eq!(iter.next(), None);
}
#[rstest]
#[timeout(Duration::from_secs(1))]
fn test_schedule_iter_every_second() {
let schedule = Schedule::new("TZ=EET * * * * * *").unwrap();
let mut iter = schedule.iter(&DateTime::parse_from_rfc3339("2024-01-01T00:00:01+00:00").unwrap());
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-01T00:00:01+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-01T00:00:02+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-01T00:00:03+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-01T00:00:04+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-01T00:00:05+00:00",
"schedule = {schedule}"
);
}
#[rstest]
#[timeout(Duration::from_secs(1))]
fn test_schedule_iter_every_minute() {
let schedule = Schedule::new("TZ=Europe/Kyiv * * * * *").unwrap();
let mut iter = schedule.iter(&DateTime::parse_from_rfc3339("2024-01-01T00:00:01+00:00").unwrap());
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-01T00:01:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-01T00:02:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-01T00:03:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-01T00:04:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-01T00:05:00+00:00",
"schedule = {schedule}"
);
}
#[rstest]
#[timeout(Duration::from_secs(1))]
fn test_schedule_iter_every_hour() {
let schedule = Schedule::new("TZ=Canada/Eastern 13 * * * *").unwrap();
let mut iter = schedule.iter(&DateTime::parse_from_rfc3339("2024-01-01T07:01:01+00:00").unwrap());
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-01T07:13:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-01T08:13:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-01T09:13:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-01T10:13:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-01T11:13:00+00:00",
"schedule = {schedule}"
);
}
#[rstest]
#[timeout(Duration::from_secs(1))]
fn test_schedule_iter_every_day() {
let schedule = Schedule::new("TZ=Asia/Tokyo 22 5 * * *").unwrap();
let mut iter = schedule.iter(&DateTime::parse_from_rfc3339("2024-01-01T04:01:01+00:00").unwrap());
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-01T20:22:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-02T20:22:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-03T20:22:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-04T20:22:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-05T20:22:00+00:00",
"schedule = {schedule}"
);
}
#[rstest]
#[timeout(Duration::from_secs(1))]
fn test_schedule_iter_every_month() {
let schedule = Schedule::new("TZ=GMT 13 13 12 * *").unwrap();
let mut iter = schedule.iter(&DateTime::parse_from_rfc3339("2024-01-12T13:13:01+00:00").unwrap());
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-02-12T13:13:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-03-12T13:13:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-04-12T13:13:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-05-12T13:13:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-06-12T13:13:00+00:00",
"schedule = {schedule}"
);
}
#[rstest]
#[timeout(Duration::from_secs(1))]
fn test_schedule_iter_every_weekday() {
let schedule = Schedule::new("TZ=Antarctica/South_Pole 13 13 ? * *").unwrap();
let mut iter = schedule.iter(&DateTime::parse_from_rfc3339("2024-01-12T13:13:01+00:00").unwrap());
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-13T00:13:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-14T00:13:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-15T00:13:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-16T00:13:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-01-17T00:13:00+00:00",
"schedule = {schedule}"
);
}
#[rstest]
#[timeout(Duration::from_secs(1))]
fn test_schedule_iter_every_year() {
let schedule = Schedule::new("TZ=Asia/Shanghai 30 12 22 6 ?").unwrap();
let mut iter = schedule.iter(&DateTime::parse_from_rfc3339("2021-01-12T13:13:01+00:00").unwrap());
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2021-06-22T04:30:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2022-06-22T04:30:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2023-06-22T04:30:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2024-06-22T04:30:00+00:00",
"schedule = {schedule}"
);
assert_eq!(
iter.next().unwrap().to_rfc3339(),
"2025-06-22T04:30:00+00:00",
"schedule = {schedule}"
);
}
#[rstest]
#[case(Utc, Utc)]
#[case(Utc, Local)]
#[case(Local, Utc)]
#[case(Local, Local)]
#[case(chrono_tz::EET, Utc)]
#[case(Utc, chrono_tz::EET)]
#[case(chrono_tz::EET, Local)]
#[case(Local, chrono_tz::EET)]
#[case(chrono_tz::EET, chrono_tz::EET)]
#[case(chrono_tz::EET, chrono_tz::Canada::Eastern)]
#[case(chrono_tz::Canada::Eastern, chrono_tz::EET)]
#[timeout(Duration::from_secs(3))]
fn test_rough_iterator<T1: TimeZone + Debug, T2: TimeZone + Debug>(
#[case] current_tz: T1,
#[case] schedule_tz: T2,
) {
use std::collections::BTreeSet;
const TAKE_ITEMS: usize = (365 + 366) * 24 + 3;
let current = current_tz.with_ymd_and_hms(2024, 3, 1, 0, 0, 0).unwrap();
let tz_str = format!("{schedule_tz:?}").to_uppercase();
let tz = if tz_str == "LOCAL" {
None
} else if tz_str == "Z" || tz_str == "UTC" {
Some(String::from("UTC"))
} else {
Some(format!("{schedule_tz:?}"))
};
let schedule = if let Some(tz) = tz {
Schedule::new(format!("TZ={tz} @hourly")).unwrap()
} else {
Schedule::new(" @hourly").unwrap()
};
let result = schedule.iter(¤t).take(TAKE_ITEMS).collect::<Vec<_>>();
assert_eq!(
result.len(),
TAKE_ITEMS,
"current_tz={current_tz:?}, schedule_tz={schedule_tz:?}, schedule='{schedule}', last={}",
result[result.len() - 1].to_rfc2822()
);
let mut set = BTreeSet::new();
set.extend(&result);
assert_eq!(set.len(), result.len());
}
}
}