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 matches_epoch(&self, epoch_secs: u64) -> bool {
136 let (sec, min, hour, day, month, dow) = epoch_to_datetime(epoch_secs);
137 self.seconds.matches(sec)
138 && self.minutes.matches(min)
139 && self.hours.matches(hour)
140 && self.days_of_month.matches(day)
141 && self.months.matches(month)
142 && self.days_of_week.matches(dow)
143 }
144}
145
146#[doc(hidden)]
149pub fn epoch_to_datetime(epoch_secs: u64) -> (u32, u32, u32, u32, u32, u32) {
150 let sec = (epoch_secs % 60) as u32;
151 let mins_total = epoch_secs / 60;
152 let min = (mins_total % 60) as u32;
153 let hours_total = mins_total / 60;
154 let hour = (hours_total % 24) as u32;
155 let days_total = hours_total / 24;
156
157 let dow = ((days_total + 4) % 7) as u32;
159
160 let (_, month, day) = days_to_ymd(days_total);
161 (sec, min, hour, day, month, dow)
162}
163
164use crate::http::date::days_to_ymd;