1use alloc::format;
18use alloc::string::String;
19use alloc::vec::Vec;
20
21use crate::tz::{LocalResult, Zone};
22use crate::{days_in_month, Civil, Timestamp};
23
24#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct Cron {
27 minutes: u64,
28 hours: u32,
29 dom: u32,
31 months: u16,
33 dow: u8,
35 dom_star: bool,
36 dow_star: bool,
37 src: String,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub struct CronError {
43 pub field: u8,
44 pub msg: &'static str,
45}
46
47const MONTHS: [&str; 12] = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
48const DAYS: [&str; 7] = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
49
50fn value(s: &str, field: u8, min: u32, max: u32) -> Result<u32, CronError> {
51 let l = s.to_ascii_lowercase();
52 if field == 3 {
53 if let Some(i) = MONTHS.iter().position(|m| *m == l) {
54 return Ok(i as u32 + 1);
55 }
56 }
57 if field == 4 {
58 if let Some(i) = DAYS.iter().position(|d| *d == l) {
59 return Ok(i as u32);
60 }
61 }
62 let mut v: u32 = 0;
63 if s.is_empty() || s.len() > 2 {
64 return Err(CronError { field, msg: "number expected" });
65 }
66 for c in s.bytes() {
67 if !c.is_ascii_digit() {
68 return Err(CronError { field, msg: "number expected" });
69 }
70 v = v * 10 + (c - b'0') as u32;
71 }
72 let hi = if field == 4 { 7 } else { max };
74 if v < min || v > hi {
75 return Err(CronError { field, msg: "value out of range" });
76 }
77 Ok(if field == 4 && v == 7 { 0 } else { v })
78}
79
80fn field(s: &str, field: u8, min: u32, max: u32) -> Result<(u64, bool), CronError> {
82 let mut bits = 0u64;
83 let mut star = false;
84 for part in s.split(',') {
85 let (range, step) = match part.split_once('/') {
86 Some((r, st)) => {
87 let n = value(st, 5, 1, 60).map_err(|_| CronError { field, msg: "bad step" })?;
88 (r, n)
89 }
90 None => (part, 1),
91 };
92 let (lo, hi) = if range == "*" {
93 if step == 1 && s == "*" {
94 star = true;
95 }
96 (min, max)
97 } else if let Some((a, hi_s)) = range.split_once('-') {
98 let a = value(a, field, min, max)?;
99 let mut b = value(hi_s, field, min, max)?;
100 if field == 4 && b == 0 && (a > 0 || matches!(hi_s, "7" | "07")) {
101 b = 7; }
103 if a > b {
104 return Err(CronError { field, msg: "range runs backwards" });
105 }
106 (a, b)
107 } else {
108 let v = value(range, field, min, max)?;
109 (v, if part.contains('/') { max } else { v })
110 };
111 let mut v = lo;
112 while v <= hi {
113 let b = if field == 4 && v == 7 { 0 } else { v };
114 bits |= 1 << b;
115 v += step;
116 }
117 }
118 if bits == 0 {
119 return Err(CronError { field, msg: "empty field" });
120 }
121 Ok((bits, star))
122}
123
124impl Cron {
125 pub fn parse(s: &str) -> Result<Cron, CronError> {
127 let t = s.trim();
128 let expanded = match t {
129 "@hourly" => "0 * * * *",
130 "@daily" | "@midnight" => "0 0 * * *",
131 "@weekly" => "0 0 * * 0",
132 "@monthly" => "0 0 1 * *",
133 "@yearly" | "@annually" => "0 0 1 1 *",
134 _ => t,
135 };
136 let f: Vec<&str> = expanded.split_whitespace().collect();
137 if f.len() != 5 {
138 return Err(CronError { field: 5, msg: "five fields expected" });
139 }
140 let (minutes, _) = field(f[0], 0, 0, 59)?;
141 let (hours, _) = field(f[1], 1, 0, 23)?;
142 let (dom, dom_star) = field(f[2], 2, 1, 31)?;
143 let (months, _) = field(f[3], 3, 1, 12)?;
144 let (dow, dow_star) = field(f[4], 4, 0, 6)?;
145 Ok(Cron {
146 minutes,
147 hours: hours as u32,
148 dom: dom as u32,
149 months: months as u16,
150 dow: dow as u8,
151 dom_star,
152 dow_star,
153 src: String::from(t),
154 })
155 }
156
157 pub fn as_str(&self) -> &str {
159 &self.src
160 }
161
162 pub fn matches_date(&self, y: i32, m: u8, d: u8) -> bool {
164 if !(1..=12).contains(&m) || !(1..=31).contains(&d) {
165 return false;
166 }
167 if self.months & (1 << m) == 0 {
168 return false;
169 }
170 let dom_ok = self.dom & (1 << d) != 0;
171 let wd = (crate::weekday(y, m, d) + 1) % 7; let dow_ok = self.dow & (1 << wd) != 0;
173 match (self.dom_star, self.dow_star) {
174 (true, true) => true,
175 (true, false) => dow_ok,
176 (false, true) => dom_ok,
177 (false, false) => dom_ok || dow_ok,
178 }
179 }
180
181 pub fn next_after(&self, after: Timestamp, zone: &Zone) -> Option<Timestamp> {
183 let mut c = zone.to_local(Timestamp::from_secs(after.secs));
185 c.second = 0;
186 c.nanos = 0;
187 let start = Civil { ..c };
188 let mut day = Civil::date(c.year, c.month, c.day);
189 let mut first_day = true;
190 for _ in 0..(366 * 5) {
191 if self.matches_date(day.year, day.month, day.day) {
192 for h in 0..24u8 {
193 if self.hours & (1 << h) == 0 || (first_day && h < start.hour) {
194 continue;
195 }
196 for mi in 0..60u8 {
197 if self.minutes & (1 << mi) == 0 || (first_day && h == start.hour && mi < start.minute) {
198 continue;
199 }
200 let cand = Civil::new(day.year, day.month, day.day, h, mi, 0);
201 let t = match zone.from_local(&cand) {
202 LocalResult::Single(t) => t,
203 LocalResult::Ambiguous(a, _) => a,
204 LocalResult::Gap { after: j } => j,
205 };
206 if t.secs > after.secs {
207 return Some(t);
208 }
209 }
210 }
211 }
212 day = day.add_days(1);
213 first_day = false;
214 while self.months & (1 << day.month) == 0 {
216 let dim = days_in_month(day.year, day.month) as i64;
217 day = day.add_days(dim - day.day as i64 + 1);
218 }
219 }
220 None
221 }
222
223 pub fn next_n(&self, after: Timestamp, zone: &Zone, n: usize) -> Vec<Timestamp> {
225 let mut out = Vec::with_capacity(n);
226 let mut t = after;
227 while out.len() < n {
228 match self.next_after(t, zone) {
229 Some(x) => {
230 out.push(x);
231 t = x;
232 }
233 None => break,
234 }
235 }
236 out
237 }
238
239 fn single(bits: u64) -> Option<u32> {
240 if bits.count_ones() == 1 { Some(bits.trailing_zeros()) } else { None }
241 }
242
243 pub fn describe(&self, en: bool) -> String {
245 let all_min = self.minutes == (1u64 << 60) - 1;
246 let all_hours = self.hours == (1u32 << 24) - 1;
247 let all_months = self.months == 0x1FFE;
248 let at = |h: u32, m: u32| format!("{:02}:{:02}", h, m);
249 let m1 = Cron::single(self.minutes);
250 let h1 = Cron::single(self.hours as u64);
251 if all_months && self.dom_star && self.dow_star {
252 if all_min && all_hours {
253 return String::from(if en { "Every minute" } else { "Jede Minute" });
254 }
255 if all_hours {
256 if let Some(m) = m1 {
257 return if en { format!("Hourly at minute {}", m) } else { format!("Stündlich zur Minute {}", m) };
258 }
259 let n = self.minutes.count_ones();
261 if n > 1 && 60 % n == 0 {
262 let step = 60 / n;
263 let mut ok = true;
264 for k in 0..n {
265 if self.minutes & (1 << (k * step)) == 0 {
266 ok = false;
267 }
268 }
269 if ok {
270 return if en { format!("Every {} minutes", step) } else { format!("Alle {} Minuten", step) };
271 }
272 }
273 }
274 if let (Some(m), Some(h)) = (m1, h1) {
275 return if en { format!("Daily at {}", at(h, m)) } else { format!("Täglich um {}", at(h, m)) };
276 }
277 if let Some(m) = m1 {
278 let n = self.hours.count_ones();
279 if n > 1 && 24 % n == 0 {
280 let step = 24 / n;
281 let first = self.hours.trailing_zeros();
282 let ok = first < step && (0..n).all(|k| self.hours & (1 << (first + k * step)) != 0);
283 if ok {
284 return if en {
285 format!("Every {} hours at minute {}", step, m)
286 } else {
287 format!("Alle {} Stunden zur Minute {}", step, m)
288 };
289 }
290 }
291 }
292 }
293 if let (Some(m), Some(h), true) = (m1, h1, all_months) {
294 if self.dom_star && !self.dow_star {
295 if self.dow == 0b0111110 {
296 return if en { format!("Weekdays at {}", at(h, m)) } else { format!("Werktags um {}", at(h, m)) };
297 }
298 let names_de = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"];
299 let names_en = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
300 let mut days = Vec::new();
301 for d in [1usize, 2, 3, 4, 5, 6, 0] {
302 if self.dow & (1 << d) != 0 {
303 days.push(if en { names_en[d] } else { names_de[d] });
304 }
305 }
306 let list = days.join(", ");
307 return if en { format!("Weekly on {} at {}", list, at(h, m)) } else { format!("Wöchentlich am {} um {}", list, at(h, m)) };
308 }
309 if !self.dom_star && self.dow_star {
310 if let Some(d) = Cron::single(self.dom as u64) {
311 return if en {
312 format!("Monthly on day {} at {}", d, at(h, m))
313 } else {
314 format!("Monatlich am {}. um {}", d, at(h, m))
315 };
316 }
317 }
318 }
319 if en { format!("Custom: {}", self.src) } else { format!("Benutzerdefiniert: {}", self.src) }
320 }
321}
322
323pub fn daily(h: u8, m: u8) -> String {
325 format!("{} {} * * *", m.min(59), h.min(23))
326}
327
328pub fn hourly(m: u8) -> String {
330 format!("{} * * * *", m.min(59))
331}
332
333pub fn weekly(mask: u8, h: u8, m: u8) -> String {
335 if mask & 0x7F == 0 || mask & 0x7F == 0x7F {
336 return daily(h, m);
337 }
338 let mut days = Vec::new();
339 for d in 0..7u8 {
340 if mask & (1 << d) != 0 {
341 days.push(format!("{}", (d + 1) % 7));
342 }
343 }
344 format!("{} {} * * {}", m.min(59), h.min(23), days.join(","))
345}
346
347pub fn monthly(day: u8, h: u8, m: u8) -> String {
349 format!("{} {} {} * *", m.min(59), h.min(23), day.clamp(1, 31))
350}
351
352pub fn classify(c: &Cron) -> (u8, u8, u8, u8, u8) {
355 let all_hours = c.hours == (1u32 << 24) - 1;
356 let all_months = c.months == 0x1FFE;
357 let m1 = Cron::single(c.minutes);
358 let h1 = Cron::single(c.hours as u64);
359 if !all_months {
360 return (0, 0, 0, 0, 0);
361 }
362 match (m1, h1, c.dom_star, c.dow_star) {
363 (Some(m), _, true, true) if all_hours => (1, 0, m as u8, 0, 0),
364 (Some(m), Some(h), true, true) => (2, h as u8, m as u8, 0x7F, 0),
365 (Some(m), Some(h), true, false) => {
366 let mut mask = 0u8;
367 for d in 0..7u8 {
368 if c.dow & (1 << ((d + 1) % 7)) != 0 {
369 mask |= 1 << d;
370 }
371 }
372 (3, h as u8, m as u8, mask, 0)
373 }
374 (Some(m), Some(h), false, true) => match Cron::single(c.dom as u64) {
375 Some(d) => (4, h as u8, m as u8, 0, d as u8),
376 None => (0, 0, 0, 0, 0),
377 },
378 _ => (0, 0, 0, 0, 0),
379 }
380}