1pub 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 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 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 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 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_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 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 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 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 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 if eb_day_of_month == "?" && eb_day_of_week == "?" {
270 eb_day_of_week = "*".to_string();
271 }
272
273 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}