next_web_utils/cron/
cron_util.rs

1use chrono::{DateTime, Datelike, Duration, Local, Timelike};
2use std::fmt;
3
4/// Cron 表达式结构
5/// Cron expression structure
6#[derive(Debug, Clone)]
7pub struct CronExpression {
8    pub minute: Vec<u8>,      // 分钟 (0-59) / Minutes (0-59)
9    pub hour: Vec<u8>,        // 小时 (0-23) / Hours (0-23)
10    pub day_of_month: Vec<u8>, // 日期 (1-31) / Day of month (1-31)
11    pub month: Vec<u8>,       // 月份 (1-12) / Month (1-12)
12    pub day_of_week: Vec<u8>, // 星期 (0-6 或 1-7,0/7 都表示星期日) / Day of week (0-6 or 1-7, 0/7 both represent Sunday)
13    pub expression: String,   // 原始表达式 / Original expression
14}
15
16/// Cron 表达式解析错误
17/// Cron expression parsing error
18#[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    /// 解析 cron 表达式为结构体
41    /// Parse cron expression into a struct
42    /// 
43    /// # 参数
44    /// # Parameters
45    /// * `expression` - cron 表达式,如 "0 0 * * *"
46    /// * `expression` - cron expression, e.g. "0 0 * * *"
47    /// 
48    /// # 示例
49    /// # Example
50    /// ```
51    /// let cron = CronUtil::parse("0 0 * * *").unwrap();
52    /// ```
53    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    /// 解析 cron 字段
79    /// Parse cron field
80    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                // 处理步长: */5, 1-30/5
90                // Handle step: */5, 1-30/5
91                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                // 处理范围: 1-5
122                // Handle range: 1-5
123                let range_values = Self::parse_range(part, min, max)?;
124                values.extend(range_values);
125            } else {
126                // 单一值
127                // Single value
128                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        // 去重并排序
137        // Remove duplicates and sort
138        values.sort();
139        values.dedup();
140        
141        Ok(values)
142    }
143    
144    /// 解析范围表达式
145    /// Parse range expression
146    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    /// 验证值是否在允许范围内
173    /// Validate if value is within allowed range
174    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    /// 验证 cron 表达式是否有效
184    /// Validate if a cron expression is valid
185    /// 
186    /// # 参数
187    /// # Parameters
188    /// * `expression` - cron 表达式,如 "0 0 * * *"
189    /// * `expression` - cron expression, e.g. "0 0 * * *"
190    /// 
191    /// # 示例
192    /// # Example
193    /// ```
194    /// if CronUtil::validate("0 0 * * *") {
195    ///     println!("Valid cron expression");
196    /// }
197    /// ```
198    pub fn validate(expression: &str) -> bool {
199        Self::parse(expression).is_ok()
200    }
201    
202    /// 从当前时间计算下一次执行时间
203    /// Calculate next execution time from current time
204    /// 
205    /// # 参数
206    /// # Parameters
207    /// * `expression` - cron 表达式
208    /// * `expression` - cron expression
209    /// 
210    /// # 示例
211    /// # Example
212    /// ```
213    /// let next = CronUtil::next_execution("0 0 * * *").unwrap();
214    /// println!("Next execution time: {}", next);
215    /// ```
216    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    /// 从指定时间计算下一次执行时间
222    /// Calculate next execution time from a specific time
223    /// 
224    /// # 参数
225    /// # Parameters
226    /// * `expression` - cron 表达式
227    /// * `expression` - cron expression
228    /// * `from` - 起始时间
229    /// * `from` - start time
230    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        // 最多迭代 2 年,避免无限循环
241        // Iterate at most 2 years to avoid infinite loop
242        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    /// 添加月数到日期
293    /// Add months to a date
294    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    /// 生成 cron 表达式的人类可读描述
315    /// Generate human-readable description of a cron expression
316    /// 
317    /// # 参数
318    /// # Parameters
319    /// * `expression` - cron 表达式
320    /// * `expression` - cron expression
321    /// 
322    /// # 示例
323    /// # Example
324    /// ```
325    /// let desc = CronUtil::describe("0 0 * * *").unwrap();
326    /// println!("{}", desc); // "Every day at 00:00"
327    /// ```
328    pub fn describe(expression: &str) -> Result<String, CronError> {
329        let cron = Self::parse(expression)?;
330        
331        // 特殊情况处理
332        // Handle special cases
333        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        // 一般描述生成
346        // General description generation
347        let mut desc = String::new();
348        
349        // 处理星期
350        // Handle day of week
351        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(); // 移除最后的逗号 / Remove last comma
360                desc.pop(); // 移除最后的空格 / Remove last space
361                desc.push(' ');
362            }
363        } else {
364            desc.push_str("Every day ");
365        }
366        
367        // 处理月份
368        // Handle month
369        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(); // 移除最后的逗号 / Remove last comma
378                desc.pop(); // 移除最后的空格 / Remove last space
379                desc.push(' ');
380            }
381        }
382        
383        // 处理时间
384        // Handle time
385        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(); // 移除最后的逗号 / Remove last comma
395                desc.pop(); // 移除最后的空格 / Remove last space
396                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(); // 移除最后的逗号 / Remove last comma
405                desc.pop(); // 移除最后的空格 / Remove last space
406            } 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(); // 移除最后的逗号 / Remove last comma
418                desc.pop(); // 移除最后的空格 / Remove last space
419            }
420        }
421        
422        Ok(desc)
423    }
424    
425    /// 获取星期几的中文名称
426    /// Get Chinese name for day of week
427    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    /// 获取星期几的英文名称
441    /// Get English name for day of week
442    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    /// 获取月份的英文名称
456    /// Get English name for month
457    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    /// 计算两个日期之间有多少次 cron 触发
476    /// Count number of cron executions between two dates
477    /// 
478    /// # 参数
479    /// # Parameters
480    /// * `expression` - cron 表达式
481    /// * `expression` - cron expression
482    /// * `start` - 开始时间
483    /// * `start` - start time
484    /// * `end` - 结束时间
485    /// * `end` - end time
486    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    /// 获取未来 n 次执行时间
515    /// Get next n execution times
516    /// 
517    /// # 参数
518    /// # Parameters
519    /// * `expression` - cron 表达式
520    /// * `expression` - cron expression
521    /// * `count` - 需要获取的次数
522    /// * `count` - number of times to get
523    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    /// 根据 cron 表达式判断某个时间点是否会触发
544    /// Check if a specific time matches a cron expression
545    /// 
546    /// # 参数
547    /// # Parameters
548    /// * `expression` - cron 表达式
549    /// * `expression` - cron expression
550    /// * `date_time` - 待检查的时间点
551    /// * `date_time` - time to check
552    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        // DOM 和 DOW 是 OR 关系
562        // DOM and DOW have an OR relationship
563        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    /// 解析常用的 cron 表达式别名
574    /// Parse common cron expression aliases
575    /// 
576    /// # 参数
577    /// # Parameters 
578    /// * `alias` - cron 表达式别名,如 "@daily", "@hourly"
579    /// * `alias` - cron expression alias, e.g. "@daily", "@hourly"
580    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    /// 根据给定的月、周、日、时、分生成 cron 表达式
592    /// Generate cron expression from given month, week day, day, hour and minute
593    /// 
594    /// # 参数
595    /// # Parameters
596    /// * `month` - 月份,1-12,0 表示所有月份
597    /// * `month` - Month, 1-12, 0 means all months
598    /// * `week_day` - 星期几,0-7(0 和 7 都表示星期日),-1 表示所有星期
599    /// * `week_day` - Day of week, 0-7 (0 and 7 both represent Sunday), -1 means all days
600    /// * `day` - 日期,1-31,0 表示所有日期
601    /// * `day` - Day of month, 1-31, 0 means all days
602    /// * `hour` - 小时,0-23,-1 表示所有小时
603    /// * `hour` - Hour, 0-23, -1 means all hours
604    /// * `minute` - 分钟,0-59,-1 表示所有分钟
605    /// * `minute` - Minute, 0-59, -1 means all minutes
606    /// 
607    /// # 返回
608    /// # Returns
609    /// * `String` - 生成的 cron 表达式
610    /// * `String` - Generated cron expression
611    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