chronon_scheduler/
cron.rs1use chrono::{DateTime, Utc};
4use chrono_tz::Tz;
5use chronon_core::{ChrononError, Result};
6
7#[derive(Debug, Clone)]
9pub struct CronExpr {
10 expr: String,
11 schedule: croner::Cron,
12 timezone: Tz,
13}
14
15impl CronExpr {
16 pub fn parse(expr: &str, timezone: Option<&str>) -> Result<Self> {
28 let schedule = croner::Cron::new(expr)
29 .with_seconds_optional()
30 .parse()
31 .map_err(|e| ChrononError::InvalidCron(format!("{expr}: {e}")))?;
32
33 let tz: Tz = match timezone {
34 Some(tz_str) => tz_str
35 .parse()
36 .map_err(|_| ChrononError::InvalidTimezone(tz_str.to_string()))?,
37 None => Tz::UTC,
38 };
39
40 Ok(Self {
41 expr: expr.to_string(),
42 schedule,
43 timezone: tz,
44 })
45 }
46
47 pub fn next_after(&self, after: DateTime<Utc>) -> Option<DateTime<Utc>> {
49 let after_tz = after.with_timezone(&self.timezone);
50 self.schedule
51 .find_next_occurrence(&after_tz, false)
52 .ok()
53 .map(|dt| dt.with_timezone(&Utc))
54 }
55
56 pub fn next_from_now(&self) -> Option<DateTime<Utc>> {
58 self.next_after(Utc::now())
59 }
60
61 pub fn expression(&self) -> &str {
63 &self.expr
64 }
65
66 pub fn timezone(&self) -> &Tz {
68 &self.timezone
69 }
70
71 pub fn has_seconds(&self) -> bool {
73 self.expr.split_whitespace().count() == 6
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use chrono::{Datelike, TimeZone, Timelike};
81
82 #[test]
83 fn parse_valid_cron() {
84 assert!(CronExpr::parse("0 0 * * *", None).is_ok());
85 }
86
87 #[test]
88 fn parse_with_timezone() {
89 let cron = CronExpr::parse("0 9 * * *", Some("America/New_York")).unwrap();
90 assert_eq!(*cron.timezone(), chrono_tz::America::New_York);
91 }
92
93 #[test]
94 fn parse_invalid_cron() {
95 assert!(CronExpr::parse("invalid", None).is_err());
96 }
97
98 #[test]
99 fn next_after_daily_noon() {
100 let cron = CronExpr::parse("0 12 * * *", None).unwrap();
101 let base = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
102 let next = cron.next_after(base).unwrap();
103 assert_eq!(next.hour(), 12);
104 assert_eq!(next.day(), 1);
105 }
106}