rws_server/scheduler/
cron.rs1use std::collections::HashSet;
2
3#[derive(Debug, Clone)]
5#[doc(hidden)]
6pub enum Field {
7 Any,
8 Values(HashSet<u32>),
9}
10
11impl Field {
12 #[doc(hidden)]
13 pub fn matches(&self, v: u32) -> bool {
14 match self {
15 Field::Any => true,
16 Field::Values(set) => set.contains(&v),
17 }
18 }
19
20 #[doc(hidden)]
23 pub fn parse(s: &str, min: u32, max: u32) -> Result<Self, String> {
24 if s == "*" {
25 return Ok(Field::Any);
26 }
27 let mut values = HashSet::new();
28 for part in s.split(',') {
29 if let Some(step_expr) = part.strip_prefix("*/") {
30 let step: u32 = step_expr
31 .parse()
32 .map_err(|_| format!("invalid step '{}' in cron field", part))?;
33 if step == 0 {
34 return Err("cron step must be > 0".to_string());
35 }
36 let mut v = min;
37 while v <= max {
38 values.insert(v);
39 v += step;
40 }
41 } else if let Some(dash) = part.find('-') {
42 let lo: u32 = part[..dash]
43 .parse()
44 .map_err(|_| format!("invalid range start in '{}'", part))?;
45 let hi: u32 = part[dash + 1..]
46 .parse()
47 .map_err(|_| format!("invalid range end in '{}'", part))?;
48 if lo > hi {
49 return Err(format!("range {}-{} is empty", lo, hi));
50 }
51 if lo < min || hi > max {
52 return Err(format!(
53 "range {}-{} is out of bounds [{}, {}]",
54 lo, hi, min, max
55 ));
56 }
57 for v in lo..=hi {
58 values.insert(v);
59 }
60 } else {
61 let v: u32 = part
62 .parse()
63 .map_err(|_| format!("invalid cron value '{}'", part))?;
64 if v < min || v > max {
65 return Err(format!(
66 "value {} is out of bounds [{}, {}]",
67 v, min, max
68 ));
69 }
70 values.insert(v);
71 }
72 }
73 Ok(Field::Values(values))
74 }
75}
76
77#[derive(Debug, Clone)]
87pub struct CronSchedule {
88 #[doc(hidden)]
89 pub seconds: Field,
90 #[doc(hidden)]
91 pub minutes: Field,
92 #[doc(hidden)]
93 pub hours: Field,
94 #[doc(hidden)]
95 pub days_of_month: Field,
96 #[doc(hidden)]
97 pub months: Field,
98 #[doc(hidden)]
99 pub days_of_week: Field,
100}
101
102impl CronSchedule {
103 pub fn parse(expr: &str) -> Result<Self, String> {
108 let parts: Vec<&str> = expr.split_whitespace().collect();
109 if parts.len() != 6 {
110 return Err(format!(
111 "expected 6 cron fields (sec min hour day month weekday), got {}",
112 parts.len()
113 ));
114 }
115 Ok(CronSchedule {
116 seconds: Field::parse(parts[0], 0, 59)?,
117 minutes: Field::parse(parts[1], 0, 59)?,
118 hours: Field::parse(parts[2], 0, 23)?,
119 days_of_month: Field::parse(parts[3], 1, 31)?,
120 months: Field::parse(parts[4], 1, 12)?,
121 days_of_week: Field::parse(parts[5], 0, 6)?,
122 })
123 }
124
125 pub fn matches_now(&self) -> bool {
127 let secs = std::time::SystemTime::now()
128 .duration_since(std::time::UNIX_EPOCH)
129 .unwrap_or_default()
130 .as_secs();
131 self.matches_epoch(secs)
132 }
133
134 pub fn next_after(&self, epoch_secs: u64) -> Option<u64> {
138 let mut t = epoch_secs.checked_add(1)?;
139 let limit = epoch_secs.saturating_add(5 * 366 * 86_400);
140 while t <= limit {
141 let (sec, min, hour, day, month, dow) = epoch_to_datetime(t);
142 if !(self.months.matches(month) && self.days_of_month.matches(day) && self.days_of_week.matches(dow)) {
143 t = (t / 86_400 + 1) * 86_400;
144 } else if !self.hours.matches(hour) {
145 t = (t / 3_600 + 1) * 3_600;
146 } else if !self.minutes.matches(min) {
147 t = (t / 60 + 1) * 60;
148 } else if !self.seconds.matches(sec) {
149 t += 1;
150 } else {
151 return Some(t);
152 }
153 }
154 None
155 }
156
157 pub fn matches_epoch(&self, epoch_secs: u64) -> bool {
159 let (sec, min, hour, day, month, dow) = epoch_to_datetime(epoch_secs);
160 self.seconds.matches(sec)
161 && self.minutes.matches(min)
162 && self.hours.matches(hour)
163 && self.days_of_month.matches(day)
164 && self.months.matches(month)
165 && self.days_of_week.matches(dow)
166 }
167}
168
169#[doc(hidden)]
172pub fn epoch_to_datetime(epoch_secs: u64) -> (u32, u32, u32, u32, u32, u32) {
173 let sec = (epoch_secs % 60) as u32;
174 let mins_total = epoch_secs / 60;
175 let min = (mins_total % 60) as u32;
176 let hours_total = mins_total / 60;
177 let hour = (hours_total % 24) as u32;
178 let days_total = hours_total / 24;
179
180 let dow = ((days_total + 4) % 7) as u32;
182
183 let (_, month, day) = days_to_ymd(days_total);
184 (sec, min, hour, day, month, dow)
185}
186
187use crate::http::date::days_to_ymd;