1use chrono::{Datelike, Local, Timelike};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum DayOfWeek {
11 Sunday = 0,
13 Monday = 1,
15 Tuesday = 2,
17 Wednesday = 3,
19 Thursday = 4,
21 Friday = 5,
23 Saturday = 6,
25}
26
27impl DayOfWeek {
28 pub fn from_chrono(weekday: chrono::Weekday) -> Self {
30 match weekday {
31 chrono::Weekday::Sun => DayOfWeek::Sunday,
32 chrono::Weekday::Mon => DayOfWeek::Monday,
33 chrono::Weekday::Tue => DayOfWeek::Tuesday,
34 chrono::Weekday::Wed => DayOfWeek::Wednesday,
35 chrono::Weekday::Thu => DayOfWeek::Thursday,
36 chrono::Weekday::Fri => DayOfWeek::Friday,
37 chrono::Weekday::Sat => DayOfWeek::Saturday,
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
62pub struct CronExpression {
63 raw: String,
64 minute: CronField,
66 hour: CronField,
68 day_of_month: CronField,
70 month: CronField,
72 day_of_week: CronField,
74}
75
76#[derive(Debug, Clone)]
77enum CronField {
78 Any, Value(u32), Range(u32, u32), Step(u32), List(Vec<u32>), StepFrom(u32, u32), }
85
86impl CronField {
87 fn matches(&self, value: u32) -> bool {
88 match self {
89 CronField::Any => true,
90 CronField::Value(v) => *v == value,
91 CronField::Range(start, end) => value >= *start && value <= *end,
92 CronField::Step(step) => value.is_multiple_of(*step),
93 CronField::StepFrom(start, step) => {
94 value >= *start && (value - start).is_multiple_of(*step)
95 }
96 CronField::List(values) => values.contains(&value),
97 }
98 }
99
100 fn parse(s: &str) -> Result<Self, String> {
101 if s == "*" {
102 return Ok(CronField::Any);
103 }
104
105 if let Some(step_str) = s.strip_prefix("*/") {
107 let step: u32 = step_str
108 .parse()
109 .map_err(|_| format!("Invalid step value in '{s}'"))?;
110 return Ok(CronField::Step(step));
111 }
112
113 if s.contains('/') && !s.starts_with('*') {
115 let parts: Vec<&str> = s.split('/').collect();
116 if parts.len() == 2 {
117 let start: u32 = parts[0]
118 .parse()
119 .map_err(|_| format!("Invalid start value in '{s}'"))?;
120 let step: u32 = parts[1]
121 .parse()
122 .map_err(|_| format!("Invalid step value in '{s}'"))?;
123 return Ok(CronField::StepFrom(start, step));
124 }
125 }
126
127 if s.contains(',') {
129 let values: Result<Vec<u32>, _> = s.split(',').map(|v| v.trim().parse()).collect();
130 return Ok(CronField::List(
131 values.map_err(|_| format!("Invalid list value in '{s}'"))?,
132 ));
133 }
134
135 if s.contains('-') {
137 let parts: Vec<&str> = s.split('-').collect();
138 if parts.len() == 2 {
139 let start: u32 = parts[0]
140 .parse()
141 .map_err(|_| format!("Invalid range start in '{s}'"))?;
142 let end: u32 = parts[1]
143 .parse()
144 .map_err(|_| format!("Invalid range end in '{s}'"))?;
145 return Ok(CronField::Range(start, end));
146 }
147 }
148
149 let value: u32 = s.parse().map_err(|_| format!("Invalid value in '{s}'"))?;
151 Ok(CronField::Value(value))
152 }
153}
154
155impl std::fmt::Display for CronField {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 match self {
158 CronField::Any => write!(f, "*"),
159 CronField::Value(v) => write!(f, "{v}"),
160 CronField::Range(s, e) => write!(f, "{s}-{e}"),
161 CronField::Step(s) => write!(f, "*/{s}"),
162 CronField::StepFrom(start, step) => write!(f, "{start}/{step}"),
163 CronField::List(l) => {
164 let s: String = l
165 .iter()
166 .map(|v| v.to_string())
167 .collect::<Vec<_>>()
168 .join(",");
169 write!(f, "{s}")
170 }
171 }
172 }
173}
174
175impl CronExpression {
176 pub fn parse(expression: &str) -> Result<Self, String> {
188 let parts: Vec<&str> = expression.split_whitespace().collect();
189
190 if parts.len() != 5 {
191 return Err(format!(
192 "Cron expression must have 5 fields, got {}",
193 parts.len()
194 ));
195 }
196
197 Ok(Self {
198 raw: expression.to_string(),
199 minute: CronField::parse(parts[0])?,
200 hour: CronField::parse(parts[1])?,
201 day_of_month: CronField::parse(parts[2])?,
202 month: CronField::parse(parts[3])?,
203 day_of_week: CronField::parse(parts[4])?,
204 })
205 }
206
207 pub fn is_due(&self) -> bool {
209 let now = Local::now();
210
211 self.minute.matches(now.minute())
212 && self.hour.matches(now.hour())
213 && self.day_of_month.matches(now.day())
214 && self.month.matches(now.month())
215 && self
216 .day_of_week
217 .matches(now.weekday().num_days_from_sunday())
218 }
219
220 pub fn expression(&self) -> &str {
222 &self.raw
223 }
224
225 pub fn at(mut self, time: &str) -> Self {
227 let parts: Vec<&str> = time.split(':').collect();
228 if parts.len() == 2 {
229 if let (Ok(hour), Ok(minute)) = (parts[0].parse::<u32>(), parts[1].parse::<u32>()) {
230 self.hour = CronField::Value(hour);
231 self.minute = CronField::Value(minute);
232 self.raw = format!(
233 "{} {} {} {} {}",
234 minute, hour, self.day_of_month, self.month, self.day_of_week,
235 );
236 }
237 }
238 self
239 }
240
241 pub fn every_minute() -> Self {
247 Self::parse("* * * * *").expect("valid cron: every minute")
248 }
249
250 pub fn every_n_minutes(n: u32) -> Self {
255 assert!(n > 0, "interval must be > 0");
256 Self::parse(&format!("*/{n} * * * *")).expect("valid cron: every N minutes")
257 }
258
259 pub fn hourly() -> Self {
261 Self::parse("0 * * * *").expect("valid cron: hourly")
262 }
263
264 pub fn hourly_at(minute: u32) -> Self {
269 assert!(minute < 60, "minute must be 0-59, got {minute}");
270 Self::parse(&format!("{minute} * * * *")).expect("valid cron: hourly at")
271 }
272
273 pub fn daily() -> Self {
275 Self::parse("0 0 * * *").expect("valid cron: daily")
276 }
277
278 pub fn daily_at(time: &str) -> Self {
282 let parts: Vec<&str> = time.split(':').collect();
283 if parts.len() == 2 {
284 let hour: u32 = parts[0].parse().unwrap_or(0);
285 let minute: u32 = parts[1].parse().unwrap_or(0);
286 Self::parse(&format!("{} {} * * *", minute.min(59), hour.min(23)))
287 .expect("valid cron: daily at")
288 } else {
289 Self::daily()
290 }
291 }
292
293 pub fn weekly() -> Self {
295 Self::parse("0 0 * * 0").expect("valid cron: weekly")
296 }
297
298 pub fn weekly_on(day: DayOfWeek) -> Self {
300 Self::parse(&format!("0 0 * * {}", day as u32)).expect("valid cron: weekly on")
301 }
302
303 pub fn on_days(days: &[DayOfWeek]) -> Self {
308 assert!(!days.is_empty(), "days must not be empty");
309 let days_str: Vec<String> = days.iter().map(|d| (*d as u32).to_string()).collect();
310 Self::parse(&format!("0 0 * * {}", days_str.join(","))).expect("valid cron: on days")
311 }
312
313 pub fn monthly() -> Self {
315 Self::parse("0 0 1 * *").expect("valid cron: monthly")
316 }
317
318 pub fn monthly_on(day: u32) -> Self {
323 assert!((1..=31).contains(&day), "day must be 1-31, got {day}");
324 Self::parse(&format!("0 0 {day} * *")).expect("valid cron: monthly on")
325 }
326
327 pub fn quarterly() -> Self {
329 Self::parse("0 0 1 1,4,7,10 *").expect("valid cron: quarterly")
330 }
331
332 pub fn yearly() -> Self {
334 Self::parse("0 0 1 1 *").expect("valid cron: yearly")
335 }
336
337 pub fn weekdays() -> Self {
339 Self::parse("0 0 * * 1-5").expect("valid cron: weekdays")
340 }
341
342 pub fn weekends() -> Self {
344 Self::parse("0 0 * * 0,6").expect("valid cron: weekends")
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn test_parse_every_minute() {
354 let expr = CronExpression::parse("* * * * *").unwrap();
355 assert_eq!(expr.expression(), "* * * * *");
356 }
357
358 #[test]
359 fn test_parse_specific_time() {
360 let expr = CronExpression::parse("30 14 * * *").unwrap();
361 assert_eq!(expr.expression(), "30 14 * * *");
362 }
363
364 #[test]
365 fn test_parse_invalid_expression() {
366 let result = CronExpression::parse("* * *");
367 assert!(result.is_err());
368 }
369
370 #[test]
371 fn test_factory_methods() {
372 assert_eq!(CronExpression::every_minute().expression(), "* * * * *");
373 assert_eq!(CronExpression::hourly().expression(), "0 * * * *");
374 assert_eq!(CronExpression::daily().expression(), "0 0 * * *");
375 assert_eq!(CronExpression::weekly().expression(), "0 0 * * 0");
376 assert_eq!(CronExpression::monthly().expression(), "0 0 1 * *");
377 }
378
379 #[test]
380 fn test_daily_at() {
381 let expr = CronExpression::daily_at("03:30");
382 assert_eq!(expr.expression(), "30 3 * * *");
383 }
384
385 #[test]
386 fn test_at_modifier() {
387 let expr = CronExpression::daily().at("14:30");
388 assert_eq!(expr.expression(), "30 14 * * *");
389 }
390}