use std::collections::BTreeSet;
use std::collections::HashSet;
use std::fmt;
use std::str::FromStr;
use jiff::RoundMode;
use jiff::Span;
use jiff::Timestamp;
use jiff::ToSpan;
use jiff::Unit;
use jiff::Zoned;
use jiff::ZonedRound;
use jiff::civil::Weekday;
use jiff::tz::TimeZone;
mod parser;
pub use parser::FallbackTimezoneOption;
pub use parser::ParseOptions;
pub use parser::normalize_crontab;
pub use parser::parse_crontab;
pub use parser::parse_crontab_with;
pub extern crate jiff;
#[derive(Debug, Clone)]
pub struct Error(String);
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for Error {}
#[derive(Debug, Clone)]
pub struct Crontab {
minutes: PossibleLiterals,
hours: PossibleLiterals,
months: PossibleLiterals,
days_of_month: ParsedDaysOfMonth,
days_of_week: ParsedDaysOfWeek,
timezone: TimeZone,
}
#[derive(Debug)]
enum PossibleValue {
Literal(u8),
NearestWeekday(u8),
LastDayOfMonth,
LastDayOfWeek(Weekday),
NthDayOfWeek(u8, Weekday),
}
#[derive(Debug, Clone)]
struct PossibleLiterals {
values: BTreeSet<u8>,
}
impl PossibleLiterals {
fn matches(&self, value: u8) -> bool {
self.values.contains(&value)
}
}
#[derive(Debug, Clone)]
struct ParsedDaysOfWeek {
literals: BTreeSet<u8>,
last_days_of_week: HashSet<Weekday>,
nth_days_of_week: HashSet<(u8, Weekday)>,
start_with_asterisk: bool,
}
impl ParsedDaysOfWeek {
fn matches(&self, value: &Zoned) -> bool {
if self.literals.contains(&(value.weekday() as u8)) {
return true;
}
for weekday in self.last_days_of_week.iter() {
if value.weekday() != *weekday {
continue;
}
if (value + 1.week()).month() > value.month() {
return true;
}
}
for (nth, weekday) in self.nth_days_of_week.iter() {
if value.weekday() != *weekday {
continue;
}
if let Ok(nth_weekday) = value.nth_weekday_of_month(*nth as i8, *weekday)
&& nth_weekday.date() == value.date()
{
return true;
}
}
false
}
}
#[derive(Debug, Clone)]
struct ParsedDaysOfMonth {
literals: BTreeSet<u8>,
last_day_of_month: bool,
nearest_weekdays: BTreeSet<u8>,
start_with_asterisk: bool,
}
impl ParsedDaysOfMonth {
fn matches(&self, value: &Zoned) -> bool {
if self.literals.contains(&(value.day() as u8)) {
return true;
}
if self.last_day_of_month && (value + 1.day()).month() > value.month() {
return true;
}
for day in self.nearest_weekdays.iter() {
let day = *day as i8;
match value.weekday() {
Weekday::Saturday | Weekday::Sunday => {
continue;
}
Weekday::Tuesday | Weekday::Wednesday | Weekday::Thursday => {
if value.day() == day {
return true;
}
}
Weekday::Monday => {
if value.day() == day {
return true;
}
if value.day() - 1 == day {
return true;
}
if value.day() == 3 && day == 1 {
return true;
}
}
Weekday::Friday => {
if value.day() == day {
return true;
}
let last_day_of_this_month = value.days_in_month();
if value.day() + 1 == day && day <= last_day_of_this_month {
return true;
}
if value.day() + 2 == day && day == last_day_of_this_month {
return true;
}
}
}
}
false
}
}
impl FromStr for Crontab {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
parse_crontab(input)
}
}
impl<'a> TryFrom<&'a str> for Crontab {
type Error = Error;
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
FromStr::from_str(input)
}
}
#[derive(Debug, Copy, Clone)]
pub struct MakeTimestamp(pub Timestamp);
impl From<Timestamp> for MakeTimestamp {
fn from(timestamp: Timestamp) -> Self {
MakeTimestamp(timestamp)
}
}
impl FromStr for MakeTimestamp {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Timestamp::from_str(input)
.map(MakeTimestamp)
.map_err(error_with_context("failed to parse timestamp"))
}
}
impl<'a> TryFrom<&'a str> for MakeTimestamp {
type Error = Error;
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
FromStr::from_str(input)
}
}
impl MakeTimestamp {
pub fn from_second(second: i64) -> Result<Self, Error> {
Timestamp::from_second(second)
.map(MakeTimestamp)
.map_err(error_with_context("failed to make timestamp"))
}
pub fn from_millisecond(millisecond: i64) -> Result<Self, Error> {
Timestamp::from_millisecond(millisecond)
.map(MakeTimestamp)
.map_err(error_with_context("failed to make timestamp"))
}
pub fn from_microsecond(microsecond: i64) -> Result<Self, Error> {
Timestamp::from_microsecond(microsecond)
.map(MakeTimestamp)
.map_err(error_with_context("failed to make timestamp"))
}
pub fn from_nanosecond(nanosecond: i128) -> Result<Self, Error> {
Timestamp::from_nanosecond(nanosecond)
.map(MakeTimestamp)
.map_err(error_with_context("failed to make timestamp"))
}
}
impl Crontab {
pub fn iter_after<T>(&self, start: T) -> Result<CronTimesIter, Error>
where
T: TryInto<MakeTimestamp>,
T::Error: std::error::Error,
{
let start = start
.try_into()
.map_err(error_with_context("failed to parse start timestamp"))?;
Ok(CronTimesIter {
crontab: self.clone(),
timestamp: start.0,
})
}
pub fn find_next<T>(&self, timestamp: T) -> Result<Zoned, Error>
where
T: TryInto<MakeTimestamp>,
T::Error: std::error::Error,
{
let zoned = timestamp
.try_into()
.map(|ts| ts.0.to_zoned(self.timezone.clone()))
.map_err(error_with_context("failed to parse timestamp"))?;
let bound = &zoned + 4.years();
let mut next = zoned;
next = advance_time_and_round(next, 1.minute(), Some(Unit::Minute))?;
loop {
if next > bound {
return Err(Error(format!(
"failed to find next timestamp in four years; end with {next}"
)));
}
match self.matches_or_next(next)? {
Ok(matched) => break Ok(matched),
Err(candidate) => next = candidate,
}
}
}
pub fn matches<T>(&self, timestamp: T) -> Result<bool, Error>
where
T: TryInto<MakeTimestamp>,
T::Error: std::error::Error,
{
let zoned = timestamp
.try_into()
.map(|ts| ts.0.to_zoned(self.timezone.clone()))
.map_err(error_with_context("failed to parse timestamp"))?;
Ok(self.matches_or_next(zoned)?.is_ok())
}
fn matches_or_next(&self, zdt: Zoned) -> Result<Result<Zoned, Zoned>, Error> {
if !self.months.matches(zdt.month() as u8) {
let rest_days = zdt.days_in_month() - zdt.day() + 1;
return advance_time_and_round(zdt, rest_days.days(), Some(Unit::Day)).map(Err);
}
if self.days_of_month.start_with_asterisk || self.days_of_week.start_with_asterisk {
let cond = self.days_of_month.matches(&zdt) && self.days_of_week.matches(&zdt);
if !cond {
return advance_time_and_round(zdt, 1.day(), Some(Unit::Day)).map(Err);
}
} else {
let cond = self.days_of_month.matches(&zdt) || self.days_of_week.matches(&zdt);
if !cond {
return advance_time_and_round(zdt, 1.day(), Some(Unit::Day)).map(Err);
}
}
if !self.hours.matches(zdt.hour() as u8) {
return advance_time_and_round(zdt, 1.hour(), Some(Unit::Hour)).map(Err);
}
if !self.minutes.matches(zdt.minute() as u8) {
return advance_time_and_round(zdt, 1.minute(), Some(Unit::Minute)).map(Err);
}
Ok(Ok(zdt)) }
}
#[derive(Debug)]
pub struct CronTimesIter {
crontab: Crontab,
timestamp: Timestamp,
}
impl Iterator for CronTimesIter {
type Item = Result<Zoned, Error>;
fn next(&mut self) -> Option<Self::Item> {
match self.crontab.find_next(self.timestamp) {
Ok(zoned) => {
self.timestamp = zoned.timestamp();
Some(Ok(zoned))
}
Err(err) => Some(Err(err)),
}
}
}
fn advance_time_and_round(zdt: Zoned, span: Span, unit: Option<Unit>) -> Result<Zoned, Error> {
let mut next = zdt;
next = next.checked_add(span).map_err(error_with_context(&format!(
"failed to advance timestamp; end with {next}"
)))?;
if let Some(unit) = unit {
next = next
.round(ZonedRound::new().mode(RoundMode::Trunc).smallest(unit))
.map_err(error_with_context(&format!(
"failed to round timestamp; end with {next}"
)))?;
}
Ok(next)
}
fn error_with_context<E: std::error::Error>(context: &str) -> impl FnOnce(E) -> Error + '_ {
move |error| Error(format!("{context}: {error}"))
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use insta::assert_snapshot;
use jiff::Zoned;
use crate::CronTimesIter;
use crate::Crontab;
fn make_iter(crontab: &str, timestamp: &str) -> CronTimesIter {
let crontab = Crontab::from_str(crontab).unwrap();
crontab.iter_after(timestamp).unwrap()
}
fn next(iter: &mut CronTimesIter) -> Zoned {
iter.next().unwrap().unwrap()
}
#[test]
fn test_next_timestamp() {
let mut iter = make_iter("0 0 1 1 * Asia/Shanghai", "2024-01-01T00:00:00+08:00");
assert_snapshot!(next(&mut iter), @"2025-01-01T00:00:00+08:00[Asia/Shanghai]");
let mut iter = make_iter("2 4 * * * Asia/Shanghai", "2024-09-11T19:08:35+08:00");
assert_snapshot!(next(&mut iter), @"2024-09-12T04:02:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-09-13T04:02:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-09-14T04:02:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-09-15T04:02:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-09-16T04:02:00+08:00[Asia/Shanghai]");
let mut iter = make_iter("0 0 31 * * Asia/Shanghai", "2024-09-11T19:08:35+08:00");
assert_snapshot!(next(&mut iter), @"2024-10-31T00:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-12-31T00:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-01-31T00:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-03-31T00:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-05-31T00:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-07-31T00:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-08-31T00:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-10-31T00:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-12-31T00:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2026-01-31T00:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2026-03-31T00:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2026-05-31T00:00:00+08:00[Asia/Shanghai]");
let mut iter = make_iter("0 18 * * 1-5 Asia/Shanghai", "2024-09-11T19:08:35+08:00");
assert_snapshot!(next(&mut iter), @"2024-09-12T18:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-09-13T18:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-09-16T18:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-09-17T18:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-09-18T18:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-09-19T18:00:00+08:00[Asia/Shanghai]");
let mut iter = make_iter("0 18 * * TUE#1 Asia/Shanghai", "2024-09-24T00:08:35+08:00");
assert_snapshot!(next(&mut iter), @"2024-10-01T18:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-11-05T18:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-12-03T18:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-01-07T18:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-02-04T18:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-03-04T18:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-04-01T18:00:00+08:00[Asia/Shanghai]");
let mut iter = make_iter("4 2 * * 1L Asia/Shanghai", "2024-09-24T00:08:35+08:00");
assert_snapshot!(next(&mut iter), @"2024-09-30T02:04:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-10-28T02:04:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-11-25T02:04:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-01-27T02:04:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-02-24T02:04:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-03-31T02:04:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-04-28T02:04:00+08:00[Asia/Shanghai]");
let mut iter = make_iter("0 18 * * FRI#5 Asia/Shanghai", "2024-09-24T00:08:35+08:00");
assert_snapshot!(next(&mut iter), @"2024-11-29T18:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-01-31T18:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-05-30T18:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-08-29T18:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-10-31T18:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2026-01-30T18:00:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2026-05-29T18:00:00+08:00[Asia/Shanghai]");
let mut iter = make_iter(
"3 11 L JAN-FEB,5 * Asia/Shanghai",
"2024-09-24T00:08:35+08:00",
);
assert_snapshot!(next(&mut iter), @"2025-01-31T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-02-28T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-05-31T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2026-01-31T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2026-02-28T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2026-05-31T11:03:00+08:00[Asia/Shanghai]");
let mut iter = make_iter("3 11 17W,L * * Asia/Shanghai", "2024-09-24T00:08:35+08:00");
assert_snapshot!(next(&mut iter), @"2024-09-30T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-10-17T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-10-31T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-11-18T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-11-30T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-12-17T11:03:00+08:00[Asia/Shanghai]");
let mut iter = make_iter("3 11 1W * * Asia/Shanghai", "2024-09-24T00:08:35+08:00");
assert_snapshot!(next(&mut iter), @"2024-10-01T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-11-01T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-12-02T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-01-01T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-02-03T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-03-03T11:03:00+08:00[Asia/Shanghai]");
let mut iter = make_iter("3 11 31W * * Asia/Shanghai", "2024-09-24T00:08:35+08:00");
assert_snapshot!(next(&mut iter), @"2024-10-31T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2024-12-31T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-01-31T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-03-31T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-05-30T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-07-31T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-08-29T11:03:00+08:00[Asia/Shanghai]");
assert_snapshot!(next(&mut iter), @"2025-10-31T11:03:00+08:00[Asia/Shanghai]");
}
}