1use crate::contracts::{Schedule, ValidatedSchedule};
2use crate::{CronError, CronResult};
3use chrono::{DateTime, NaiveDate, Utc};
4use chrono_tz::Tz;
5use serde::{Deserialize, Serialize};
6use std::fmt;
7use std::time::Duration;
8use rand::RngExt;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum Month {
13 January = 1,
14 February = 2,
15 March = 3,
16 April = 4,
17 May = 5,
18 June = 6,
19 July = 7,
20 August = 8,
21 September = 9,
22 October = 10,
23 November = 11,
24 December = 12,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30pub enum Weekday {
31 Sunday = 0,
32 Monday = 1,
33 Tuesday = 2,
34 Wednesday = 3,
35 Thursday = 4,
36 Friday = 5,
37 Saturday = 6,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42enum CronField {
43 All,
45 Value(u32),
47 Range(u32, u32),
49 Step(Box<CronField>, u32),
51 List(Vec<CronField>),
53}
54
55impl fmt::Display for CronField {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 match self {
58 CronField::All => write!(f, "*"),
59 CronField::Value(v) => write!(f, "{}", v),
60 CronField::Range(start, end) => write!(f, "{}-{}", start, end),
61 CronField::Step(base, step) => write!(f, "{}/{}", base, step),
62 CronField::List(values) => {
63 let s: Vec<String> = values.iter().map(|v| v.to_string()).collect();
64 write!(f, "{}", s.join(","))
65 }
66 }
67 }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct CronExpression {
76 seconds: CronField,
77 minutes: CronField,
78 hours: CronField,
79 day_of_month: CronField,
80 month: CronField,
81 day_of_week: CronField,
82 year: CronField,
83
84 timezone: Option<Tz>,
85 jitter: Option<Duration>,
86 blackout_dates: Vec<NaiveDate>,
87
88 #[serde(skip)]
89 error: Option<String>,
90 #[serde(skip)]
91 validated: Option<ValidatedSchedule>,
92}
93
94impl Default for CronExpression {
95 fn default() -> Self {
96 Self {
97 seconds: CronField::Value(0),
98 minutes: CronField::All,
99 hours: CronField::All,
100 day_of_month: CronField::All,
101 month: CronField::All,
102 day_of_week: CronField::All,
103 year: CronField::All,
104 timezone: None,
105 jitter: None,
106 blackout_dates: Vec::new(),
107 error: None,
108 validated: None,
109 }
110 }
111}
112
113impl Schedule for CronExpression {
114 fn next_after(&self, after: &DateTime<Utc>, tz: Tz) -> Option<DateTime<Utc>> {
115 let active_tz = self.timezone.unwrap_or(tz);
116
117 let mut current_after = *after;
119
120 for _ in 0..100 {
121 let mut next = if let Some(v) = &self.validated {
123 v.next_after(¤t_after, active_tz)
124 } else if let Ok(v) = self.to_validated() {
125 v.next_after(¤t_after, active_tz)
126 } else {
127 return None;
128 }?;
129
130 if let Some(jitter_dur) = self.jitter {
132 let mut rng = rand::rng();
133 let millis = jitter_dur.as_millis();
134 if millis > 0 {
135 let offset = rng.random_range(0..millis);
136 next += Duration::from_millis(offset as u64);
137 }
138 }
139
140 let date = next.date_naive();
142 if self.blackout_dates.contains(&date) {
143 current_after = next;
144 continue;
145 }
146
147 return Some(next);
148 }
149
150 None
151 }
152}
153
154impl CronExpression {
155 pub fn builder() -> Self {
157 Self::default()
158 }
159
160 pub fn with_timezone(mut self, tz: Tz) -> Self {
162 self.timezone = Some(tz);
163 self
164 }
165
166 pub fn with_jitter(mut self, max_jitter: Duration) -> Self {
171 self.jitter = Some(max_jitter);
172 self
173 }
174
175 pub fn exclude_date(mut self, date: NaiveDate) -> Self {
177 self.blackout_dates.push(date);
178 self
179 }
180
181 pub fn exclude_dates(mut self, dates: Vec<NaiveDate>) -> Self {
183 self.blackout_dates.extend(dates);
184 self
185 }
186
187 pub fn hourly(mut self) -> Self {
190 self.seconds = CronField::Value(0);
191 self.minutes = CronField::Value(0);
192 self.hours = CronField::All;
193 self
194 }
195
196 pub fn daily(mut self) -> Self {
197 self.seconds = CronField::Value(0);
198 self.minutes = CronField::Value(0);
199 self.hours = CronField::Value(0);
200 self
201 }
202
203 pub fn weekly(mut self) -> Self {
204 self = self.daily();
205 self.day_of_week = CronField::Value(1);
206 self
207 }
208
209 pub fn monthly(mut self) -> Self {
210 self = self.daily();
211 self.day_of_month = CronField::Value(1);
212 self
213 }
214
215 fn validate_range(&mut self, val: u32, min: u32, max: u32, field: &str) {
218 if val < min || val > max {
219 self.error = Some(format!(
220 "Invalid value {} for field {}: must be between {} and {}",
221 val, field, min, max
222 ));
223 }
224 }
225
226 pub fn second(mut self, second: u32) -> Self {
227 self.validate_range(second, 0, 59, "seconds");
228 self.seconds = CronField::Value(second);
229 self
230 }
231
232 pub fn every_second(mut self) -> Self {
233 self.seconds = CronField::All;
234 self
235 }
236
237 pub fn seconds_interval(mut self, interval: u32) -> Self {
238 self.validate_range(interval, 1, 59, "seconds interval");
239 self.seconds = CronField::Step(Box::new(CronField::All), interval);
240 self
241 }
242
243 pub fn minute(mut self, minute: u32) -> Self {
244 self.validate_range(minute, 0, 59, "minutes");
245 self.minutes = CronField::Value(minute);
246 self
247 }
248
249 pub fn every_minute(mut self) -> Self {
250 self.minutes = CronField::All;
251 self
252 }
253
254 pub fn minutes_interval(mut self, interval: u32) -> Self {
255 self.validate_range(interval, 1, 59, "minutes interval");
256 self.minutes = CronField::Step(Box::new(CronField::All), interval);
257 self
258 }
259
260 pub fn hour(mut self, hour: u32) -> Self {
261 self.validate_range(hour, 0, 23, "hours");
262 self.hours = CronField::Value(hour);
263 self
264 }
265
266 pub fn hours_list(mut self, hours: &[u32]) -> Self {
267 let mut fields = Vec::new();
268 for &h in hours {
269 self.validate_range(h, 0, 23, "hours list");
270 fields.push(CronField::Value(h));
271 }
272 self.hours = CronField::List(fields);
273 self
274 }
275
276 pub fn every_hour(mut self) -> Self {
277 self.hours = CronField::All;
278 self
279 }
280
281 pub fn hours_range(mut self, start: u32, end: u32) -> Self {
282 self.validate_range(start, 0, 23, "hours range start");
283 self.validate_range(end, 0, 23, "hours range end");
284 if start >= end {
285 self.error = Some(format!(
286 "Hours range start ({}) must be less than end ({})",
287 start, end
288 ));
289 }
290 self.hours = CronField::Range(start, end);
291 self
292 }
293
294 pub fn day_of_month(mut self, day: u32) -> Self {
295 self.validate_range(day, 1, 31, "day of month");
296 self.day_of_month = CronField::Value(day);
297 self
298 }
299
300 pub fn month(mut self, month: Month) -> Self {
301 self.month = CronField::Value(month as u32);
302 self
303 }
304
305 pub fn day_of_week(mut self, day: Weekday) -> Self {
306 let cron_value = (day as u32) + 1;
309 self.day_of_week = CronField::Value(cron_value);
310 self
311 }
312
313 pub fn weekdays_only(mut self) -> Self {
314 self.day_of_week = CronField::Range(2, 6);
316 self
317 }
318
319 pub fn weekends_only(mut self) -> Self {
320 self.day_of_week = CronField::List(vec![CronField::Value(7), CronField::Value(1)]);
322 self
323 }
324
325 pub fn sundays_only(mut self) -> Self {
326 self.day_of_week = CronField::Value(1);
328 self
329 }
330
331 pub fn mondays_only(mut self) -> Self {
332 self.day_of_week = CronField::Value(2);
333 self
334 }
335
336 pub fn tuesdays_only(mut self) -> Self {
337 self.day_of_week = CronField::Value(3);
338 self
339 }
340
341 pub fn wednesdays_only(mut self) -> Self {
342 self.day_of_week = CronField::Value(4);
343 self
344 }
345
346 pub fn thursdays_only(mut self) -> Self {
347 self.day_of_week = CronField::Value(5);
348 self
349 }
350
351 pub fn fridays_only(mut self) -> Self {
352 self.day_of_week = CronField::Value(6);
353 self
354 }
355
356 pub fn saturdays_only(mut self) -> Self {
357 self.day_of_week = CronField::Value(7);
358 self
359 }
360
361 pub fn quarterly(mut self) -> Self {
362 self.month = CronField::List(vec![
363 CronField::Value(1), CronField::Value(4), CronField::Value(7), CronField::Value(10), ]);
368 self.day_of_month = CronField::Value(1);
369 self.seconds = CronField::Value(0);
370 self.minutes = CronField::Value(0);
371 self.hours = CronField::Value(0);
372 self
373 }
374
375 pub fn year(mut self, year: u32) -> Self {
376 self.year = CronField::Value(year);
377 self
378 }
379
380 pub fn build(&self) -> String {
381 format!(
382 "{} {} {} {} {} {} {}",
383 self.seconds,
384 self.minutes,
385 self.hours,
386 self.day_of_month,
387 self.month,
388 self.day_of_week,
389 self.year
390 )
391 }
392
393 pub fn to_validated(&self) -> CronResult<ValidatedSchedule> {
394 if let Some(err) = &self.error {
395 return Err(CronError::InvalidSchedule(err.clone()));
396 }
397 ValidatedSchedule::parse(&self.build())
398 }
399}
400
401impl fmt::Display for CronExpression {
402 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403 write!(f, "{}", self.build())
404 }
405}