1use chrono::{Datelike, Local, Timelike};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum DayOfWeek {
11 Sunday = 0,
12 Monday = 1,
13 Tuesday = 2,
14 Wednesday = 3,
15 Thursday = 4,
16 Friday = 5,
17 Saturday = 6,
18}
19
20impl DayOfWeek {
21 pub fn from_chrono(weekday: chrono::Weekday) -> Self {
23 match weekday {
24 chrono::Weekday::Sun => DayOfWeek::Sunday,
25 chrono::Weekday::Mon => DayOfWeek::Monday,
26 chrono::Weekday::Tue => DayOfWeek::Tuesday,
27 chrono::Weekday::Wed => DayOfWeek::Wednesday,
28 chrono::Weekday::Thu => DayOfWeek::Thursday,
29 chrono::Weekday::Fri => DayOfWeek::Friday,
30 chrono::Weekday::Sat => DayOfWeek::Saturday,
31 }
32 }
33}
34
35#[derive(Debug, Clone)]
55pub struct CronExpression {
56 raw: String,
57 minute: CronField,
59 hour: CronField,
61 day_of_month: CronField,
63 month: CronField,
65 day_of_week: CronField,
67}
68
69#[derive(Debug, Clone)]
70enum CronField {
71 Any, Value(u32), Range(u32, u32), Step(u32), List(Vec<u32>), StepFrom(u32, u32), }
78
79impl CronField {
80 fn matches(&self, value: u32) -> bool {
81 match self {
82 CronField::Any => true,
83 CronField::Value(v) => *v == value,
84 CronField::Range(start, end) => value >= *start && value <= *end,
85 CronField::Step(step) => value.is_multiple_of(*step),
86 CronField::StepFrom(start, step) => {
87 value >= *start && (value - start).is_multiple_of(*step)
88 }
89 CronField::List(values) => values.contains(&value),
90 }
91 }
92
93 fn parse(s: &str) -> Result<Self, String> {
94 if s == "*" {
95 return Ok(CronField::Any);
96 }
97
98 if let Some(step_str) = s.strip_prefix("*/") {
100 let step: u32 = step_str
101 .parse()
102 .map_err(|_| format!("Invalid step value in '{s}'"))?;
103 return Ok(CronField::Step(step));
104 }
105
106 if s.contains('/') && !s.starts_with('*') {
108 let parts: Vec<&str> = s.split('/').collect();
109 if parts.len() == 2 {
110 let start: u32 = parts[0]
111 .parse()
112 .map_err(|_| format!("Invalid start value in '{s}'"))?;
113 let step: u32 = parts[1]
114 .parse()
115 .map_err(|_| format!("Invalid step value in '{s}'"))?;
116 return Ok(CronField::StepFrom(start, step));
117 }
118 }
119
120 if s.contains(',') {
122 let values: Result<Vec<u32>, _> = s.split(',').map(|v| v.trim().parse()).collect();
123 return Ok(CronField::List(
124 values.map_err(|_| format!("Invalid list value in '{s}'"))?,
125 ));
126 }
127
128 if s.contains('-') {
130 let parts: Vec<&str> = s.split('-').collect();
131 if parts.len() == 2 {
132 let start: u32 = parts[0]
133 .parse()
134 .map_err(|_| format!("Invalid range start in '{s}'"))?;
135 let end: u32 = parts[1]
136 .parse()
137 .map_err(|_| format!("Invalid range end in '{s}'"))?;
138 return Ok(CronField::Range(start, end));
139 }
140 }
141
142 let value: u32 = s.parse().map_err(|_| format!("Invalid value in '{s}'"))?;
144 Ok(CronField::Value(value))
145 }
146}
147
148impl std::fmt::Display for CronField {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 match self {
151 CronField::Any => write!(f, "*"),
152 CronField::Value(v) => write!(f, "{v}"),
153 CronField::Range(s, e) => write!(f, "{s}-{e}"),
154 CronField::Step(s) => write!(f, "*/{s}"),
155 CronField::StepFrom(start, step) => write!(f, "{start}/{step}"),
156 CronField::List(l) => {
157 let s: String = l
158 .iter()
159 .map(|v| v.to_string())
160 .collect::<Vec<_>>()
161 .join(",");
162 write!(f, "{s}")
163 }
164 }
165 }
166}
167
168impl CronExpression {
169 pub fn parse(expression: &str) -> Result<Self, String> {
181 let parts: Vec<&str> = expression.split_whitespace().collect();
182
183 if parts.len() != 5 {
184 return Err(format!(
185 "Cron expression must have 5 fields, got {}",
186 parts.len()
187 ));
188 }
189
190 Ok(Self {
191 raw: expression.to_string(),
192 minute: CronField::parse(parts[0])?,
193 hour: CronField::parse(parts[1])?,
194 day_of_month: CronField::parse(parts[2])?,
195 month: CronField::parse(parts[3])?,
196 day_of_week: CronField::parse(parts[4])?,
197 })
198 }
199
200 pub fn is_due(&self) -> bool {
202 let now = Local::now();
203
204 self.minute.matches(now.minute())
205 && self.hour.matches(now.hour())
206 && self.day_of_month.matches(now.day())
207 && self.month.matches(now.month())
208 && self
209 .day_of_week
210 .matches(now.weekday().num_days_from_sunday())
211 }
212
213 pub fn expression(&self) -> &str {
215 &self.raw
216 }
217
218 pub fn at(mut self, time: &str) -> Self {
220 let parts: Vec<&str> = time.split(':').collect();
221 if parts.len() == 2 {
222 if let (Ok(hour), Ok(minute)) = (parts[0].parse::<u32>(), parts[1].parse::<u32>()) {
223 self.hour = CronField::Value(hour);
224 self.minute = CronField::Value(minute);
225 self.raw = format!(
226 "{} {} {} {} {}",
227 minute, hour, self.day_of_month, self.month, self.day_of_week,
228 );
229 }
230 }
231 self
232 }
233
234 pub fn every_minute() -> Self {
240 Self::parse("* * * * *").expect("valid cron: every minute")
241 }
242
243 pub fn every_n_minutes(n: u32) -> Self {
248 assert!(n > 0, "interval must be > 0");
249 Self::parse(&format!("*/{n} * * * *")).expect("valid cron: every N minutes")
250 }
251
252 pub fn hourly() -> Self {
254 Self::parse("0 * * * *").expect("valid cron: hourly")
255 }
256
257 pub fn hourly_at(minute: u32) -> Self {
262 assert!(minute < 60, "minute must be 0-59, got {minute}");
263 Self::parse(&format!("{minute} * * * *")).expect("valid cron: hourly at")
264 }
265
266 pub fn daily() -> Self {
268 Self::parse("0 0 * * *").expect("valid cron: daily")
269 }
270
271 pub fn daily_at(time: &str) -> Self {
275 let parts: Vec<&str> = time.split(':').collect();
276 if parts.len() == 2 {
277 let hour: u32 = parts[0].parse().unwrap_or(0);
278 let minute: u32 = parts[1].parse().unwrap_or(0);
279 Self::parse(&format!("{} {} * * *", minute.min(59), hour.min(23)))
280 .expect("valid cron: daily at")
281 } else {
282 Self::daily()
283 }
284 }
285
286 pub fn weekly() -> Self {
288 Self::parse("0 0 * * 0").expect("valid cron: weekly")
289 }
290
291 pub fn weekly_on(day: DayOfWeek) -> Self {
293 Self::parse(&format!("0 0 * * {}", day as u32)).expect("valid cron: weekly on")
294 }
295
296 pub fn on_days(days: &[DayOfWeek]) -> Self {
301 assert!(!days.is_empty(), "days must not be empty");
302 let days_str: Vec<String> = days.iter().map(|d| (*d as u32).to_string()).collect();
303 Self::parse(&format!("0 0 * * {}", days_str.join(","))).expect("valid cron: on days")
304 }
305
306 pub fn monthly() -> Self {
308 Self::parse("0 0 1 * *").expect("valid cron: monthly")
309 }
310
311 pub fn monthly_on(day: u32) -> Self {
316 assert!((1..=31).contains(&day), "day must be 1-31, got {day}");
317 Self::parse(&format!("0 0 {day} * *")).expect("valid cron: monthly on")
318 }
319
320 pub fn quarterly() -> Self {
322 Self::parse("0 0 1 1,4,7,10 *").expect("valid cron: quarterly")
323 }
324
325 pub fn yearly() -> Self {
327 Self::parse("0 0 1 1 *").expect("valid cron: yearly")
328 }
329
330 pub fn weekdays() -> Self {
332 Self::parse("0 0 * * 1-5").expect("valid cron: weekdays")
333 }
334
335 pub fn weekends() -> Self {
337 Self::parse("0 0 * * 0,6").expect("valid cron: weekends")
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn test_parse_every_minute() {
347 let expr = CronExpression::parse("* * * * *").unwrap();
348 assert_eq!(expr.expression(), "* * * * *");
349 }
350
351 #[test]
352 fn test_parse_specific_time() {
353 let expr = CronExpression::parse("30 14 * * *").unwrap();
354 assert_eq!(expr.expression(), "30 14 * * *");
355 }
356
357 #[test]
358 fn test_parse_invalid_expression() {
359 let result = CronExpression::parse("* * *");
360 assert!(result.is_err());
361 }
362
363 #[test]
364 fn test_factory_methods() {
365 assert_eq!(CronExpression::every_minute().expression(), "* * * * *");
366 assert_eq!(CronExpression::hourly().expression(), "0 * * * *");
367 assert_eq!(CronExpression::daily().expression(), "0 0 * * *");
368 assert_eq!(CronExpression::weekly().expression(), "0 0 * * 0");
369 assert_eq!(CronExpression::monthly().expression(), "0 0 1 * *");
370 }
371
372 #[test]
373 fn test_daily_at() {
374 let expr = CronExpression::daily_at("03:30");
375 assert_eq!(expr.expression(), "30 3 * * *");
376 }
377
378 #[test]
379 fn test_at_modifier() {
380 let expr = CronExpression::daily().at("14:30");
381 assert_eq!(expr.expression(), "30 14 * * *");
382 }
383}