1#[macro_use]
2extern crate rust_i18n;
3extern crate strfmt;
4
5use string_builder::Builder;
6
7mod description_builder;
8
9rust_i18n::i18n!("locales");
10
11mod string_utils {
12 pub fn not_contains_any(str: &String, chars: &[char]) -> bool {
13 str.chars().all(|c| !chars.contains(&c))
14 }
15
16 pub fn is_numeric(s: &str) -> bool {
17 for c in s.chars() {
18 if !c.is_numeric() {
19 return false;
20 }
21 }
22 return true;
23 }
24}
25
26mod date_time_utils {
27 pub static DAYS_OF_WEEK_ARR: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
28 pub static MONTHS_ARR: [&str; 12] = [
29 "january",
30 "february",
31 "march",
32 "april",
33 "may",
34 "june",
35 "july",
36 "august",
37 "september",
38 "october",
39 "november",
40 "december",
41 ];
42
43 use crate::cronparser::Options;
44
45 pub fn format_time(
46 hours_expression: &String,
47 minutes_expression: &String,
48 opts: &Options,
49 ) -> String {
50 format_time_secs(
51 &hours_expression,
52 &minutes_expression,
53 &"".to_string(),
54 opts,
55 )
56 }
57
58 pub fn format_time_secs(
59 hours_expression: &String,
60 minutes_expression: &String,
61 seconds_expression: &String,
62 opts: &Options,
63 ) -> String {
64 let mut hour: i8 = hours_expression.parse().unwrap();
65 let mut period: String = "".to_string();
66
67 if !opts.twenty_four_hour_time {
68 period = if hour >= 12 {
69 t!("time_pm")
70 } else {
71 t!("time_am")
72 };
73 if !period.len() > 0 {
74 period = " ".to_string() + .
75 }
76 if hour > 12 {
77 hour -= 12;
78 }
79 if hour == 0 {
80 hour = 12;
81 }
82 }
83
84 let minutes = minutes_expression.parse::<i8>().unwrap().to_string();
85 let mut seconds: String = "".to_string();
86
87 if !seconds_expression.is_empty() {
88 seconds = ":".to_string() + &seconds_expression.parse::<i8>().unwrap().to_string();
89 seconds = format!("{:0>2}", seconds);
90 }
91 let formatted_hours = if opts.twenty_four_hour_time {
92 format!("{:0>2}", hour)
93 } else {
94 format!("{}", hour)
95 };
96 format!(
97 "{0}:{1}{2}{3}",
98 formatted_hours,
99 format!("{:0>2}", minutes),
100 seconds,
101 period
102 )
103 }
104
105 pub fn get_day_of_week_name(day_of_week: usize) -> String {
106 let day_str = DAYS_OF_WEEK_ARR[day_of_week % 7];
107 t!(day_str)
108 }
109}
110
111pub fn format_minutes(minutes_expression: &str) -> String {
112 if minutes_expression.contains(",") {
113 let mparts = minutes_expression.split(",");
114 let mut formatted_expression = Builder::default();
115 for mpt in mparts {
116 formatted_expression.append(format!("{:02}", mpt.parse::<i8>().unwrap()));
117 formatted_expression.append(",");
118 }
119 formatted_expression.string().unwrap()
120 } else {
121 format!("{:02}", minutes_expression.parse::<i8>().unwrap())
122 }
123}
124
125pub mod cronparser {
126 pub enum CasingTypeEnum {
127 Title,
128 Sentence,
129 LowerCase,
130 }
131
132 pub enum DescriptionTypeEnum {
133 FULL,
134 TIMEOFDAY,
135 SECONDS,
136 MINUTES,
137 HOURS,
138 DAYOFWEEK,
139 MONTH,
140 DAYOFMONTH,
141 YEAR,
142 }
143
144 pub struct Options {
145 pub throw_exception_on_parse_error: bool,
146 pub casing_type: CasingTypeEnum,
147 pub verbose: bool,
148 pub zero_based_day_of_week: bool,
149 pub twenty_four_hour_time: bool,
150 pub need_space_between_words: bool,
151 }
152
153 impl Options {
154 pub fn options() -> Options {
155 return Options {
156 throw_exception_on_parse_error: true,
157 casing_type: CasingTypeEnum::Sentence,
158 verbose: false,
159 zero_based_day_of_week: true,
160 twenty_four_hour_time: false,
161 need_space_between_words: true,
162 };
163 }
164
165 pub fn twenty_four_hour() -> Options {
166 let opts = Options::options();
167 let opts2 = Options {
168 twenty_four_hour_time: true,
169 ..opts
170 };
171 return opts2;
172 }
173 }
174
175 pub mod cron_expression_descriptor {
176 use lazy_static::lazy_static;
177 use std::collections::HashMap;
178 use string_builder::Builder;
179
180 use crate::cronparser::{CasingTypeEnum, DescriptionTypeEnum, Options};
181 use crate::date_time_utils::{format_time, format_time_secs};
182 use crate::description_builder::DescriptionBuilder;
183 use crate::description_builder::{
184 DayOfMonthDescriptionBuilder, DayOfWeekDescriptionBuilder, HoursDescriptionBuilder,
185 MinutesDescriptionBuilder, MonthDescriptionBuilder, SecondsDescriptionBuilder,
186 YearDescriptionBuilder,
187 };
188 use crate::{cronparser, string_utils};
189
190 const SPECIAL_CHARACTERS: [char; 4] = ['/', '-', ',', '*'];
191
192 #[derive(Debug, PartialEq)]
193 pub struct ParseException {
194 pub s: String,
195 pub error_offset: u8,
196 }
197
198 mod expression_parser {
199 use lazy_static::lazy_static;
212
213 use crate::cronparser::cron_expression_descriptor::ParseException;
214 use crate::cronparser::Options;
215 use crate::string_utils;
216 use regex::Regex;
217
218 pub fn parse(
219 expression: &str,
220 options: &Options,
221 ) -> Result<Vec<String>, ParseException> {
222 let mut parsed: Vec<&str> = vec![""; 7];
223 if expression.trim().is_empty() {
224 lazy_static! {
225 static ref ERR_STR: String = t!("expression_empty_exception");
226 }
227 Err(ParseException {
228 s: expression.to_string(),
229 error_offset: 0,
230 })
231 } else {
232 let expression_parts: Vec<&str> =
233 expression.trim().split_whitespace().collect();
234 if expression_parts.len() < 5 {
235 return Err(ParseException {
236 s: expression.to_string(),
237 error_offset: 0,
238 });
239 } else if expression_parts.len() == 5 {
240 parsed[0] = "";
241 (1..=5).for_each(|i| parsed[i] = expression_parts[i - 1]);
242 } else if expression_parts.len() == 6 {
244 lazy_static! {
245 static ref YEAR_RE: Regex = Regex::new(r"\d{4}$").unwrap();
246 }
247 if YEAR_RE.is_match(expression_parts[5]) {
248 (1..=6).for_each(|i| parsed[i] = expression_parts[i - 1]);
249 } else {
250 (0..6).for_each(|i| parsed[i] = expression_parts[i]);
251 }
252 } else if expression_parts.len() == 7 {
253 (0..=6).for_each(|i| parsed[i] = expression_parts[i]);
254 } else {
255 let result2 = Err(ParseException {
256 s: expression.to_string(),
257 error_offset: 7,
258 });
259 return result2;
260 }
261
262 let normalized_expr = normalise_expression(parsed, options);
263 Ok(normalized_expr)
264 }
265 }
266
267 fn normalise_expression(expression_parts: Vec<&str>, options: &Options) -> Vec<String> {
268 static DAYS_OF_WEEK_ARR: [&str; 7] =
269 ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
270 static MONTHS_ARR: [&str; 12] = [
271 "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV",
272 "DEC",
273 ];
274 let mut normalised: Vec<String> = vec!["".to_string(); 7];
275
276 (0..expression_parts.len()).for_each(|i| {
277 normalised[i] = expression_parts[i].to_string();
278 });
279
280 normalised[3] = normalised[3].replace("?", "*");
281 normalised[5] = normalised[5].replace("?", "*");
282
283 (0..=2).for_each(|i| {
284 normalised[i] = if normalised[i].starts_with("0/") {
285 normalised[i].replace("0/", "*/")
286 } else {
287 normalised[i].to_string()
288 }
289 });
290
291 (3..=5).for_each(|i| {
292 normalised[i] = if normalised[i].starts_with("1/") {
293 normalised[i].replace("1/", "*/")
294 } else {
295 normalised[i].to_string()
296 }
297 });
298
299 for i in 0..normalised.len() {
300 if normalised[i] == "*/1" {
301 normalised[i] = "*".to_string();
302 }
303 }
304 if !string_utils::is_numeric(&normalised[5]) {
307 for i in 0..=6 {
308 normalised[5] =
309 normalised[5].replace(DAYS_OF_WEEK_ARR[i], i.to_string().as_str());
310 }
311 }
312
313 if !string_utils::is_numeric(&normalised[4]) {
315 for i in 1..12 {
316 normalised[4] =
317 normalised[4].replace(MONTHS_ARR[i - 1], i.to_string().as_str());
318 }
319 }
320
321 if "0" == normalised[0] {
323 normalised[0] = "".to_string();
324 }
325
326 if options.zero_based_day_of_week && "0" == normalised[5] {
330 normalised[5] = "7".to_string();
331 }
332
333 normalised
337 }
338 }
339
340 pub fn get_description(
341 description_type: DescriptionTypeEnum,
342 expression: &str,
343 options: &Options,
344 locale: &str,
345 ) -> Result<String, ParseException> {
346 rust_i18n::set_locale(&locale);
347 let expression_parsed = expression_parser::parse(expression, options);
348 match expression_parsed {
349 Ok(expression_parts) => {
350 let description_res = match description_type {
351 DescriptionTypeEnum::FULL => {
352 get_full_description(&expression_parts, options)
353 }
354 DescriptionTypeEnum::TIMEOFDAY => {
355 get_time_of_day_description(&expression_parts, options)
356 }
357 DescriptionTypeEnum::SECONDS => {
358 get_seconds_description(&expression_parts, options)
359 }
360 DescriptionTypeEnum::MINUTES => {
361 get_minutes_description(&expression_parts, options)
362 }
363 DescriptionTypeEnum::HOURS => {
364 get_hours_description(&expression_parts, options)
365 }
366 DescriptionTypeEnum::DAYOFWEEK => {
367 get_day_of_week_description(&expression_parts, options)
368 }
369 DescriptionTypeEnum::MONTH => {
370 get_month_description(&expression_parts, options)
371 }
372 DescriptionTypeEnum::DAYOFMONTH => {
373 get_day_of_month_description(&expression_parts, options)
374 }
375 DescriptionTypeEnum::YEAR => {
376 get_year_description(&expression_parts, options)
377 }
378 };
379 Ok(description_res)
380 }
381 Err(pe) => Err(pe),
382 }
383 }
384
385 fn get_full_description(expression_parts: &Vec<String>, options: &Options) -> String {
387 let time_segment = get_time_of_day_description(&expression_parts, options);
388 let day_of_month_desc = get_day_of_month_description(&expression_parts, options);
389 let month_desc = get_month_description(&expression_parts, options);
390 let day_of_week_desc = get_day_of_week_description(&expression_parts, options);
391 let year_desc = get_year_description(&expression_parts, options);
392 let week_or_month_desc = if "*" == &expression_parts[3] {
393 day_of_week_desc
394 } else {
395 day_of_month_desc
396 };
397 let desc1 = format!(
398 "{0}{1}{2}{3}",
399 time_segment, week_or_month_desc, month_desc, year_desc
400 );
401 let desc2 = transform_verbosity(desc1, options);
405 transform_case(&desc2, options)
406 }
407
408 fn transform_verbosity(description: String, options: &Options) -> String {
409 let mut desc_temp = description.clone();
410 if !options.verbose {
411 desc_temp =
412 desc_temp.replace(&t!("messages.every_1_minute"), &t!("messages.every_minute"));
413 desc_temp =
414 desc_temp.replace(&t!("messages.every_1_hour"), &t!("messages.every_hour"));
415 desc_temp =
416 desc_temp.replace(&t!("messages.every_1_day"), &t!("messages.every_day"));
417 desc_temp = desc_temp.replace(&format!(", {}", &t!("messages.every_minute")), "");
418 desc_temp = desc_temp.replace(&format!(", {}", &t!("messages.every_hour")), "");
419 desc_temp = desc_temp.replace(&format!(", {}", &t!("messages.every_day")), "");
420 desc_temp = desc_temp.replace(&format!(", {}", &t!("messages.every_year")), "");
421 }
422 desc_temp
423 }
424
425 fn transform_case(description: &str, options: &Options) -> String {
426 match &options.casing_type {
427 CasingTypeEnum::Sentence => description[0..1].to_uppercase() + &description[1..],
428 CasingTypeEnum::Title => description[0..1].to_uppercase() + &description[1..],
429 CasingTypeEnum::LowerCase => description.to_lowercase(),
430 }
431 }
432
433 fn get_year_description(expression_parts: &Vec<String>, options: &Options) -> String {
434 let builder = YearDescriptionBuilder { options };
435 builder.get_segment_description(
436 &expression_parts[6],
437 format!(", {}", t!("messages.every_year")),
438 )
439 }
440
441 fn get_day_of_week_description(
442 expression_parts: &Vec<String>,
443 options: &Options,
444 ) -> String {
445 let builder = DayOfWeekDescriptionBuilder { options };
446 builder.get_segment_description(
448 &expression_parts[5],
449 format!(", {}", t!("messages.every_day")),
450 )
451 }
452
453 fn get_minutes_description(expression_parts: &Vec<String>, options: &Options) -> String {
454 let builder = MinutesDescriptionBuilder { options };
455 builder.get_segment_description(&expression_parts[1], t!("messages.every_minute"))
456 }
457
458 fn get_seconds_description(expression_parts: &Vec<String>, options: &Options) -> String {
459 let builder = SecondsDescriptionBuilder { options };
460 builder.get_segment_description(&expression_parts[0], t!("messages.every_second"))
461 }
462
463 fn get_hours_description(expression_parts: &Vec<String>, options: &Options) -> String {
464 let builder = HoursDescriptionBuilder { options };
465 builder.get_segment_description(&expression_parts[2], t!("messages.every_hour"))
466 }
467
468 fn get_month_description(expression_parts: &Vec<String>, options: &Options) -> String {
469 let builder = MonthDescriptionBuilder { options };
470 builder.get_segment_description(&expression_parts[4], "".to_string())
471 }
472
473 fn get_day_of_month_description(
474 expression_parts: &Vec<String>,
475 options: &Options,
476 ) -> String {
477 use regex::Regex;
478 use strfmt::strfmt;
479 let exp = expression_parts[3].replace("?", "*");
480 let description = if "L" == exp {
481 format!(", {}", t!("messages.on_the_last_day_of_the_month"))
482 } else if "WL" == exp || "LW" == exp {
483 format!(", {}", t!("messages.on_the_last_weekday_of_the_month"))
484 } else {
485 lazy_static! {
486 static ref DOM_RE: Regex = Regex::new(r"(\dW)|(W\d)").unwrap();
487 }
488 if DOM_RE.is_match(&exp) {
489 let capt = DOM_RE.captures_iter(&exp).next().unwrap();
490 let no_w = capt[0].replace("W", "");
491 let day_number = no_w.parse::<u8>().unwrap();
492 let day_string = if day_number == 1 {
493 t!("messages.first_weekday")
494 } else {
495 t!("messages.weekday_nearest_day", 0 = &no_w)
496 };
497 let fmt_str = format!(", {}", t!("messages.on_the_of_the_month"));
498 let mut vars = HashMap::new();
499 vars.insert("0".to_string(), day_string);
500 strfmt(&fmt_str, &vars).unwrap()
501 } else {
502 let builder = DayOfMonthDescriptionBuilder { options };
503 builder.get_segment_description(&exp, format!(", {}", t!("messages.every_day")))
505 }
506 };
507 description
508 }
509
510 fn get_time_of_day_description(
511 expression_parts: &Vec<String>,
512 options: &Options,
513 ) -> String {
514 let seconds_expression = &expression_parts[0];
515 let minutes_expression = &expression_parts[1];
516 let hours_expression = &expression_parts[2];
517
518 let mut description = Builder::default();
519
520 if minutes_expression
521 .chars()
522 .all(|c| !SPECIAL_CHARACTERS.contains(&c))
523 && hours_expression
524 .chars()
525 .all(|c| !SPECIAL_CHARACTERS.contains(&c))
526 && seconds_expression
527 .chars()
528 .all(|c| !SPECIAL_CHARACTERS.contains(&c))
529 {
530 description.append(t!("at"));
531 if options.need_space_between_words {
532 description.append(" ");
533 }
534 description.append(format_time_secs(
535 hours_expression,
536 minutes_expression,
537 seconds_expression,
538 options,
539 ));
540 } else if minutes_expression.contains("-")
541 && !minutes_expression.contains("/")
542 && string_utils::not_contains_any(hours_expression, &SPECIAL_CHARACTERS)
543 {
544 let mut minute_parts = minutes_expression.split("-");
545 let msg0 = format_time(
546 hours_expression,
547 &minute_parts.next().unwrap().to_string(),
548 options,
549 );
550 let msg1 = format_time(
551 hours_expression,
552 &minute_parts.next().unwrap().to_string(),
553 options,
554 );
555 description.append(t!("messages.every_minute_between", 0 = &msg0, 1 = &msg1));
556 } else if hours_expression.contains(",")
557 && string_utils::not_contains_any(minutes_expression, &SPECIAL_CHARACTERS)
558 {
559 let hour_parts: Vec<_> = hours_expression.split(",").collect();
560 let hpsz = hour_parts.len();
561 description.append(t!("at"));
562
563 for (i, hp) in hour_parts.iter().enumerate() {
564 description.append(" ");
565 description.append(format_time(&hp.to_string(), minutes_expression, options));
566 if i < hpsz - 2 {
567 description.append(",");
568 }
569 if i == hpsz - 2 {
570 description.append(" ");
571 description.append(t!("and"));
572 }
573 }
574 } else {
575 let seconds_description = get_seconds_description(expression_parts, options);
576 let minutes_description = get_minutes_description(expression_parts, options);
577 let hours_description = get_hours_description(expression_parts, options);
578 description.append(seconds_description);
582 if description.len() > 0 && !minutes_description.is_empty() {
583 description.append(", ");
584 }
585 description.append(minutes_description);
586 if description.len() > 0 && !hours_description.is_empty() {
587 description.append(", ");
588 }
589 description.append(hours_description);
590 }
591 description.string().unwrap()
592 }
593
594 pub fn get_description_cron(expression: &str) -> Result<String, ParseException> {
595 get_description(
597 DescriptionTypeEnum::FULL,
598 expression,
599 &Options::options(),
600 &rust_i18n::locale(),
601 )
602 }
603
604 pub fn get_description_cron_options(
605 expression: &str,
606 options: &cronparser::Options,
607 ) -> Result<String, ParseException> {
608 get_description(
609 DescriptionTypeEnum::FULL,
610 expression,
611 options,
612 &rust_i18n::locale(),
613 )
614 }
615
616 pub fn get_description_cron_locale(expression: &str, locale: &str) -> Result<String, ParseException> {
617 get_description(
618 DescriptionTypeEnum::FULL,
619 expression,
620 &Options::options(),
621 locale,
622 )
623 }
624
625 pub fn get_description_cron_options_locale(
626 expression: &str,
627 options: &Options,
628 locale: &str,
629 ) -> Result<String, ParseException> {
630 get_description(DescriptionTypeEnum::FULL, expression, options, locale)
631 }
632
633 pub fn get_description_cron_type_expr(
634 desc_type: DescriptionTypeEnum,
635 expression: &str,
636 ) -> Result<String, ParseException> {
637 get_description(
638 desc_type,
639 expression,
640 &Options::options(),
641 &rust_i18n::locale(),
642 )
643 }
644
645 pub fn get_description_cron_type_expr_locale(
646 desc_type: DescriptionTypeEnum,
647 expression: &str,
648 locale: &str,
649 ) -> Result<String, ParseException> {
650 get_description(desc_type, expression, &Options::options(), locale)
651 }
652
653 pub fn get_description_cron_type_expr_opts(
654 desc_type: DescriptionTypeEnum,
655 expression: &str,
656 options: &Options,
657 ) -> Result<String, ParseException> {
658 get_description(desc_type, expression, options, &rust_i18n::locale())
659 }
660 }
661}