1use chrono::{DateTime, Datelike, Duration, Local, Timelike};
2use std::fmt;
3
4#[derive(Debug, Clone)]
7pub struct CronExpression {
8 pub minute: Vec<u8>, pub hour: Vec<u8>, pub day_of_month: Vec<u8>, pub month: Vec<u8>, pub day_of_week: Vec<u8>, pub expression: String, }
15
16#[derive(Debug)]
19pub enum CronError {
20 InvalidExpression(String),
21 InvalidField(String),
22 InvalidRange(String),
23 InvalidValue(String),
24}
25
26impl fmt::Display for CronError {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 CronError::InvalidExpression(msg) => write!(f, "Invalid cron expression: {}", msg),
30 CronError::InvalidField(msg) => write!(f, "Invalid cron field: {}", msg),
31 CronError::InvalidRange(msg) => write!(f, "Invalid range: {}", msg),
32 CronError::InvalidValue(msg) => write!(f, "Invalid value: {}", msg),
33 }
34 }
35}
36
37pub struct CronUtil;
38
39impl CronUtil {
40 pub fn parse(expression: &str) -> Result<CronExpression, CronError> {
54 let parts: Vec<&str> = expression.split_whitespace().collect();
55
56 if parts.len() != 5 {
57 return Err(CronError::InvalidExpression(
58 format!("Expected to have 5 fields, actually there are {}", parts.len())
59 ));
60 }
61
62 let minute = Self::parse_field(parts[0], 0, 59)?;
63 let hour = Self::parse_field(parts[1], 0, 23)?;
64 let day_of_month = Self::parse_field(parts[2], 1, 31)?;
65 let month = Self::parse_field(parts[3], 1, 12)?;
66 let day_of_week = Self::parse_field(parts[4], 0, 6)?;
67
68 Ok(CronExpression {
69 minute,
70 hour,
71 day_of_month,
72 month,
73 day_of_week,
74 expression: expression.to_string(),
75 })
76 }
77
78 fn parse_field(field: &str, min: u8, max: u8) -> Result<Vec<u8>, CronError> {
81 if field == "*" {
82 return Ok((min..=max).collect());
83 }
84
85 let mut values = Vec::new();
86
87 for part in field.split(',') {
88 if part.contains('/') {
89 let segments: Vec<&str> = part.split('/').collect();
92 if segments.len() != 2 {
93 return Err(CronError::InvalidField(format!("Invalid step expression: {}", part)));
94 }
95
96 let range = segments[0];
97 let step = segments[1].parse::<u8>().map_err(|_| {
98 CronError::InvalidValue(format!("Invalid step value: {}", segments[1]))
99 })?;
100
101 if step == 0 {
102 return Err(CronError::InvalidValue("Step cannot be 0".to_string()));
103 }
104
105 let range_values = if range == "*" {
106 (min..=max).collect::<Vec<u8>>()
107 } else if range.contains('-') {
108 Self::parse_range(range, min, max)?
109 } else {
110 let value = range.parse::<u8>().map_err(|_| {
111 CronError::InvalidValue(format!("Invalid value: {}", range))
112 })?;
113 Self::validate_range(value, min, max)?;
114 vec![value]
115 };
116
117 for i in (0..range_values.len()).step_by(step as usize) {
118 values.push(range_values[i]);
119 }
120 } else if part.contains('-') {
121 let range_values = Self::parse_range(part, min, max)?;
124 values.extend(range_values);
125 } else {
126 let value = part.parse::<u8>().map_err(|_| {
129 CronError::InvalidValue(format!("Invalid field value: {}", part))
130 })?;
131 Self::validate_range(value, min, max)?;
132 values.push(value);
133 }
134 }
135
136 values.sort();
139 values.dedup();
140
141 Ok(values)
142 }
143
144 fn parse_range(range: &str, min: u8, max: u8) -> Result<Vec<u8>, CronError> {
147 let parts: Vec<&str> = range.split('-').collect();
148 if parts.len() != 2 {
149 return Err(CronError::InvalidRange(format!("Invalid range expression: {}", range)));
150 }
151
152 let start = parts[0].parse::<u8>().map_err(|_| {
153 CronError::InvalidValue(format!("Invalid range start value: {}", parts[0]))
154 })?;
155
156 let end = parts[1].parse::<u8>().map_err(|_| {
157 CronError::InvalidValue(format!("Invalid range end value: {}", parts[1]))
158 })?;
159
160 Self::validate_range(start, min, max)?;
161 Self::validate_range(end, min, max)?;
162
163 if start > end {
164 return Err(CronError::InvalidRange(
165 format!("Range start value ({}) greater than end value ({})", start, end)
166 ));
167 }
168
169 Ok((start..=end).collect())
170 }
171
172 fn validate_range(value: u8, min: u8, max: u8) -> Result<(), CronError> {
175 if value < min || value > max {
176 return Err(CronError::InvalidRange(
177 format!("Value {} out of allowed range {}-{}", value, min, max)
178 ));
179 }
180 Ok(())
181 }
182
183 pub fn validate(expression: &str) -> bool {
199 Self::parse(expression).is_ok()
200 }
201
202 pub fn next_execution(expression: &str) -> Result<DateTime<Local>, CronError> {
217 let now = Local::now();
218 Self::next_execution_from(expression, now)
219 }
220
221 pub fn next_execution_from(expression: &str, from: DateTime<Local>) -> Result<DateTime<Local>, CronError> {
231 let cron = Self::parse(expression)?;
232
233 let mut candidate = from + Duration::minutes(1);
234 candidate = candidate
235 .with_second(0)
236 .unwrap()
237 .with_nanosecond(0)
238 .unwrap();
239
240 let limit = from + Duration::days(366 * 2);
243
244 while candidate < limit {
245 let month = candidate.month() as u8;
246 if !cron.month.contains(&month) {
247 candidate = Self::add_months(candidate, 1).with_day(1).unwrap();
248 continue;
249 }
250
251 let dom = candidate.day() as u8;
252 if !cron.day_of_month.contains(&dom) {
253 candidate = candidate + Duration::days(1);
254 candidate = candidate
255 .with_hour(0)
256 .unwrap()
257 .with_minute(0)
258 .unwrap();
259 continue;
260 }
261
262 let dow = candidate.weekday().num_days_from_sunday() as u8;
263 if !cron.day_of_week.contains(&dow) && !cron.day_of_week.contains(&7) {
264 candidate = candidate + Duration::days(1);
265 candidate = candidate
266 .with_hour(0)
267 .unwrap()
268 .with_minute(0)
269 .unwrap();
270 continue;
271 }
272
273 let hour = candidate.hour() as u8;
274 if !cron.hour.contains(&hour) {
275 candidate = candidate + Duration::hours(1);
276 candidate = candidate.with_minute(0).unwrap();
277 continue;
278 }
279
280 let minute = candidate.minute() as u8;
281 if !cron.minute.contains(&minute) {
282 candidate = candidate + Duration::minutes(1);
283 continue;
284 }
285
286 return Ok(candidate);
287 }
288
289 Err(CronError::InvalidExpression("Cannot find next execution time, expression may be invalid or never executes".to_string()))
290 }
291
292 fn add_months(dt: DateTime<Local>, months: i32) -> DateTime<Local> {
295 let mut year = dt.year();
296 let mut month = dt.month() as i32 + months;
297
298 while month > 12 {
299 year += 1;
300 month -= 12;
301 }
302
303 while month < 1 {
304 year -= 1;
305 month += 12;
306 }
307
308 dt.with_year(year)
309 .unwrap()
310 .with_month(month as u32)
311 .unwrap()
312 }
313
314 pub fn describe(expression: &str) -> Result<String, CronError> {
329 let cron = Self::parse(expression)?;
330
331 if expression == "* * * * *" {
334 return Ok("Every minute".to_string());
335 }
336
337 if expression == "0 * * * *" {
338 return Ok("Every hour at the top of the hour".to_string());
339 }
340
341 if expression == "0 0 * * *" {
342 return Ok("Every day at 00:00".to_string());
343 }
344
345 let mut desc = String::new();
348
349 if cron.day_of_week.len() < 7 {
352 if cron.day_of_week.len() == 1 {
353 desc.push_str(&format!("Every {} ", Self::day_of_week_name_en(cron.day_of_week[0])));
354 } else {
355 desc.push_str("Every week on ");
356 for &day in &cron.day_of_week {
357 desc.push_str(&format!("{}, ", Self::day_of_week_name_en(day)));
358 }
359 desc.pop(); desc.pop(); desc.push(' ');
362 }
363 } else {
364 desc.push_str("Every day ");
365 }
366
367 if cron.month.len() < 12 {
370 if cron.month.len() == 1 {
371 desc.push_str(&format!("in {} ", Self::month_name_en(cron.month[0])));
372 } else {
373 desc.push_str("in ");
374 for &month in &cron.month {
375 desc.push_str(&format!("{}, ", Self::month_name_en(month)));
376 }
377 desc.pop(); desc.pop(); desc.push(' ');
380 }
381 }
382
383 if cron.hour.len() == 1 && cron.minute.len() == 1 {
386 desc.push_str(&format!("at {:02}:{:02}", cron.hour[0], cron.minute[0]));
387 } else if cron.hour.len() == 1 {
388 desc.push_str(&format!("at {:02}:xx", cron.hour[0]));
389 if cron.minute.len() <= 5 {
390 desc.push_str(" (minutes: ");
391 for &min in &cron.minute {
392 desc.push_str(&format!("{:02}, ", min));
393 }
394 desc.pop(); desc.pop(); desc.push(')');
397 }
398 } else {
399 if cron.hour.len() <= 5 {
400 desc.push_str("at hours: ");
401 for &hour in &cron.hour {
402 desc.push_str(&format!("{:02}, ", hour));
403 }
404 desc.pop(); desc.pop(); } else {
407 desc.push_str("every hour");
408 }
409
410 if cron.minute.len() == 1 {
411 desc.push_str(&format!(", minute: {:02}", cron.minute[0]));
412 } else if cron.minute.len() <= 5 {
413 desc.push_str(", minutes: ");
414 for &min in &cron.minute {
415 desc.push_str(&format!("{:02}, ", min));
416 }
417 desc.pop(); desc.pop(); }
420 }
421
422 Ok(desc)
423 }
424
425 fn day_of_week_name(day: u8) -> &'static str {
428 match day {
429 0 | 7 => "星期日",
430 1 => "星期一",
431 2 => "星期二",
432 3 => "星期三",
433 4 => "星期四",
434 5 => "星期五",
435 6 => "星期六",
436 _ => "未知",
437 }
438 }
439
440 fn day_of_week_name_en(day: u8) -> &'static str {
443 match day {
444 0 | 7 => "Sunday",
445 1 => "Monday",
446 2 => "Tuesday",
447 3 => "Wednesday",
448 4 => "Thursday",
449 5 => "Friday",
450 6 => "Saturday",
451 _ => "Unknown",
452 }
453 }
454
455 fn month_name_en(month: u8) -> &'static str {
458 match month {
459 1 => "January",
460 2 => "February",
461 3 => "March",
462 4 => "April",
463 5 => "May",
464 6 => "June",
465 7 => "July",
466 8 => "August",
467 9 => "September",
468 10 => "October",
469 11 => "November",
470 12 => "December",
471 _ => "Unknown",
472 }
473 }
474
475 pub fn count_executions_between(
487 expression: &str,
488 start: DateTime<Local>,
489 end: DateTime<Local>
490 ) -> Result<u32, CronError> {
491 if start >= end {
492 return Ok(0);
493 }
494
495 let mut count = 0;
496 let mut current = start;
497
498 while current < end {
499 match Self::next_execution_from(expression, current) {
500 Ok(next) => {
501 if next >= end {
502 break;
503 }
504 count += 1;
505 current = next + Duration::minutes(1);
506 },
507 Err(_) => break,
508 }
509 }
510
511 Ok(count)
512 }
513
514 pub fn next_n_executions(
524 expression: &str,
525 count: usize
526 ) -> Result<Vec<DateTime<Local>>, CronError> {
527 let mut executions = Vec::with_capacity(count);
528 let mut current = Local::now();
529
530 for _ in 0..count {
531 match Self::next_execution_from(expression, current) {
532 Ok(next) => {
533 executions.push(next);
534 current = next + Duration::minutes(1);
535 },
536 Err(e) => return Err(e),
537 }
538 }
539
540 Ok(executions)
541 }
542
543 pub fn matches(expression: &str, date_time: DateTime<Local>) -> Result<bool, CronError> {
553 let cron = Self::parse(expression)?;
554
555 let minute = date_time.minute() as u8;
556 let hour = date_time.hour() as u8;
557 let day = date_time.day() as u8;
558 let month = date_time.month() as u8;
559 let dow = date_time.weekday().num_days_from_sunday() as u8;
560
561 let day_matches = cron.day_of_month.contains(&day) ||
564 cron.day_of_week.contains(&dow) ||
565 (dow == 0 && cron.day_of_week.contains(&7));
566
567 Ok(cron.minute.contains(&minute) &&
568 cron.hour.contains(&hour) &&
569 cron.month.contains(&month) &&
570 day_matches)
571 }
572
573 pub fn parse_alias(alias: &str) -> Result<String, CronError> {
581 match alias {
582 "@yearly" | "@annually" => Ok("0 0 1 1 *".to_string()),
583 "@monthly" => Ok("0 0 1 * *".to_string()),
584 "@weekly" => Ok("0 0 * * 0".to_string()),
585 "@daily" | "@midnight" => Ok("0 0 * * *".to_string()),
586 "@hourly" => Ok("0 * * * *".to_string()),
587 _ => Err(CronError::InvalidExpression(format!("Unknown cron alias: {}", alias))),
588 }
589 }
590
591 pub fn generate_cron(month: i32, week_day: i32, day: i32, hour: i32, minute: i32) -> String {
612 let minute_str = if minute < 0 || minute > 59 { "*".to_string() } else { minute.to_string() };
613 let hour_str = if hour < 0 || hour > 23 { "*".to_string() } else { hour.to_string() };
614 let day_str = if day < 1 || day > 31 { "*".to_string() } else { day.to_string() };
615
616 let month_str = if month < 1 || month > 12 {
617 "*".to_string()
618 } else {
619 month.to_string()
620 };
621
622 let week_day_str = if week_day < 0 || week_day > 7 {
623 "*".to_string()
624 } else {
625 week_day.to_string()
626 };
627
628 format!("{} {} {} {} {}", minute_str, hour_str, day_str, month_str, week_day_str)
629 }
630}
631
632
633