subms_timer_wheel/features/
cron.rs1use std::fmt;
28
29#[derive(Debug, PartialEq, Eq)]
30pub enum CronError {
31 WrongFieldCount(usize),
33 InvalidField(&'static str, String),
35}
36
37impl fmt::Display for CronError {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 match self {
40 CronError::WrongFieldCount(n) => {
41 write!(f, "cron expression must have 5 fields, got {n}")
42 }
43 CronError::InvalidField(name, raw) => write!(f, "invalid {name} field: {raw}"),
44 }
45 }
46}
47
48impl std::error::Error for CronError {}
49
50#[derive(Debug, Clone)]
51pub struct CronSchedule {
52 minute: Vec<u8>,
53 hour: Vec<u8>,
54 dom: Vec<u8>,
55 month: Vec<u8>,
56 dow: Vec<u8>,
57}
58
59impl CronSchedule {
60 pub fn parse(expr: &str) -> Result<Self, CronError> {
62 let fields: Vec<&str> = expr.split_whitespace().collect();
63 if fields.len() != 5 {
64 return Err(CronError::WrongFieldCount(fields.len()));
65 }
66 Ok(Self {
67 minute: parse_field(fields[0], 0, 59, "minute")?,
68 hour: parse_field(fields[1], 0, 23, "hour")?,
69 dom: parse_field(fields[2], 1, 31, "day-of-month")?,
70 month: parse_field(fields[3], 1, 12, "month")?,
71 dow: parse_field(fields[4], 0, 6, "day-of-week")?,
72 })
73 }
74
75 pub fn minutes(&self) -> &[u8] {
76 &self.minute
77 }
78 pub fn hours(&self) -> &[u8] {
79 &self.hour
80 }
81 pub fn days_of_month(&self) -> &[u8] {
82 &self.dom
83 }
84 pub fn months(&self) -> &[u8] {
85 &self.month
86 }
87 pub fn days_of_week(&self) -> &[u8] {
88 &self.dow
89 }
90
91 pub fn next_after(&self, after_epoch: u64) -> Option<u64> {
96 let mut t = after_epoch.div_ceil(60) * 60;
98 let cap = after_epoch + 5 * 365 * 24 * 60 * 60;
99 while t < cap {
100 let (year, month, dom, dow, hour, minute) = civil_from_epoch(t);
101 if !self.minute.contains(&(minute as u8)) {
102 t += 60;
103 continue;
104 }
105 if !self.hour.contains(&(hour as u8)) {
106 t += 60;
107 continue;
108 }
109 if !self.month.contains(&(month as u8)) {
110 t += 60;
111 continue;
112 }
113 if !self.dom.contains(&(dom as u8)) {
119 t += 60;
120 continue;
121 }
122 if !self.dow.contains(&(dow as u8)) {
123 t += 60;
124 continue;
125 }
126 let _ = year;
127 return Some(t);
128 }
129 None
130 }
131}
132
133fn parse_field(s: &str, lo: u32, hi: u32, name: &'static str) -> Result<Vec<u8>, CronError> {
134 let mut out = Vec::new();
135 for part in s.split(',') {
136 let part = part.trim();
137 if part.is_empty() {
138 return Err(CronError::InvalidField(name, s.to_string()));
139 }
140 if let Some(rest) = part.strip_prefix("*/") {
142 let step: u32 = rest
143 .parse()
144 .map_err(|_| CronError::InvalidField(name, s.to_string()))?;
145 if step == 0 {
146 return Err(CronError::InvalidField(name, s.to_string()));
147 }
148 let mut v = lo;
149 while v <= hi {
150 out.push(v as u8);
151 v += step;
152 }
153 continue;
154 }
155 if part == "*" {
157 for v in lo..=hi {
158 out.push(v as u8);
159 }
160 continue;
161 }
162 if let Some((a, b)) = part.split_once('-') {
164 let a: u32 = a
165 .parse()
166 .map_err(|_| CronError::InvalidField(name, s.to_string()))?;
167 let b: u32 = b
168 .parse()
169 .map_err(|_| CronError::InvalidField(name, s.to_string()))?;
170 if a < lo || b > hi || a > b {
171 return Err(CronError::InvalidField(name, s.to_string()));
172 }
173 for v in a..=b {
174 out.push(v as u8);
175 }
176 continue;
177 }
178 let v: u32 = part
180 .parse()
181 .map_err(|_| CronError::InvalidField(name, s.to_string()))?;
182 if v < lo || v > hi {
183 return Err(CronError::InvalidField(name, s.to_string()));
184 }
185 out.push(v as u8);
186 }
187 out.sort_unstable();
188 out.dedup();
189 Ok(out)
190}
191
192fn civil_from_epoch(epoch: u64) -> (i32, u32, u32, u32, u32, u32) {
195 let days_since_epoch = (epoch / 86_400) as i64;
196 let secs_today = (epoch % 86_400) as u32;
197 let hour = secs_today / 3600;
198 let minute = (secs_today % 3600) / 60;
199 let dow = (((days_since_epoch + 4) % 7 + 7) % 7) as u32;
201
202 let z = days_since_epoch + 719_468;
204 let era = if z >= 0 {
205 z / 146_097
206 } else {
207 (z - 146_096) / 146_097
208 };
209 let doe = (z - era * 146_097) as u64;
210 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
211 let y = yoe as i64 + era * 400;
212 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
213 let mp = (5 * doy + 2) / 153;
214 let dom = (doy - (153 * mp + 2) / 5 + 1) as u32;
215 let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
216 let year = (y + if month <= 2 { 1 } else { 0 }) as i32;
217 (year, month, dom, dow, hour, minute)
218}
219
220pub struct CronScheduler {
224 schedule: CronSchedule,
225 last_fire_epoch: u64,
226}
227
228impl CronScheduler {
229 pub fn new(schedule: CronSchedule, now_epoch: u64) -> Self {
230 Self {
231 schedule,
232 last_fire_epoch: now_epoch,
233 }
234 }
235
236 pub fn schedule(&self) -> &CronSchedule {
237 &self.schedule
238 }
239
240 pub fn next_fire(&self, now_epoch: u64) -> Option<u64> {
244 let after = now_epoch.max(self.last_fire_epoch + 1);
245 self.schedule.next_after(after)
246 }
247
248 pub fn record_fire(&mut self, epoch: u64) {
251 self.last_fire_epoch = epoch;
252 }
253}
254
255#[cfg(test)]
256#[path = "cron_tests.rs"]
257mod tests;