Skip to main content

alien_core/
crontab_to_eventbridge.rs

1/// Converts a standard crontab expression to an AWS EventBridge cron or rate expression.
2///
3/// # Crontab Format
4/// A crontab expression has 5 or 6 fields:
5/// `minute hour day-of-month month day-of-week [year]`
6///
7/// # Conversion Rules
8///
9/// ## Rate expressions
10/// Simple periodic schedules are converted to EventBridge `rate()` expressions:
11/// - `* * * * *` -> `rate(1 minute)`
12/// - `0 * * * *` -> `rate(1 hour)`
13/// - `0 0 * * *` -> `rate(1 day)`
14/// - `*/N * * * *` -> `rate(N minutes)`
15/// - `0 */N * * *` -> `rate(N hours)`
16/// - `0 0 */N * *` -> `rate(N days)`
17///
18/// ## Cron expressions
19/// All other expressions are converted to EventBridge `cron()` format. The key difference
20/// from standard crontab is that EventBridge requires exactly one of day-of-month or
21/// day-of-week to be `?` (meaning "no specific value"). This function handles that
22/// adjustment automatically:
23/// - If both are `*`, day-of-week becomes `?`
24/// - If day-of-month is `*` and day-of-week is set, day-of-month becomes `?`
25/// - If day-of-week is `*` and day-of-month is set, day-of-week becomes `?`
26///
27/// # Errors
28/// Returns `Err(String)` for invalid crontab expressions, including:
29/// - Wrong number of fields (must be 5 or 6)
30/// - Values out of range (e.g., minute > 59, hour > 23)
31/// - Invalid combinations (e.g., `L-W` in day-of-month)
32/// - Invalid dates (e.g., February 30)
33///
34/// # Examples
35/// ```
36/// use alien_core::crontab_to_eventbridge::crontab_to_eventbridge;
37///
38/// assert_eq!(crontab_to_eventbridge("0 12 * * *").unwrap(), "cron(0 12 * * ? *)");
39/// assert_eq!(crontab_to_eventbridge("*/5 * * * *").unwrap(), "rate(5 minutes)");
40/// assert_eq!(crontab_to_eventbridge("0 0 * * MON").unwrap(), "cron(0 0 ? * MON *)");
41/// ```
42pub fn crontab_to_eventbridge(crontab: &str) -> Result<String, String> {
43    let parts: Vec<&str> = crontab.trim().split_whitespace().collect();
44
45    if parts.len() < 5 || parts.len() > 6 {
46        return Err("Invalid crontab expression".to_string());
47    }
48
49    let minute = parts[0];
50    let hour = parts[1];
51    let day_of_month = parts[2];
52    let month = parts[3];
53    let day_of_week = parts[4];
54    let year = if parts.len() == 6 { parts[5] } else { "*" };
55
56    const MONTH_NAMES: &[&str] = &[
57        "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC",
58    ];
59    const DAY_NAMES: &[&str] = &["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
60
61    fn is_month_name(value: &str) -> bool {
62        MONTH_NAMES.contains(&value.to_uppercase().as_str())
63    }
64
65    fn is_day_name(value: &str) -> bool {
66        DAY_NAMES.contains(&value.to_uppercase().as_str())
67    }
68
69    /// Validates a simple numeric value within a given range or name list.
70    fn validate_simple_value(
71        value: &str,
72        field_name: &str,
73        min: i64,
74        max: i64,
75        name_list: &[&str],
76    ) -> Result<(), String> {
77        if name_list.contains(&value.to_uppercase().as_str())
78            || value == "L"
79            || value.ends_with('W')
80        {
81            return Ok(());
82        }
83
84        let num: i64 = value.parse().map_err(|_| {
85            format!(
86                "Invalid value {} in {} field of crontab expression",
87                value, field_name
88            )
89        })?;
90
91        if num < min || num > max {
92            return Err(format!(
93                "Invalid value {} in {} field of crontab expression",
94                value, field_name
95            ));
96        }
97
98        Ok(())
99    }
100
101    /// Validates the range of a given crontab field value.
102    fn validate_range(
103        value: &str,
104        field_name: &str,
105        min: i64,
106        max: i64,
107        name_list: &[&str],
108    ) -> Result<(), String> {
109        if matches!(value, "*" | "?" | "L") || value.ends_with('W') || value.contains('#') {
110            return Ok(());
111        }
112
113        let values: Vec<&str> = value.split(',').collect();
114        for val in values {
115            if val.contains('/') {
116                let slash_parts: Vec<&str> = val.splitn(2, '/').collect();
117                let range = slash_parts[0];
118                let step = slash_parts[1];
119                if !matches!(range, "*" | "?" | "L") && !range.ends_with('W') {
120                    if range.contains('-') {
121                        let dash_parts: Vec<&str> = range.splitn(2, '-').collect();
122                        validate_simple_value(dash_parts[0], field_name, min, max, name_list)?;
123                        validate_simple_value(dash_parts[1], field_name, min, max, name_list)?;
124                    } else {
125                        validate_simple_value(range, field_name, min, max, name_list)?;
126                    }
127                }
128                validate_simple_value(step, &format!("{} step", field_name), 1, max, &[])?;
129            } else if val.contains('-') {
130                let dash_parts: Vec<&str> = val.splitn(2, '-').collect();
131                validate_simple_value(dash_parts[0], field_name, min, max, name_list)?;
132                validate_simple_value(dash_parts[1], field_name, min, max, name_list)?;
133            } else {
134                validate_simple_value(val, field_name, min, max, name_list)?;
135            }
136        }
137
138        Ok(())
139    }
140
141    /// Validates the day of the month field for invalid combinations.
142    fn validate_day_of_month(value: &str) -> Result<(), String> {
143        if value.contains('L') && value.contains('W') {
144            return Err(format!(
145                "Invalid value {} in day of month field of crontab expression",
146                value
147            ));
148        }
149        Ok(())
150    }
151
152    /// Validates the day of the week field for `#` notation.
153    fn validate_day_of_week(value: &str) -> Result<(), String> {
154        let values: Vec<&str> = value.split(',').collect();
155        for val in values {
156            if val.contains('#') {
157                let hash_parts: Vec<&str> = val.splitn(2, '#').collect();
158                let day = hash_parts[0];
159                let nth = hash_parts[1];
160                validate_simple_value(day, "day of week", 0, 7, DAY_NAMES)?;
161                let nth_num: i64 = nth.parse().map_err(|_| {
162                    format!(
163                        "Invalid value {} in day of week field of crontab expression",
164                        val
165                    )
166                })?;
167                if nth_num < 1 || nth_num > 5 {
168                    return Err(format!(
169                        "Invalid value {} in day of week field of crontab expression",
170                        val
171                    ));
172                }
173            }
174        }
175        Ok(())
176    }
177
178    // Validate each crontab field
179    validate_range(minute, "minute", 0, 59, &[])?;
180    validate_range(hour, "hour", 0, 23, &[])?;
181    validate_range(day_of_month, "day of month", 1, 31, &[])?;
182    validate_range(month, "month", 1, 12, MONTH_NAMES)?;
183    validate_range(day_of_week, "day of week", 0, 7, DAY_NAMES)?;
184    if year != "*" {
185        validate_range(year, "year", 1970, 2099, &[])?;
186    }
187
188    validate_day_of_month(day_of_month)?;
189    validate_day_of_week(day_of_week)?;
190
191    // Additional validation for February dates
192    if let Ok(month_num) = month.parse::<i64>() {
193        if month_num == 2 && (day_of_month == "30" || day_of_month == "31") {
194            return Err("Invalid date: February does not have 30 or 31 days".to_string());
195        }
196    }
197
198    /// Maps a crontab value to an EventBridge-friendly format.
199    fn map_crontab_to_eventbridge(value: &str, field_name: &str) -> Result<String, String> {
200        if matches!(value, "*" | "?" | "L")
201            || value.ends_with('W')
202            || value.contains('#')
203            || value.chars().all(|c| c.is_ascii_digit())
204            || value.contains('/')
205            || value.contains('-')
206            || value.contains(',')
207            || (field_name == "month" && is_month_name(value))
208            || (field_name == "day of week" && is_day_name(value))
209        {
210            return Ok(value.to_string());
211        }
212        Err(format!(
213            "Invalid value {} in {} field of crontab expression",
214            value, field_name
215        ))
216    }
217
218    // Check for rate expressions
219    if minute == "*" && hour == "*" && day_of_month == "*" && month == "*" && day_of_week == "*" {
220        return Ok("rate(1 minute)".to_string());
221    }
222    if minute == "0" && hour == "*" && day_of_month == "*" && month == "*" && day_of_week == "*" {
223        return Ok("rate(1 hour)".to_string());
224    }
225    if minute == "0" && hour == "0" && day_of_month == "*" && month == "*" && day_of_week == "*" {
226        return Ok("rate(1 day)".to_string());
227    }
228    if minute.starts_with("*/")
229        && hour == "*"
230        && day_of_month == "*"
231        && month == "*"
232        && day_of_week == "*"
233    {
234        let minute_rate = &minute[2..];
235        return Ok(format!("rate({} minutes)", minute_rate));
236    }
237    if minute == "0"
238        && hour.starts_with("*/")
239        && day_of_month == "*"
240        && month == "*"
241        && day_of_week == "*"
242    {
243        let hour_rate = &hour[2..];
244        return Ok(format!("rate({} hours)", hour_rate));
245    }
246    if minute == "0"
247        && hour == "0"
248        && day_of_month.starts_with("*/")
249        && month == "*"
250        && day_of_week == "*"
251    {
252        let day_rate = &day_of_month[2..];
253        return Ok(format!("rate({} days)", day_rate));
254    }
255
256    // Adjust day_of_month and day_of_week for EventBridge cron
257    let mut eb_day_of_month = day_of_month.to_string();
258    let mut eb_day_of_week = day_of_week.to_string();
259
260    if day_of_month == "*" && day_of_week == "*" {
261        eb_day_of_week = "?".to_string();
262    } else if day_of_month == "*" {
263        eb_day_of_month = "?".to_string();
264    } else if day_of_week == "*" {
265        eb_day_of_week = "?".to_string();
266    }
267
268    // Ensure only one of day_of_month or day_of_week is set to '?'
269    if eb_day_of_month == "?" && eb_day_of_week == "?" {
270        eb_day_of_week = "*".to_string();
271    }
272
273    // Construct EventBridge cron expression parts
274    let eb_minute = map_crontab_to_eventbridge(minute, "minute")?;
275    let eb_hour = map_crontab_to_eventbridge(hour, "hour")?;
276    let eb_month = map_crontab_to_eventbridge(month, "month")?;
277    let eb_day_of_month_mapped = map_crontab_to_eventbridge(&eb_day_of_month, "day of month")?;
278    let eb_day_of_week_mapped = map_crontab_to_eventbridge(&eb_day_of_week, "day of week")?;
279    let eb_year = map_crontab_to_eventbridge(year, "year")?;
280
281    Ok(format!(
282        "cron({} {} {} {} {} {})",
283        eb_minute, eb_hour, eb_day_of_month_mapped, eb_month, eb_day_of_week_mapped, eb_year
284    ))
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn simple_crontab_to_eventbridge_cron() {
293        assert_eq!(
294            crontab_to_eventbridge("0 12 * * *").unwrap(),
295            "cron(0 12 * * ? *)"
296        );
297    }
298
299    #[test]
300    fn specific_day_of_week() {
301        assert_eq!(
302            crontab_to_eventbridge("0 18 * * 5").unwrap(),
303            "cron(0 18 ? * 5 *)"
304        );
305    }
306
307    #[test]
308    fn specific_day_of_month() {
309        assert_eq!(
310            crontab_to_eventbridge("0 6 15 * *").unwrap(),
311            "cron(0 6 15 * ? *)"
312        );
313    }
314
315    #[test]
316    fn specific_month() {
317        assert_eq!(
318            crontab_to_eventbridge("0 9 * 7 *").unwrap(),
319            "cron(0 9 * 7 ? *)"
320        );
321    }
322
323    #[test]
324    fn specific_minute() {
325        assert_eq!(
326            crontab_to_eventbridge("30 * * * *").unwrap(),
327            "cron(30 * * * ? *)"
328        );
329    }
330
331    #[test]
332    fn rate_every_minute() {
333        assert_eq!(
334            crontab_to_eventbridge("* * * * *").unwrap(),
335            "rate(1 minute)"
336        );
337    }
338
339    #[test]
340    fn rate_every_hour() {
341        assert_eq!(crontab_to_eventbridge("0 * * * *").unwrap(), "rate(1 hour)");
342    }
343
344    #[test]
345    fn rate_every_day() {
346        assert_eq!(crontab_to_eventbridge("0 0 * * *").unwrap(), "rate(1 day)");
347    }
348
349    #[test]
350    fn complex_multiple_fields() {
351        assert_eq!(
352            crontab_to_eventbridge("15 10 15 6 *").unwrap(),
353            "cron(15 10 15 6 ? *)"
354        );
355    }
356
357    #[test]
358    fn complex_step_values() {
359        assert_eq!(
360            crontab_to_eventbridge("0/15 14 1,15 * 1-5").unwrap(),
361            "cron(0/15 14 1,15 * 1-5 *)"
362        );
363    }
364
365    #[test]
366    fn complex_ranges() {
367        assert_eq!(
368            crontab_to_eventbridge("0 0 1-10 * 2-6").unwrap(),
369            "cron(0 0 1-10 * 2-6 *)"
370        );
371    }
372
373    #[test]
374    fn complex_multiple_ranges_and_steps() {
375        assert_eq!(
376            crontab_to_eventbridge("5-10/2 0-12/3 1-15/5 * 1-5").unwrap(),
377            "cron(5-10/2 0-12/3 1-15/5 * 1-5 *)"
378        );
379    }
380
381    #[test]
382    fn specific_year() {
383        assert_eq!(
384            crontab_to_eventbridge("0 0 1 1 * 2025").unwrap(),
385            "cron(0 0 1 1 ? 2025)"
386        );
387    }
388
389    #[test]
390    fn multiple_specific_fields_with_year() {
391        assert_eq!(
392            crontab_to_eventbridge("0 0 1 1 1 2025").unwrap(),
393            "cron(0 0 1 1 1 2025)"
394        );
395    }
396
397    #[test]
398    fn steps_in_all_fields_with_year() {
399        assert_eq!(
400            crontab_to_eventbridge("0/5 0/2 1/3 1/4 1/5 2025/2").unwrap(),
401            "cron(0/5 0/2 1/3 1/4 1/5 2025/2)"
402        );
403    }
404
405    #[test]
406    fn rate_every_5_minutes() {
407        assert_eq!(
408            crontab_to_eventbridge("*/5 * * * *").unwrap(),
409            "rate(5 minutes)"
410        );
411    }
412
413    #[test]
414    fn rate_every_2_hours() {
415        assert_eq!(
416            crontab_to_eventbridge("0 */2 * * *").unwrap(),
417            "rate(2 hours)"
418        );
419    }
420
421    #[test]
422    fn rate_every_3_days() {
423        assert_eq!(
424            crontab_to_eventbridge("0 0 */3 * *").unwrap(),
425            "rate(3 days)"
426        );
427    }
428
429    #[test]
430    fn invalid_crontab_expression() {
431        let err = crontab_to_eventbridge("invalid expression").unwrap_err();
432        assert_eq!(err, "Invalid crontab expression");
433    }
434
435    #[test]
436    fn empty_crontab_expression() {
437        let err = crontab_to_eventbridge("").unwrap_err();
438        assert_eq!(err, "Invalid crontab expression");
439    }
440
441    #[test]
442    fn combined_hour_and_minute_ranges() {
443        assert_eq!(
444            crontab_to_eventbridge("10-20/5 8-16/2 * * *").unwrap(),
445            "cron(10-20/5 8-16/2 * * ? *)"
446        );
447    }
448
449    #[test]
450    fn mixed_lists_and_ranges_in_day_of_week() {
451        assert_eq!(
452            crontab_to_eventbridge("0 12 * * 1-5,7").unwrap(),
453            "cron(0 12 ? * 1-5,7 *)"
454        );
455    }
456
457    #[test]
458    fn day_of_month_list_and_interval() {
459        assert_eq!(
460            crontab_to_eventbridge("0 0 1,15,20-25/5 * *").unwrap(),
461            "cron(0 0 1,15,20-25/5 * ? *)"
462        );
463    }
464
465    #[test]
466    fn complex_minute_and_hour_intervals() {
467        assert_eq!(
468            crontab_to_eventbridge("1-59/15 0-23/3 * * *").unwrap(),
469            "cron(1-59/15 0-23/3 * * ? *)"
470        );
471    }
472
473    #[test]
474    fn overlapping_minute_intervals() {
475        assert_eq!(
476            crontab_to_eventbridge("0-30/10,15-45/15 * * * *").unwrap(),
477            "cron(0-30/10,15-45/15 * * * ? *)"
478        );
479    }
480
481    #[test]
482    fn multiple_intervals_and_specific_hours() {
483        assert_eq!(
484            crontab_to_eventbridge("0/5 8-17/1 * * *").unwrap(),
485            "cron(0/5 8-17/1 * * ? *)"
486        );
487    }
488
489    #[test]
490    fn multiple_day_of_week_intervals() {
491        assert_eq!(
492            crontab_to_eventbridge("0 0 * * 1-3,5-7").unwrap(),
493            "cron(0 0 ? * 1-3,5-7 *)"
494        );
495    }
496
497    #[test]
498    fn complex_intervals_and_steps() {
499        assert_eq!(
500            crontab_to_eventbridge("0 0/2 1-31/5 * 1-6/2").unwrap(),
501            "cron(0 0/2 1-31/5 * 1-6/2 *)"
502        );
503    }
504
505    #[test]
506    fn invalid_minute_value() {
507        let err = crontab_to_eventbridge("60 * * * *").unwrap_err();
508        assert_eq!(
509            err,
510            "Invalid value 60 in minute field of crontab expression"
511        );
512    }
513
514    #[test]
515    fn invalid_hour_value() {
516        let err = crontab_to_eventbridge("0 24 * * *").unwrap_err();
517        assert_eq!(err, "Invalid value 24 in hour field of crontab expression");
518    }
519
520    #[test]
521    fn invalid_day_of_month_value() {
522        let err = crontab_to_eventbridge("0 0 32 * *").unwrap_err();
523        assert_eq!(
524            err,
525            "Invalid value 32 in day of month field of crontab expression"
526        );
527    }
528
529    #[test]
530    fn invalid_month_value() {
531        let err = crontab_to_eventbridge("0 0 * 13 *").unwrap_err();
532        assert_eq!(err, "Invalid value 13 in month field of crontab expression");
533    }
534
535    #[test]
536    fn invalid_day_of_week_value() {
537        let err = crontab_to_eventbridge("0 0 * * 8").unwrap_err();
538        assert_eq!(
539            err,
540            "Invalid value 8 in day of week field of crontab expression"
541        );
542    }
543
544    #[test]
545    fn more_than_6_fields() {
546        let err = crontab_to_eventbridge("0 0 * * * * extra").unwrap_err();
547        assert_eq!(err, "Invalid crontab expression");
548    }
549
550    #[test]
551    fn less_than_5_fields() {
552        let err = crontab_to_eventbridge("0 0 * *").unwrap_err();
553        assert_eq!(err, "Invalid crontab expression");
554    }
555
556    #[test]
557    fn named_months() {
558        assert_eq!(
559            crontab_to_eventbridge("0 0 * JAN *").unwrap(),
560            "cron(0 0 * JAN ? *)"
561        );
562    }
563
564    #[test]
565    fn named_days_of_week() {
566        assert_eq!(
567            crontab_to_eventbridge("0 0 * * MON").unwrap(),
568            "cron(0 0 ? * MON *)"
569        );
570    }
571
572    #[test]
573    fn mixed_ranges_steps_and_named_days() {
574        assert_eq!(
575            crontab_to_eventbridge("*/15 1-5/2 1,15 * MON-FRI").unwrap(),
576            "cron(*/15 1-5/2 1,15 * MON-FRI *)"
577        );
578    }
579
580    #[test]
581    fn l_wildcard_in_day_of_month() {
582        assert_eq!(
583            crontab_to_eventbridge("0 0 L * *").unwrap(),
584            "cron(0 0 L * ? *)"
585        );
586    }
587
588    #[test]
589    fn invalid_l_w_combination() {
590        let err = crontab_to_eventbridge("0 0 L-W * *").unwrap_err();
591        assert_eq!(
592            err,
593            "Invalid value L-W in day of month field of crontab expression"
594        );
595    }
596
597    #[test]
598    fn step_values_in_day_of_month() {
599        assert_eq!(
600            crontab_to_eventbridge("0 0 1-31/7 * *").unwrap(),
601            "cron(0 0 1-31/7 * ? *)"
602        );
603    }
604
605    #[test]
606    fn hash_wildcard_in_day_of_week() {
607        assert_eq!(
608            crontab_to_eventbridge("0 0 * * 2#1").unwrap(),
609            "cron(0 0 ? * 2#1 *)"
610        );
611    }
612
613    #[test]
614    fn invalid_hash_wildcard() {
615        let err = crontab_to_eventbridge("0 0 * * 2#8").unwrap_err();
616        assert_eq!(
617            err,
618            "Invalid value 2#8 in day of week field of crontab expression"
619        );
620    }
621
622    #[test]
623    fn named_month_and_specific_day_of_month() {
624        assert_eq!(
625            crontab_to_eventbridge("0 0 1 JAN *").unwrap(),
626            "cron(0 0 1 JAN ? *)"
627        );
628    }
629
630    #[test]
631    fn named_day_of_week_and_specific_month() {
632        assert_eq!(
633            crontab_to_eventbridge("0 0 * FEB MON").unwrap(),
634            "cron(0 0 ? FEB MON *)"
635        );
636    }
637
638    #[test]
639    fn leap_year_date() {
640        assert_eq!(
641            crontab_to_eventbridge("0 0 29 2 *").unwrap(),
642            "cron(0 0 29 2 ? *)"
643        );
644    }
645
646    #[test]
647    fn invalid_leap_year_date() {
648        assert!(crontab_to_eventbridge("0 0 30 2 *").is_err());
649    }
650
651    #[test]
652    fn mixed_l_w_hash_wildcards_error() {
653        assert!(crontab_to_eventbridge("0 0 L-W * 2#1").is_err());
654    }
655
656    #[test]
657    fn range_not_starting_from_zero() {
658        assert_eq!(
659            crontab_to_eventbridge("10-50/10 10-22/2 * * *").unwrap(),
660            "cron(10-50/10 10-22/2 * * ? *)"
661        );
662    }
663
664    #[test]
665    fn empty_fields_too_few() {
666        let err = crontab_to_eventbridge("0 0 * *").unwrap_err();
667        assert_eq!(err, "Invalid crontab expression");
668    }
669
670    #[test]
671    fn boundary_minute_59() {
672        assert_eq!(
673            crontab_to_eventbridge("59 0 * * *").unwrap(),
674            "cron(59 0 * * ? *)"
675        );
676    }
677
678    #[test]
679    fn boundary_hour_23() {
680        assert_eq!(
681            crontab_to_eventbridge("0 23 * * *").unwrap(),
682            "cron(0 23 * * ? *)"
683        );
684    }
685
686    #[test]
687    fn boundary_day_of_month_31() {
688        assert_eq!(
689            crontab_to_eventbridge("0 0 31 * *").unwrap(),
690            "cron(0 0 31 * ? *)"
691        );
692    }
693
694    #[test]
695    fn boundary_month_12() {
696        assert_eq!(
697            crontab_to_eventbridge("0 0 * 12 *").unwrap(),
698            "cron(0 0 * 12 ? *)"
699        );
700    }
701
702    #[test]
703    fn boundary_day_of_week_7() {
704        assert_eq!(
705            crontab_to_eventbridge("0 0 * * 7").unwrap(),
706            "cron(0 0 ? * 7 *)"
707        );
708    }
709
710    #[test]
711    fn step_values_in_all_fields() {
712        assert_eq!(
713            crontab_to_eventbridge("0/15 0/2 1-30/3 1-12/2 0-7/2").unwrap(),
714            "cron(0/15 0/2 1-30/3 1-12/2 0-7/2 *)"
715        );
716    }
717
718    #[test]
719    fn mixed_step_values_and_lists() {
720        assert_eq!(
721            crontab_to_eventbridge("0/10,20,30 1,2,3-5/2 1,15,30 * 1-5").unwrap(),
722            "cron(0/10,20,30 1,2,3-5/2 1,15,30 * 1-5 *)"
723        );
724    }
725
726    #[test]
727    fn complex_wildcard_combinations_error() {
728        assert!(crontab_to_eventbridge("0-5,10-15/2,20/4 * L-W * 1-5").is_err());
729    }
730
731    #[test]
732    fn boundary_year_1970() {
733        assert_eq!(
734            crontab_to_eventbridge("0 0 1 1 ? 1970").unwrap(),
735            "cron(0 0 1 1 ? 1970)"
736        );
737    }
738
739    #[test]
740    fn invalid_year_value() {
741        let err = crontab_to_eventbridge("0 0 1 1 ? 2200").unwrap_err();
742        assert_eq!(
743            err,
744            "Invalid value 2200 in year field of crontab expression"
745        );
746    }
747
748    #[test]
749    fn non_numeric_characters_in_fields() {
750        let err = crontab_to_eventbridge("0 0 a * *").unwrap_err();
751        assert_eq!(
752            err,
753            "Invalid value a in day of month field of crontab expression"
754        );
755    }
756
757    #[test]
758    fn overflow_minute_60() {
759        let err = crontab_to_eventbridge("60 0 * * *").unwrap_err();
760        assert_eq!(
761            err,
762            "Invalid value 60 in minute field of crontab expression"
763        );
764    }
765
766    #[test]
767    fn overflow_hour_24() {
768        let err = crontab_to_eventbridge("0 24 * * *").unwrap_err();
769        assert_eq!(err, "Invalid value 24 in hour field of crontab expression");
770    }
771
772    #[test]
773    fn overflow_day_of_month_32() {
774        let err = crontab_to_eventbridge("0 0 32 * *").unwrap_err();
775        assert_eq!(
776            err,
777            "Invalid value 32 in day of month field of crontab expression"
778        );
779    }
780
781    #[test]
782    fn overflow_month_13() {
783        let err = crontab_to_eventbridge("0 0 * 13 *").unwrap_err();
784        assert_eq!(err, "Invalid value 13 in month field of crontab expression");
785    }
786
787    #[test]
788    fn overflow_day_of_week_8() {
789        let err = crontab_to_eventbridge("0 0 * * 8").unwrap_err();
790        assert_eq!(
791            err,
792            "Invalid value 8 in day of week field of crontab expression"
793        );
794    }
795
796    #[test]
797    fn invalid_special_characters_in_year() {
798        let err = crontab_to_eventbridge("0 0 * * * !").unwrap_err();
799        assert_eq!(err, "Invalid value ! in year field of crontab expression");
800    }
801}