chronon_scheduler/
cron.rs1use chrono::{DateTime, Utc};
4use chrono_tz::Tz;
5use chronon_core::{ChrononError, Result};
6
7#[derive(Debug, Clone)]
31pub struct CronExpr {
32 expr: String,
33 schedule: croner::Cron,
34 timezone: Tz,
35}
36
37impl CronExpr {
38 pub fn parse(expr: &str, timezone: Option<&str>) -> Result<Self> {
50 let schedule = croner::Cron::new(expr)
51 .with_seconds_optional()
52 .parse()
53 .map_err(|e| ChrononError::InvalidCron(format!("{expr}: {e}")))?;
54
55 let tz: Tz = match timezone {
56 Some(tz_str) => tz_str
57 .parse()
58 .map_err(|_| ChrononError::InvalidTimezone(tz_str.to_string()))?,
59 None => Tz::UTC,
60 };
61
62 Ok(Self {
63 expr: expr.to_string(),
64 schedule,
65 timezone: tz,
66 })
67 }
68
69 pub fn next_after(&self, after: DateTime<Utc>) -> Option<DateTime<Utc>> {
71 let after_tz = after.with_timezone(&self.timezone);
72 self.schedule
73 .find_next_occurrence(&after_tz, false)
74 .ok()
75 .map(|dt| dt.with_timezone(&Utc))
76 }
77
78 pub fn next_from_now(&self) -> Option<DateTime<Utc>> {
80 self.next_after(Utc::now())
81 }
82
83 pub fn expression(&self) -> &str {
85 &self.expr
86 }
87
88 pub fn timezone(&self) -> &Tz {
90 &self.timezone
91 }
92
93 pub fn has_seconds(&self) -> bool {
95 self.expr.split_whitespace().count() == 6
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use chrono::{Datelike, TimeZone, Timelike};
103
104 #[test]
105 fn parse_valid_cron() {
106 assert!(CronExpr::parse("0 0 * * *", None).is_ok());
107 }
108
109 #[test]
110 fn parse_with_timezone() {
111 let cron = CronExpr::parse("0 9 * * *", Some("America/New_York")).unwrap();
112 assert_eq!(*cron.timezone(), chrono_tz::America::New_York);
113 }
114
115 #[test]
116 fn parse_invalid_cron() {
117 assert!(CronExpr::parse("invalid", None).is_err());
118 }
119
120 #[test]
121 fn next_after_daily_noon() {
122 let cron = CronExpr::parse("0 12 * * *", None).unwrap();
123 let base = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
124 let next = cron.next_after(base).unwrap();
125 assert_eq!(next.hour(), 12);
126 assert_eq!(next.day(), 1);
127 }
128}