rust_web_server/scheduler/
cron.rs1use std::collections::HashSet;
2
3#[derive(Debug, Clone)]
5pub(crate) enum Field {
6 Any,
7 Values(HashSet<u32>),
8}
9
10impl Field {
11 pub(crate) fn matches(&self, v: u32) -> bool {
12 match self {
13 Field::Any => true,
14 Field::Values(set) => set.contains(&v),
15 }
16 }
17
18 pub(crate) fn parse(s: &str, min: u32, max: u32) -> Result<Self, String> {
21 if s == "*" {
22 return Ok(Field::Any);
23 }
24 let mut values = HashSet::new();
25 for part in s.split(',') {
26 if let Some(step_expr) = part.strip_prefix("*/") {
27 let step: u32 = step_expr
28 .parse()
29 .map_err(|_| format!("invalid step '{}' in cron field", part))?;
30 if step == 0 {
31 return Err("cron step must be > 0".to_string());
32 }
33 let mut v = min;
34 while v <= max {
35 values.insert(v);
36 v += step;
37 }
38 } else if let Some(dash) = part.find('-') {
39 let lo: u32 = part[..dash]
40 .parse()
41 .map_err(|_| format!("invalid range start in '{}'", part))?;
42 let hi: u32 = part[dash + 1..]
43 .parse()
44 .map_err(|_| format!("invalid range end in '{}'", part))?;
45 if lo > hi {
46 return Err(format!("range {}-{} is empty", lo, hi));
47 }
48 if lo < min || hi > max {
49 return Err(format!(
50 "range {}-{} is out of bounds [{}, {}]",
51 lo, hi, min, max
52 ));
53 }
54 for v in lo..=hi {
55 values.insert(v);
56 }
57 } else {
58 let v: u32 = part
59 .parse()
60 .map_err(|_| format!("invalid cron value '{}'", part))?;
61 if v < min || v > max {
62 return Err(format!(
63 "value {} is out of bounds [{}, {}]",
64 v, min, max
65 ));
66 }
67 values.insert(v);
68 }
69 }
70 Ok(Field::Values(values))
71 }
72}
73
74#[derive(Debug, Clone)]
84pub struct CronSchedule {
85 pub(crate) seconds: Field,
86 pub(crate) minutes: Field,
87 pub(crate) hours: Field,
88 pub(crate) days_of_month: Field,
89 pub(crate) months: Field,
90 pub(crate) days_of_week: Field,
91}
92
93impl CronSchedule {
94 pub fn parse(expr: &str) -> Result<Self, String> {
99 let parts: Vec<&str> = expr.split_whitespace().collect();
100 if parts.len() != 6 {
101 return Err(format!(
102 "expected 6 cron fields (sec min hour day month weekday), got {}",
103 parts.len()
104 ));
105 }
106 Ok(CronSchedule {
107 seconds: Field::parse(parts[0], 0, 59)?,
108 minutes: Field::parse(parts[1], 0, 59)?,
109 hours: Field::parse(parts[2], 0, 23)?,
110 days_of_month: Field::parse(parts[3], 1, 31)?,
111 months: Field::parse(parts[4], 1, 12)?,
112 days_of_week: Field::parse(parts[5], 0, 6)?,
113 })
114 }
115
116 pub fn matches_now(&self) -> bool {
118 let secs = std::time::SystemTime::now()
119 .duration_since(std::time::UNIX_EPOCH)
120 .unwrap_or_default()
121 .as_secs();
122 self.matches_epoch(secs)
123 }
124
125 pub fn matches_epoch(&self, epoch_secs: u64) -> bool {
127 let (sec, min, hour, day, month, dow) = epoch_to_datetime(epoch_secs);
128 self.seconds.matches(sec)
129 && self.minutes.matches(min)
130 && self.hours.matches(hour)
131 && self.days_of_month.matches(day)
132 && self.months.matches(month)
133 && self.days_of_week.matches(dow)
134 }
135}
136
137pub(crate) fn epoch_to_datetime(epoch_secs: u64) -> (u32, u32, u32, u32, u32, u32) {
140 let sec = (epoch_secs % 60) as u32;
141 let mins_total = epoch_secs / 60;
142 let min = (mins_total % 60) as u32;
143 let hours_total = mins_total / 60;
144 let hour = (hours_total % 24) as u32;
145 let days_total = hours_total / 24;
146
147 let dow = ((days_total + 4) % 7) as u32;
149
150 let (_, month, day) = days_to_ymd(days_total);
151 (sec, min, hour, day, month, dow)
152}
153
154pub(crate) fn days_to_ymd(days: u64) -> (u32, u32, u32) {
157 let z = days + 719468;
158 let era = z / 146097;
159 let doe = z - era * 146097;
160 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
161 let y = yoe + era * 400;
162 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
163 let mp = (5 * doy + 2) / 153;
164 let d = doy - (153 * mp + 2) / 5 + 1;
165 let m = if mp < 10 { mp + 3 } else { mp - 9 };
166 let y = if m <= 2 { y + 1 } else { y };
167 (y as u32, m as u32, d as u32)
168}