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 % step == 0,
86 CronField::StepFrom(start, step) => value >= *start && (value - start) % step == 0,
87 CronField::List(values) => values.contains(&value),
88 }
89 }
90
91 fn parse(s: &str) -> Result<Self, String> {
92 if s == "*" {
93 return Ok(CronField::Any);
94 }
95
96 if s.starts_with("*/") {
98 let step: u32 = s[2..]
99 .parse()
100 .map_err(|_| format!("Invalid step value in '{}'", s))?;
101 return Ok(CronField::Step(step));
102 }
103
104 if s.contains('/') && !s.starts_with('*') {
106 let parts: Vec<&str> = s.split('/').collect();
107 if parts.len() == 2 {
108 let start: u32 = parts[0]
109 .parse()
110 .map_err(|_| format!("Invalid start value in '{}'", s))?;
111 let step: u32 = parts[1]
112 .parse()
113 .map_err(|_| format!("Invalid step value in '{}'", s))?;
114 return Ok(CronField::StepFrom(start, step));
115 }
116 }
117
118 if s.contains(',') {
120 let values: Result<Vec<u32>, _> = s.split(',').map(|v| v.trim().parse()).collect();
121 return Ok(CronField::List(
122 values.map_err(|_| format!("Invalid list value in '{}'", s))?,
123 ));
124 }
125
126 if s.contains('-') {
128 let parts: Vec<&str> = s.split('-').collect();
129 if parts.len() == 2 {
130 let start: u32 = parts[0]
131 .parse()
132 .map_err(|_| format!("Invalid range start in '{}'", s))?;
133 let end: u32 = parts[1]
134 .parse()
135 .map_err(|_| format!("Invalid range end in '{}'", s))?;
136 return Ok(CronField::Range(start, end));
137 }
138 }
139
140 let value: u32 = s
142 .parse()
143 .map_err(|_| format!("Invalid value in '{}'", s))?;
144 Ok(CronField::Value(value))
145 }
146
147 fn to_string(&self) -> String {
148 match self {
149 CronField::Any => "*".to_string(),
150 CronField::Value(v) => v.to_string(),
151 CronField::Range(s, e) => format!("{}-{}", s, e),
152 CronField::Step(s) => format!("*/{}", s),
153 CronField::StepFrom(start, step) => format!("{}/{}", start, step),
154 CronField::List(l) => l
155 .iter()
156 .map(|v| v.to_string())
157 .collect::<Vec<_>>()
158 .join(","),
159 }
160 }
161}
162
163impl CronExpression {
164 pub fn parse(expression: &str) -> Result<Self, String> {
176 let parts: Vec<&str> = expression.split_whitespace().collect();
177
178 if parts.len() != 5 {
179 return Err(format!(
180 "Cron expression must have 5 fields, got {}",
181 parts.len()
182 ));
183 }
184
185 Ok(Self {
186 raw: expression.to_string(),
187 minute: CronField::parse(parts[0])?,
188 hour: CronField::parse(parts[1])?,
189 day_of_month: CronField::parse(parts[2])?,
190 month: CronField::parse(parts[3])?,
191 day_of_week: CronField::parse(parts[4])?,
192 })
193 }
194
195 pub fn is_due(&self) -> bool {
197 let now = Local::now();
198
199 self.minute.matches(now.minute())
200 && self.hour.matches(now.hour())
201 && self.day_of_month.matches(now.day())
202 && self.month.matches(now.month())
203 && self.day_of_week.matches(now.weekday().num_days_from_sunday())
204 }
205
206 pub fn expression(&self) -> &str {
208 &self.raw
209 }
210
211 pub fn at(mut self, time: &str) -> Self {
213 let parts: Vec<&str> = time.split(':').collect();
214 if parts.len() == 2 {
215 if let (Ok(hour), Ok(minute)) = (parts[0].parse::<u32>(), parts[1].parse::<u32>()) {
216 self.hour = CronField::Value(hour);
217 self.minute = CronField::Value(minute);
218 self.raw = format!(
219 "{} {} {} {} {}",
220 minute,
221 hour,
222 self.day_of_month.to_string(),
223 self.month.to_string(),
224 self.day_of_week.to_string(),
225 );
226 }
227 }
228 self
229 }
230
231 pub fn every_minute() -> Self {
237 Self::parse("* * * * *").unwrap()
238 }
239
240 pub fn every_n_minutes(n: u32) -> Self {
242 Self::parse(&format!("*/{} * * * *", n)).unwrap()
243 }
244
245 pub fn hourly() -> Self {
247 Self::parse("0 * * * *").unwrap()
248 }
249
250 pub fn hourly_at(minute: u32) -> Self {
252 Self::parse(&format!("{} * * * *", minute)).unwrap()
253 }
254
255 pub fn daily() -> Self {
257 Self::parse("0 0 * * *").unwrap()
258 }
259
260 pub fn daily_at(time: &str) -> Self {
262 let parts: Vec<&str> = time.split(':').collect();
263 if parts.len() == 2 {
264 let hour = parts[0].parse().unwrap_or(0);
265 let minute = parts[1].parse().unwrap_or(0);
266 Self::parse(&format!("{} {} * * *", minute, hour)).unwrap()
267 } else {
268 Self::daily()
269 }
270 }
271
272 pub fn weekly() -> Self {
274 Self::parse("0 0 * * 0").unwrap()
275 }
276
277 pub fn weekly_on(day: DayOfWeek) -> Self {
279 Self::parse(&format!("0 0 * * {}", day as u32)).unwrap()
280 }
281
282 pub fn on_days(days: &[DayOfWeek]) -> Self {
284 let days_str: Vec<String> = days.iter().map(|d| (*d as u32).to_string()).collect();
285 Self::parse(&format!("0 0 * * {}", days_str.join(","))).unwrap()
286 }
287
288 pub fn monthly() -> Self {
290 Self::parse("0 0 1 * *").unwrap()
291 }
292
293 pub fn monthly_on(day: u32) -> Self {
295 Self::parse(&format!("0 0 {} * *", day)).unwrap()
296 }
297
298 pub fn quarterly() -> Self {
300 Self::parse("0 0 1 1,4,7,10 *").unwrap()
301 }
302
303 pub fn yearly() -> Self {
305 Self::parse("0 0 1 1 *").unwrap()
306 }
307
308 pub fn weekdays() -> Self {
310 Self::parse("0 0 * * 1-5").unwrap()
311 }
312
313 pub fn weekends() -> Self {
315 Self::parse("0 0 * * 0,6").unwrap()
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn test_parse_every_minute() {
325 let expr = CronExpression::parse("* * * * *").unwrap();
326 assert_eq!(expr.expression(), "* * * * *");
327 }
328
329 #[test]
330 fn test_parse_specific_time() {
331 let expr = CronExpression::parse("30 14 * * *").unwrap();
332 assert_eq!(expr.expression(), "30 14 * * *");
333 }
334
335 #[test]
336 fn test_parse_invalid_expression() {
337 let result = CronExpression::parse("* * *");
338 assert!(result.is_err());
339 }
340
341 #[test]
342 fn test_factory_methods() {
343 assert_eq!(CronExpression::every_minute().expression(), "* * * * *");
344 assert_eq!(CronExpression::hourly().expression(), "0 * * * *");
345 assert_eq!(CronExpression::daily().expression(), "0 0 * * *");
346 assert_eq!(CronExpression::weekly().expression(), "0 0 * * 0");
347 assert_eq!(CronExpression::monthly().expression(), "0 0 1 * *");
348 }
349
350 #[test]
351 fn test_daily_at() {
352 let expr = CronExpression::daily_at("03:30");
353 assert_eq!(expr.expression(), "30 3 * * *");
354 }
355
356 #[test]
357 fn test_at_modifier() {
358 let expr = CronExpression::daily().at("14:30");
359 assert_eq!(expr.expression(), "30 14 * * *");
360 }
361}