From 1ca07681d40658d0124c968e70d2a963839280e1 Mon Sep 17 00:00:00 2001 From: chizor iwuh Date: Sun, 7 May 2023 09:30:30 +0100 Subject: [PATCH 1/2] cron analyzer --- Cargo.toml | 1 + src/components/cron_tab.rs | 35 +++ src/components/mod.rs | 2 + src/components/navbar.rs | 4 + src/main.rs | 1 + src/pages/cron.rs | 22 ++ src/pages/mod.rs | 2 + src/utils/cron_analyzer.rs | 566 +++++++++++++++++++++++++++++++++++++ src/utils/mod.rs | 2 + 9 files changed, 635 insertions(+) create mode 100644 src/components/cron_tab.rs create mode 100644 src/pages/cron.rs create mode 100644 src/utils/cron_analyzer.rs diff --git a/Cargo.toml b/Cargo.toml index e472fbd..3a10892 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ gloo-utils = "0.1.4" js-sys = "0.3.61" log = { version = "0.4", features = ["max_level_info"] } pyth-sdk-solana = "0.7.1" +regex = "1.8.1" reqwest = { version = "0.11.16", features = ["blocking", "json"] } serde = { version = "1", features = ["derive"] } serde_json = "1.0" diff --git a/src/components/cron_tab.rs b/src/components/cron_tab.rs new file mode 100644 index 0000000..c442024 --- /dev/null +++ b/src/components/cron_tab.rs @@ -0,0 +1,35 @@ +use dioxus::{html::input_data::keyboard_types::Key, prelude::*}; + +use crate::utils::CronAnalyzer; + +pub fn CronTab(cx: Scope) -> Element { + let cron_state = use_state(cx, || "0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri 2018/2".to_owned()); + let cron_result = use_state(cx, || "".to_owned()); + let expr = cron_state.get(); + + // Move the focus to the search bar. + // autofocus property on input is having issues: https://github.com/DioxusLabs/dioxus/issues/725 + + cx.render(rsx! { + input { + class: "rounded bg-[#0e0e10] border-2 border-white focus:border-0 text-slate-100 p-5 w-full focus:ring-0 focus:outline-0 text-base", + id: "cron-tab", + r#type: "text", + placeholder: "Enter 7 field cron expression e.g */10 * * * * * *", + value: "{expr}", + oninput: move |e| { + // v = e.value.clone().as_str().to_string(); + cron_state.set(e.value.clone().as_str().to_string()); + }, + onclick: move |e| e.stop_propagation(), + onkeydown: move |e| { + if e.key() == Key::Enter { + cron_result.set(CronAnalyzer::from_expr(expr.clone())) + } + }, + } + p { + "{cron_result.get()}" + } + }) +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 2db3537..f20a1ef 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -4,6 +4,7 @@ pub mod blocks_table; pub mod chat; pub mod clock; pub mod connect_button; +pub mod cron_tab; pub mod markets_table; pub mod navbar; pub mod page_control; @@ -21,6 +22,7 @@ pub use blocks_table::*; pub use chat::*; pub use clock::*; pub use connect_button::*; +pub use cron_tab::*; pub use markets_table::*; pub use navbar::*; pub use page_control::*; diff --git a/src/components/navbar.rs b/src/components/navbar.rs index 5a4edce..61e2a05 100644 --- a/src/components/navbar.rs +++ b/src/components/navbar.rs @@ -10,6 +10,10 @@ pub fn Navbar(cx: Scope) -> Element { Logo {} div { class: "flex items-center space-x-4", + Link { + to: "/cron", + "Cron" + } SearchButton {} ConnectButton {} } diff --git a/src/main.rs b/src/main.rs index 50506e2..07bac83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,6 +73,7 @@ fn App(cx: Scope) -> Element { // Route { to: "/keys", KeysPage{} } // Route { to: "/keys/new", NewKeyPage{} } Route { to: "/", ProgramsPage{} } + Route { to: "/cron", CronPage{} } Route { to: "/threads/:address", ThreadPage {} } Route { to: "/transaction/:signature", TransactionPage {} } Route { to: "", NotFoundPage{} } diff --git a/src/pages/cron.rs b/src/pages/cron.rs new file mode 100644 index 0000000..71b801a --- /dev/null +++ b/src/pages/cron.rs @@ -0,0 +1,22 @@ +use dioxus::prelude::*; + +use crate::components::CronTab; + +use super::Page; + +pub fn CronPage(cx: Scope) -> Element { + cx.render(rsx! { + Page { + div { + class: "flex flex-col space-y-12", + + h1 { + class: "text-2xl font-semibold mb-6", + "Cron Analyzer" + } + + CronTab {} + } + } + }) +} diff --git a/src/pages/mod.rs b/src/pages/mod.rs index 660d7bf..9944936 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -1,5 +1,6 @@ pub mod account; pub mod accounts; +pub mod cron; pub mod files; pub mod home; pub mod keys; @@ -13,6 +14,7 @@ pub mod transaction; pub use account::*; pub use accounts::*; +pub use cron::*; pub use files::*; pub use home::*; pub use keys::*; diff --git a/src/utils/cron_analyzer.rs b/src/utils/cron_analyzer.rs new file mode 100644 index 0000000..75e71de --- /dev/null +++ b/src/utils/cron_analyzer.rs @@ -0,0 +1,566 @@ +use regex::Regex; + +// TODO: Should throw and handle errors on bad expressions +pub struct CronAnalyzer { + second: SecondField, + minute: MinuteField, + hour: HourField, + day_of_month: DayOfMonthField, + month: MonthField, + day_of_week: DayOfWeekField, + year: YearField, +} + +impl CronAnalyzer { + pub fn from_expr(expression: String) -> String { + let split_expression = expression.trim().split_whitespace().collect::>(); + + let second = SecondField { + raw: split_expression[0].to_owned(), + }; + let minute = MinuteField { + raw: split_expression[1].to_owned(), + }; + let hour = HourField { + raw: split_expression[2].to_owned(), + }; + let day_of_month = DayOfMonthField { + raw: split_expression[3].to_owned(), + }; + let month = MonthField { + raw: split_expression[4].to_owned(), + }; + let day_of_week = DayOfWeekField { + raw: split_expression[5].to_owned(), + }; + let year = YearField { + raw: split_expression[6].to_owned(), + }; + + let analyzer = CronAnalyzer { + second, + minute, + hour, + day_of_month, + month, + day_of_week, + year, + }; + + analyzer.analyze() + } + + pub fn analyze(&self) -> String { + + let second = &self.second; + let minute = &self.minute; + let hour = &self.hour; + let day_of_month = &self.day_of_month; + let month = &self.month; + let day_of_week = &self.day_of_week; + let year = &self.year; + + let days_anded = hour.raw.starts_with("*") || month.raw.starts_with("*"); + + let s = match !day_of_month.analyze().is_empty() && !day_of_week.analyze().is_empty() { + false => "".to_owned(), + true => match days_anded { + false => "and".to_owned(), + true => "if it's".to_owned(), + }, + }; + + let re = Regex::new(r"^0*\d\d?$").unwrap(); + + let time: Option<[String; 3]> = + match re.is_match(&second.raw) && re.is_match(&minute.raw) && re.is_match(&hour.raw) { + true => { + let second = "0".to_owned() + &second.raw; + let minute = "0".to_owned() + &minute.raw; + let hour = "0".to_owned() + &hour.raw; + Some([ + second[second.len() - 2..].to_owned(), + minute[minute.len() - 2..].to_owned(), + hour[hour.len() - 2..].to_owned(), + ]) + } + false => None, + }; + + match time { + Some(t) => { + ("At ".to_owned() + + &t[2] + + ":" + + &t[1] + + ":" + + &t[0] + + " " + + &day_of_month.analyze() + + " " + + &s + + " " + + &day_of_week.analyze() + + " " + + &month.analyze() + + " " + + &year.analyze()) + .trim() + .to_owned() + + "." + } + None => { + ("At ".to_owned() + + &second.analyze() + + " " + + &minute.analyze() + + " " + + &hour.analyze() + + " " + + &day_of_month.analyze() + + " " + + &s + + " " + + &day_of_week.analyze() + + " " + + &month.analyze() + + " " + + &year.analyze()) + .trim() + .to_owned() + + "." + } + } + } +} + +pub struct SecondField { + pub raw: String, +} + +impl<'a> Field<'a> for SecondField { + fn raw(&self) -> String { + self.raw.clone() + } + + fn name(&self) -> String { + "second".to_owned() + } + fn min(&self) -> usize { + 0 + } + fn max(&self) -> usize { + 59 + } + fn selection(&self) -> Option> { + None + } + + fn convert_if_word(&self, input: &str) -> String { + input.to_owned() + } + + fn analyze(&self) -> String { + self.format_field(false) + } +} + +pub struct MinuteField { + pub raw: String, +} + +impl<'a> Field<'a> for MinuteField { + fn raw(&self) -> String { + self.raw.clone() + } + + fn name(&self) -> String { + "minute".to_owned() + } + fn min(&self) -> usize { + 0 + } + fn max(&self) -> usize { + 59 + } + fn selection(&self) -> Option> { + None + } + + fn convert_if_word(&self, input: &str) -> String { + input.to_owned() + } + + fn analyze(&self) -> String { + match self.raw.as_str() { + "*" => "".to_owned(), + _ => "past ".to_owned() + self.format_field(false).as_str(), + } + } +} + +pub struct HourField { + pub raw: String, +} + +impl<'a> Field<'a> for HourField { + fn raw(&self) -> String { + self.raw.clone() + } + + fn name(&self) -> String { + "hour".to_owned() + } + fn min(&self) -> usize { + 0 + } + fn max(&self) -> usize { + 23 + } + fn selection(&self) -> Option> { + None + } + + fn convert_if_word(&self, input: &str) -> String { + input.to_owned() + } + + fn analyze(&self) -> String { + match self.raw.as_str() { + "*" => "".to_owned(), + _ => "past ".to_owned() + self.format_field(false).as_str(), + } + } +} + +pub struct DayOfMonthField { + pub raw: String, +} + +impl<'a> Field<'a> for DayOfMonthField { + fn raw(&self) -> String { + self.raw.clone() + } + + fn name(&self) -> String { + "day-of-month".to_owned() + } + fn min(&self) -> usize { + 1 + } + fn max(&self) -> usize { + 31 + } + fn selection(&self) -> Option> { + None + } + + fn convert_if_word(&self, input: &str) -> String { + input.to_owned() + } + + fn analyze(&self) -> String { + match self.raw.as_str() { + "*" => "".to_owned(), + _ => " on ".to_owned() + self.format_field(false).as_str(), + } + } +} + +pub struct MonthField { + pub raw: String, +} + +impl<'a> Field<'a> for MonthField { + fn raw(&self) -> String { + self.raw.clone() + } + fn name(&self) -> String { + "month".to_owned() + } + fn min(&self) -> usize { + 1 + } + fn max(&self) -> usize { + 12 + } + fn selection(&self) -> Option> { + Some(vec![ + "", + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]) + } + + fn convert_if_word(&self, input: &str) -> String { + match input.to_lowercase().as_str() { + "jan" | "january" => "1".to_owned(), + "feb" | "february" => "2".to_owned(), + "mar" | "march" => "3".to_owned(), + "apr" | "april" => "4".to_owned(), + "may" => "5".to_owned(), + "jun" | "june" => "6".to_owned(), + "jul" | "july" => "7".to_owned(), + "aug" | "august" => "8".to_owned(), + "sep" | "september" => "9".to_owned(), + "oct" | "october" => "10".to_owned(), + "nov" | "november" => "11".to_owned(), + "dec" | "december" => "12".to_owned(), + _ => input.to_owned(), + } + } + + fn analyze(&self) -> String { + match self.raw.as_str() { + "*" => "".to_owned(), + _ => " in ".to_owned() + self.format_field(true).as_str(), + } + } +} + +pub struct DayOfWeekField { + pub raw: String, +} + +impl<'a> Field<'a> for DayOfWeekField { + fn raw(&self) -> String { + self.raw.clone() + } + fn name(&self) -> String { + "day-of-week".to_owned() + } + fn min(&self) -> usize { + 1 + } + fn max(&self) -> usize { + 7 + } + fn selection(&self) -> Option> { + Some(vec![ + "", + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ]) + } + + fn convert_if_word(&self, input: &str) -> String { + match input.to_lowercase().as_str() { + "sun" | "sunday" => "1".to_owned(), + "mon" | "monday" => "2".to_owned(), + "tue" | "tues" | "tuesday" => "3".to_owned(), + "wed" | "wednesday" => "4".to_owned(), + "thu" | "thurs" | "thursday" => "5".to_owned(), + "fri" | "friday" => "6".to_owned(), + "sat" | "saturday" => "7".to_owned(), + _ => input.to_owned(), + } + } + + fn analyze(&self) -> String { + match self.raw.as_str() { + "*" => "".to_owned(), + _ => " on ".to_owned() + self.format_field(true).as_str(), + } + } +} + +pub struct YearField { + pub raw: String, +} + +impl<'a> Field<'a> for YearField { + fn raw(&self) -> String { + self.raw.clone() + } + fn name(&self) -> String { + "year".to_owned() + } + fn min(&self) -> usize { + 1970 + } + fn max(&self) -> usize { + 2100 + } + fn selection(&self) -> Option> { + None + } + + fn convert_if_word(&self, input: &str) -> String { + input.to_owned() + } + + fn analyze(&self) -> String { + match self.raw.as_str() { + "*" => "".to_owned(), + _ => " in ".to_owned() + self.format_field(false).as_str(), + } + } +} + + + +pub trait Field<'a> { + fn raw(&self) -> String; + fn name(&self) -> String; + fn min(&self) -> usize; + fn max(&self) -> usize; + fn selection(&self) -> Option>; + fn convert_if_word(&self, input: &str) -> String; + fn analyze(&self) -> String; + + fn suffix(&self, number: &str) -> String { + match number.parse::() { + Err(_) => String::from("Not a string"), + Ok(num) => { + // let some = if num > 20 { num % 10 } else { num }; + match num % 10 { + 1 => num.to_string() + "st", + 2 => num.to_string() + "nd", + 3 => num.to_string() + "rd", + _ => num.to_string() + "th", + } + } + } + } + + fn format_field(&self, d_or_m: bool) -> String { + let raw_string = self.raw(); + let name = self.name(); + let formatted_sections = raw_string + .split(",") + .map(|section| self.format_field_section(section)) + .collect::>(); + let formatted_string = match formatted_sections.len() { + 0 => "".to_string(), + 1 => formatted_sections[0].to_string(), + 2 => (formatted_sections[0].to_owned() + "and" + &formatted_sections[1]).to_string(), + _ => { + formatted_sections[0..formatted_sections.len() - 1].join(",") + + ", and" + + &formatted_sections[formatted_sections.len() - 1] + } + }; + let some = match d_or_m { + true => " ".to_owned(), + false => name.to_owned() + " ", + }; + println!("to cancel: {}{}", &some, &formatted_string); + (some + &formatted_string) + .replace("every 1st", "every") + .replace((name.to_owned() + " every").as_str(), "every") + .replace((", ".to_owned() + &name).as_str(), ", ") + .replace((", and ".to_owned() + &name).as_str(), ", and ") + } + + fn format_field_section(&self, section: &str) -> String { + // let re = Regex::new(r"\d+|.").unwrap(); + // let some = re.find(field).expect("could not find match").as_str(); + + let raw_string = section.to_owned(); + let selection = self.selection(); + let max = self.max(); + let name = self.name(); + + if raw_string == "*" { + return "every ".to_owned() + &self.name(); + } else { + let re = Regex::new(r"\d+|\w+|.").unwrap(); + + // let some = field.matches(r"\d+|.").collect::>(); + let sections = re + .find_iter(&raw_string) + .map(|m| self.convert_if_word(m.as_str())) + .collect::>(); + println!("some: {:?}", §ions); + + let date_from_selection = |index: usize| match &selection { + None => index.to_string(), + Some(v) => v[index].to_string(), + }; + + match sections[0].parse::() { + Ok(index) => match sections.len() { + 1 => "".to_owned() + &date_from_selection(index), + 3 => { + // if let Ok(num) = some[2].parse::(){}; + match sections[2].parse::() { + Err(_) => "".to_owned(), + Ok(num) => match sections[1].as_str() { + "/" => { + "every ".to_owned() + + &self.suffix(§ions[2]) + + " " + + &name + + " from " + + &date_from_selection(index) + + " through " + + &date_from_selection(max) + } + "-" => { + "every ".to_owned() + + " " + + &name + + " from " + + &date_from_selection(index) + + " through " + + &date_from_selection(num) + } + _ => "".to_owned(), + }, + } + } + 5 => { + let num = sections[2].parse::().unwrap(); + match sections[1] == "-" + && num >= index + && sections[3] == "/" + && sections[4].parse::().unwrap() >= 1 + { + true => { + "every ".to_owned() + + &self.suffix(§ions[4]) + + " " + + &name + + " " + + " from " + + &date_from_selection(index) + + " through " + + &date_from_selection(num) + } + false => "".to_owned(), + } + } + _ => "".to_owned(), + }, + Err(_) => { + match sections.len() == 3 + && sections[1] == "/" + && sections[2].parse::().is_ok() + && sections[0] == "*" + { + true => { + "every ".to_owned() + " " + &self.suffix(§ions[2]) + " " + &name + } + false => "".to_owned(), + } + } + } + } + } +} + diff --git a/src/utils/mod.rs b/src/utils/mod.rs index ddec491..f381873 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,5 +1,7 @@ pub mod client; +pub mod cron_analyzer; pub mod format; pub use client::*; +pub use cron_analyzer::*; pub use format::*; From 9c412ffbad4f0280070a2d645cf58fe316f55c6e Mon Sep 17 00:00:00 2001 From: chizor iwuh Date: Thu, 11 May 2023 03:07:25 +0100 Subject: [PATCH 2/2] handle bad expressions --- src/components/cron_tab.rs | 6 +- src/utils/cron_analyzer.rs | 566 ------------------------ src/utils/cron_analyzer/day_of_month.rs | 38 ++ src/utils/cron_analyzer/day_of_week.rs | 55 +++ src/utils/cron_analyzer/error.rs | 21 + src/utils/cron_analyzer/field.rs | 173 ++++++++ src/utils/cron_analyzer/hour.rs | 41 ++ src/utils/cron_analyzer/minute.rs | 41 ++ src/utils/cron_analyzer/mod.rs | 157 +++++++ src/utils/cron_analyzer/month.rs | 65 +++ src/utils/cron_analyzer/second.rs | 35 ++ src/utils/cron_analyzer/year.rs | 37 ++ 12 files changed, 668 insertions(+), 567 deletions(-) delete mode 100644 src/utils/cron_analyzer.rs create mode 100644 src/utils/cron_analyzer/day_of_month.rs create mode 100644 src/utils/cron_analyzer/day_of_week.rs create mode 100644 src/utils/cron_analyzer/error.rs create mode 100644 src/utils/cron_analyzer/field.rs create mode 100644 src/utils/cron_analyzer/hour.rs create mode 100644 src/utils/cron_analyzer/minute.rs create mode 100644 src/utils/cron_analyzer/mod.rs create mode 100644 src/utils/cron_analyzer/month.rs create mode 100644 src/utils/cron_analyzer/second.rs create mode 100644 src/utils/cron_analyzer/year.rs diff --git a/src/components/cron_tab.rs b/src/components/cron_tab.rs index c442024..136f112 100644 --- a/src/components/cron_tab.rs +++ b/src/components/cron_tab.rs @@ -24,7 +24,11 @@ pub fn CronTab(cx: Scope) -> Element { onclick: move |e| e.stop_propagation(), onkeydown: move |e| { if e.key() == Key::Enter { - cron_result.set(CronAnalyzer::from_expr(expr.clone())) + let analysis = match CronAnalyzer::from_expr(expr.clone()) { + Ok(analysis) => analysis, + Err(e) => format!("{e}") + }; + cron_result.set(analysis) } }, } diff --git a/src/utils/cron_analyzer.rs b/src/utils/cron_analyzer.rs deleted file mode 100644 index 75e71de..0000000 --- a/src/utils/cron_analyzer.rs +++ /dev/null @@ -1,566 +0,0 @@ -use regex::Regex; - -// TODO: Should throw and handle errors on bad expressions -pub struct CronAnalyzer { - second: SecondField, - minute: MinuteField, - hour: HourField, - day_of_month: DayOfMonthField, - month: MonthField, - day_of_week: DayOfWeekField, - year: YearField, -} - -impl CronAnalyzer { - pub fn from_expr(expression: String) -> String { - let split_expression = expression.trim().split_whitespace().collect::>(); - - let second = SecondField { - raw: split_expression[0].to_owned(), - }; - let minute = MinuteField { - raw: split_expression[1].to_owned(), - }; - let hour = HourField { - raw: split_expression[2].to_owned(), - }; - let day_of_month = DayOfMonthField { - raw: split_expression[3].to_owned(), - }; - let month = MonthField { - raw: split_expression[4].to_owned(), - }; - let day_of_week = DayOfWeekField { - raw: split_expression[5].to_owned(), - }; - let year = YearField { - raw: split_expression[6].to_owned(), - }; - - let analyzer = CronAnalyzer { - second, - minute, - hour, - day_of_month, - month, - day_of_week, - year, - }; - - analyzer.analyze() - } - - pub fn analyze(&self) -> String { - - let second = &self.second; - let minute = &self.minute; - let hour = &self.hour; - let day_of_month = &self.day_of_month; - let month = &self.month; - let day_of_week = &self.day_of_week; - let year = &self.year; - - let days_anded = hour.raw.starts_with("*") || month.raw.starts_with("*"); - - let s = match !day_of_month.analyze().is_empty() && !day_of_week.analyze().is_empty() { - false => "".to_owned(), - true => match days_anded { - false => "and".to_owned(), - true => "if it's".to_owned(), - }, - }; - - let re = Regex::new(r"^0*\d\d?$").unwrap(); - - let time: Option<[String; 3]> = - match re.is_match(&second.raw) && re.is_match(&minute.raw) && re.is_match(&hour.raw) { - true => { - let second = "0".to_owned() + &second.raw; - let minute = "0".to_owned() + &minute.raw; - let hour = "0".to_owned() + &hour.raw; - Some([ - second[second.len() - 2..].to_owned(), - minute[minute.len() - 2..].to_owned(), - hour[hour.len() - 2..].to_owned(), - ]) - } - false => None, - }; - - match time { - Some(t) => { - ("At ".to_owned() - + &t[2] - + ":" - + &t[1] - + ":" - + &t[0] - + " " - + &day_of_month.analyze() - + " " - + &s - + " " - + &day_of_week.analyze() - + " " - + &month.analyze() - + " " - + &year.analyze()) - .trim() - .to_owned() - + "." - } - None => { - ("At ".to_owned() - + &second.analyze() - + " " - + &minute.analyze() - + " " - + &hour.analyze() - + " " - + &day_of_month.analyze() - + " " - + &s - + " " - + &day_of_week.analyze() - + " " - + &month.analyze() - + " " - + &year.analyze()) - .trim() - .to_owned() - + "." - } - } - } -} - -pub struct SecondField { - pub raw: String, -} - -impl<'a> Field<'a> for SecondField { - fn raw(&self) -> String { - self.raw.clone() - } - - fn name(&self) -> String { - "second".to_owned() - } - fn min(&self) -> usize { - 0 - } - fn max(&self) -> usize { - 59 - } - fn selection(&self) -> Option> { - None - } - - fn convert_if_word(&self, input: &str) -> String { - input.to_owned() - } - - fn analyze(&self) -> String { - self.format_field(false) - } -} - -pub struct MinuteField { - pub raw: String, -} - -impl<'a> Field<'a> for MinuteField { - fn raw(&self) -> String { - self.raw.clone() - } - - fn name(&self) -> String { - "minute".to_owned() - } - fn min(&self) -> usize { - 0 - } - fn max(&self) -> usize { - 59 - } - fn selection(&self) -> Option> { - None - } - - fn convert_if_word(&self, input: &str) -> String { - input.to_owned() - } - - fn analyze(&self) -> String { - match self.raw.as_str() { - "*" => "".to_owned(), - _ => "past ".to_owned() + self.format_field(false).as_str(), - } - } -} - -pub struct HourField { - pub raw: String, -} - -impl<'a> Field<'a> for HourField { - fn raw(&self) -> String { - self.raw.clone() - } - - fn name(&self) -> String { - "hour".to_owned() - } - fn min(&self) -> usize { - 0 - } - fn max(&self) -> usize { - 23 - } - fn selection(&self) -> Option> { - None - } - - fn convert_if_word(&self, input: &str) -> String { - input.to_owned() - } - - fn analyze(&self) -> String { - match self.raw.as_str() { - "*" => "".to_owned(), - _ => "past ".to_owned() + self.format_field(false).as_str(), - } - } -} - -pub struct DayOfMonthField { - pub raw: String, -} - -impl<'a> Field<'a> for DayOfMonthField { - fn raw(&self) -> String { - self.raw.clone() - } - - fn name(&self) -> String { - "day-of-month".to_owned() - } - fn min(&self) -> usize { - 1 - } - fn max(&self) -> usize { - 31 - } - fn selection(&self) -> Option> { - None - } - - fn convert_if_word(&self, input: &str) -> String { - input.to_owned() - } - - fn analyze(&self) -> String { - match self.raw.as_str() { - "*" => "".to_owned(), - _ => " on ".to_owned() + self.format_field(false).as_str(), - } - } -} - -pub struct MonthField { - pub raw: String, -} - -impl<'a> Field<'a> for MonthField { - fn raw(&self) -> String { - self.raw.clone() - } - fn name(&self) -> String { - "month".to_owned() - } - fn min(&self) -> usize { - 1 - } - fn max(&self) -> usize { - 12 - } - fn selection(&self) -> Option> { - Some(vec![ - "", - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ]) - } - - fn convert_if_word(&self, input: &str) -> String { - match input.to_lowercase().as_str() { - "jan" | "january" => "1".to_owned(), - "feb" | "february" => "2".to_owned(), - "mar" | "march" => "3".to_owned(), - "apr" | "april" => "4".to_owned(), - "may" => "5".to_owned(), - "jun" | "june" => "6".to_owned(), - "jul" | "july" => "7".to_owned(), - "aug" | "august" => "8".to_owned(), - "sep" | "september" => "9".to_owned(), - "oct" | "october" => "10".to_owned(), - "nov" | "november" => "11".to_owned(), - "dec" | "december" => "12".to_owned(), - _ => input.to_owned(), - } - } - - fn analyze(&self) -> String { - match self.raw.as_str() { - "*" => "".to_owned(), - _ => " in ".to_owned() + self.format_field(true).as_str(), - } - } -} - -pub struct DayOfWeekField { - pub raw: String, -} - -impl<'a> Field<'a> for DayOfWeekField { - fn raw(&self) -> String { - self.raw.clone() - } - fn name(&self) -> String { - "day-of-week".to_owned() - } - fn min(&self) -> usize { - 1 - } - fn max(&self) -> usize { - 7 - } - fn selection(&self) -> Option> { - Some(vec![ - "", - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - ]) - } - - fn convert_if_word(&self, input: &str) -> String { - match input.to_lowercase().as_str() { - "sun" | "sunday" => "1".to_owned(), - "mon" | "monday" => "2".to_owned(), - "tue" | "tues" | "tuesday" => "3".to_owned(), - "wed" | "wednesday" => "4".to_owned(), - "thu" | "thurs" | "thursday" => "5".to_owned(), - "fri" | "friday" => "6".to_owned(), - "sat" | "saturday" => "7".to_owned(), - _ => input.to_owned(), - } - } - - fn analyze(&self) -> String { - match self.raw.as_str() { - "*" => "".to_owned(), - _ => " on ".to_owned() + self.format_field(true).as_str(), - } - } -} - -pub struct YearField { - pub raw: String, -} - -impl<'a> Field<'a> for YearField { - fn raw(&self) -> String { - self.raw.clone() - } - fn name(&self) -> String { - "year".to_owned() - } - fn min(&self) -> usize { - 1970 - } - fn max(&self) -> usize { - 2100 - } - fn selection(&self) -> Option> { - None - } - - fn convert_if_word(&self, input: &str) -> String { - input.to_owned() - } - - fn analyze(&self) -> String { - match self.raw.as_str() { - "*" => "".to_owned(), - _ => " in ".to_owned() + self.format_field(false).as_str(), - } - } -} - - - -pub trait Field<'a> { - fn raw(&self) -> String; - fn name(&self) -> String; - fn min(&self) -> usize; - fn max(&self) -> usize; - fn selection(&self) -> Option>; - fn convert_if_word(&self, input: &str) -> String; - fn analyze(&self) -> String; - - fn suffix(&self, number: &str) -> String { - match number.parse::() { - Err(_) => String::from("Not a string"), - Ok(num) => { - // let some = if num > 20 { num % 10 } else { num }; - match num % 10 { - 1 => num.to_string() + "st", - 2 => num.to_string() + "nd", - 3 => num.to_string() + "rd", - _ => num.to_string() + "th", - } - } - } - } - - fn format_field(&self, d_or_m: bool) -> String { - let raw_string = self.raw(); - let name = self.name(); - let formatted_sections = raw_string - .split(",") - .map(|section| self.format_field_section(section)) - .collect::>(); - let formatted_string = match formatted_sections.len() { - 0 => "".to_string(), - 1 => formatted_sections[0].to_string(), - 2 => (formatted_sections[0].to_owned() + "and" + &formatted_sections[1]).to_string(), - _ => { - formatted_sections[0..formatted_sections.len() - 1].join(",") - + ", and" - + &formatted_sections[formatted_sections.len() - 1] - } - }; - let some = match d_or_m { - true => " ".to_owned(), - false => name.to_owned() + " ", - }; - println!("to cancel: {}{}", &some, &formatted_string); - (some + &formatted_string) - .replace("every 1st", "every") - .replace((name.to_owned() + " every").as_str(), "every") - .replace((", ".to_owned() + &name).as_str(), ", ") - .replace((", and ".to_owned() + &name).as_str(), ", and ") - } - - fn format_field_section(&self, section: &str) -> String { - // let re = Regex::new(r"\d+|.").unwrap(); - // let some = re.find(field).expect("could not find match").as_str(); - - let raw_string = section.to_owned(); - let selection = self.selection(); - let max = self.max(); - let name = self.name(); - - if raw_string == "*" { - return "every ".to_owned() + &self.name(); - } else { - let re = Regex::new(r"\d+|\w+|.").unwrap(); - - // let some = field.matches(r"\d+|.").collect::>(); - let sections = re - .find_iter(&raw_string) - .map(|m| self.convert_if_word(m.as_str())) - .collect::>(); - println!("some: {:?}", §ions); - - let date_from_selection = |index: usize| match &selection { - None => index.to_string(), - Some(v) => v[index].to_string(), - }; - - match sections[0].parse::() { - Ok(index) => match sections.len() { - 1 => "".to_owned() + &date_from_selection(index), - 3 => { - // if let Ok(num) = some[2].parse::(){}; - match sections[2].parse::() { - Err(_) => "".to_owned(), - Ok(num) => match sections[1].as_str() { - "/" => { - "every ".to_owned() - + &self.suffix(§ions[2]) - + " " - + &name - + " from " - + &date_from_selection(index) - + " through " - + &date_from_selection(max) - } - "-" => { - "every ".to_owned() - + " " - + &name - + " from " - + &date_from_selection(index) - + " through " - + &date_from_selection(num) - } - _ => "".to_owned(), - }, - } - } - 5 => { - let num = sections[2].parse::().unwrap(); - match sections[1] == "-" - && num >= index - && sections[3] == "/" - && sections[4].parse::().unwrap() >= 1 - { - true => { - "every ".to_owned() - + &self.suffix(§ions[4]) - + " " - + &name - + " " - + " from " - + &date_from_selection(index) - + " through " - + &date_from_selection(num) - } - false => "".to_owned(), - } - } - _ => "".to_owned(), - }, - Err(_) => { - match sections.len() == 3 - && sections[1] == "/" - && sections[2].parse::().is_ok() - && sections[0] == "*" - { - true => { - "every ".to_owned() + " " + &self.suffix(§ions[2]) + " " + &name - } - false => "".to_owned(), - } - } - } - } - } -} - diff --git a/src/utils/cron_analyzer/day_of_month.rs b/src/utils/cron_analyzer/day_of_month.rs new file mode 100644 index 0000000..026fbdc --- /dev/null +++ b/src/utils/cron_analyzer/day_of_month.rs @@ -0,0 +1,38 @@ +use super::{field::Field, error::CronAnalyzerError}; + +pub struct DayOfMonthField { + pub raw: String, +} + +impl<'a> Field<'a> for DayOfMonthField { + fn raw(&self) -> String { + self.raw.clone() + } + + fn name(&self) -> String { + "day-of-month".to_owned() + } + fn min(&self) -> usize { + 1 + } + fn max(&self) -> usize { + 31 + } + fn selection(&self) -> Option> { + None + } + + fn convert_if_word(&self, input: &str) -> String { + input.to_owned() + } + + fn analyze(&self) -> Result { + match self.raw.as_str() { + "*" => Ok(format!("")), + _ => match self.format_field(false) { + Ok(s) => Ok(format!("on {s}")), + Err(e) => Err(e), + }, + } + } +} \ No newline at end of file diff --git a/src/utils/cron_analyzer/day_of_week.rs b/src/utils/cron_analyzer/day_of_week.rs new file mode 100644 index 0000000..5863a70 --- /dev/null +++ b/src/utils/cron_analyzer/day_of_week.rs @@ -0,0 +1,55 @@ +use super::{field::Field, error::CronAnalyzerError}; + +pub struct DayOfWeekField { + pub raw: String, +} + +impl<'a> Field<'a> for DayOfWeekField { + fn raw(&self) -> String { + self.raw.clone() + } + fn name(&self) -> String { + "day-of-week".to_owned() + } + fn min(&self) -> usize { + 1 + } + fn max(&self) -> usize { + 7 + } + fn selection(&self) -> Option> { + Some(vec![ + "", + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ]) + } + + fn convert_if_word(&self, input: &str) -> String { + match input.to_lowercase().as_str() { + "sun" | "sunday" => "1".to_owned(), + "mon" | "monday" => "2".to_owned(), + "tue" | "tues" | "tuesday" => "3".to_owned(), + "wed" | "wednesday" => "4".to_owned(), + "thu" | "thurs" | "thursday" => "5".to_owned(), + "fri" | "friday" => "6".to_owned(), + "sat" | "saturday" => "7".to_owned(), + _ => input.to_owned(), + } + } + + fn analyze(&self) -> Result { + match self.raw.as_str() { + "*" => Ok(format!("")), + _ => match self.format_field(true) { + Ok(s) => Ok(format!("on {s}")), + Err(e) => Err(e), + }, + } + } +} \ No newline at end of file diff --git a/src/utils/cron_analyzer/error.rs b/src/utils/cron_analyzer/error.rs new file mode 100644 index 0000000..0edb773 --- /dev/null +++ b/src/utils/cron_analyzer/error.rs @@ -0,0 +1,21 @@ +use std::{error, fmt }; + +#[derive(Debug)] +pub struct CronAnalyzerError { + msg: String, +} + +impl CronAnalyzerError { + pub fn new(msg: String) -> Self { + CronAnalyzerError { msg } + } +} + +impl error::Error for CronAnalyzerError {} + +impl fmt::Display for CronAnalyzerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.msg) + } +} + diff --git a/src/utils/cron_analyzer/field.rs b/src/utils/cron_analyzer/field.rs new file mode 100644 index 0000000..fa4224a --- /dev/null +++ b/src/utils/cron_analyzer/field.rs @@ -0,0 +1,173 @@ +use regex::Regex; + +use super::error::CronAnalyzerError; + +pub trait Field<'a> { + fn raw(&self) -> String; + fn name(&self) -> String; + fn min(&self) -> usize; + fn max(&self) -> usize; + fn selection(&self) -> Option>; + fn convert_if_word(&self, input: &str) -> String; + fn analyze(&self) -> Result; + + fn in_range(&self, check: usize) -> Result<(), CronAnalyzerError> { + match self.min() <= check && check <= self.max() { + false => Err(CronAnalyzerError::new(format!( + "Input '{check}' not within '{}' range", + self.name() + ))), + true => Ok(()), + } + } + + fn suffix(&self, number: &str) -> Result { + match number.parse::() { + Err(_) => Err(CronAnalyzerError::new(format!( + "'{number}' not a number at {}", + &self.name() + ))), + Ok(num) => Ok(match num % 10 { + 1 => num.to_string() + "st", + 2 => num.to_string() + "nd", + 3 => num.to_string() + "rd", + _ => num.to_string() + "th", + }), + } + } + + fn format_field(&self, day_of: bool) -> Result { + let raw_string = self.raw(); + let name = self.name(); + let sections: Result, CronAnalyzerError> = raw_string + .split(",") + .map(|section| self.format_field_section(section)) + .collect(); + + match sections { + Ok(formatted_sections) => { + let formatted_string = match formatted_sections.len() { + 0 => format!(""), + 1 => format!("{}", formatted_sections[0].to_string()), + 2 => format!("{} and {}", &formatted_sections[0], &formatted_sections[1]), + _ => { + format!( + "{}, and {}", + &formatted_sections[0..formatted_sections.len() - 1].join(", "), + &formatted_sections[formatted_sections.len() - 1] + ) + } + }; + let s = match day_of { + true => "".to_owned(), + false => format!("{} ", &name), + }; + + Ok(format!("{}{}", &s, &formatted_string) + .replace("every 1st", "every") + .replace(&format!("{} every", &name), "every") + .replace(&format!(", {}", &name), ",") + .replace(&format!(", and {}", &name), ", and")) + } + Err(e) => Err(e), + } + } + + fn format_field_section(&self, section: &str) -> Result { + let raw_string = section.to_owned(); + let selection = self.selection(); + let max = self.max(); + let name = self.name(); + + if raw_string == "*" { + return Ok(format!("every {}", &name)); + } else { + let re = Regex::new(r"\d+|\w+|.").unwrap(); + + let sections = re + .find_iter(&raw_string) + .map(|m| self.convert_if_word(m.as_str())) + .collect::>(); + + let date_from_selection = |index: usize| match &selection { + None => index.to_string(), + Some(v) => v[index].to_string(), + }; + + match sections[0].parse::() { + Ok(index) => { + self.in_range(index)?; + match sections.len() { + 1 => Ok(format!(" {}", &date_from_selection(index))), + 3 => match sections[2].parse::() { + Err(_) => Err(CronAnalyzerError::new(format!( + "Invalid input '{}' at '{}' field", + §ion, &name + ))), + Ok(num) => match sections[1].as_str() { + "/" => Ok(format!( + "every {} {} from {} through {}", + &self.suffix(§ions[2])?, + &name, + &date_from_selection(index), + &date_from_selection(max) + )), + "-" => { + self.in_range(num)?; + Ok(format!( + "every {} from {} through {}", + &name, + &date_from_selection(index), + &date_from_selection(num) + )) + } + _ => Err(CronAnalyzerError::new(format!( + "Invalid input '{}' at '{}' field", + §ion, &name + ))), + }, + }, + 5 => { + let num = sections[2].parse::().unwrap(); + self.in_range(num)?; + match sections[1] == "-" + && num >= index + && sections[3] == "/" + && sections[4].parse::().unwrap() >= 1 + { + true => Ok(format!( + "every {} {} from {} through {}", + &self.suffix(§ions[4])?, + &name, + &date_from_selection(index), + &date_from_selection(num) + )), + false => Err(CronAnalyzerError::new(format!( + "Invalid input '{}' at '{}' field", + §ion, &name + ))), + } + } + _ => Err(CronAnalyzerError::new(format!( + "Invalid input '{}' at '{}' field", + §ion, &name + ))), + } + } + Err(_) => { + match sections.len() == 3 + && sections[1] == "/" + && sections[2].parse::().is_ok() + && sections[0] == "*" + { + true => Ok(format!("every {} {}", &self.suffix(§ions[2])?, &name)), + false => Err(CronAnalyzerError::new(format!( + "Invalid input '{}' at '{}' field", + §ion, &name + ))), + } + } + } + } + } +} diff --git a/src/utils/cron_analyzer/hour.rs b/src/utils/cron_analyzer/hour.rs new file mode 100644 index 0000000..5fba4e5 --- /dev/null +++ b/src/utils/cron_analyzer/hour.rs @@ -0,0 +1,41 @@ +use super::{field::Field, error::CronAnalyzerError}; + +pub struct HourField { + pub raw: String, +} + +impl<'a> Field<'a> for HourField { + fn raw(&self) -> String { + self.raw.clone() + } + + fn name(&self) -> String { + "hour".to_owned() + } + fn min(&self) -> usize { + 0 + } + fn max(&self) -> usize { + 23 + } + fn selection(&self) -> Option> { + None + } + + fn convert_if_word(&self, input: &str) -> String { + input.to_owned() + } + + fn analyze(&self) -> Result { + match self.raw.as_str() { + "*" => Ok(format!("")), + _ => { + match self.format_field(false) { + + Ok(s) => Ok(format!("past {s}")), + Err(e) => Err(e) + } + }, + } + } +} diff --git a/src/utils/cron_analyzer/minute.rs b/src/utils/cron_analyzer/minute.rs new file mode 100644 index 0000000..0d86155 --- /dev/null +++ b/src/utils/cron_analyzer/minute.rs @@ -0,0 +1,41 @@ +use super::{field::Field, error::CronAnalyzerError}; + +pub struct MinuteField { + pub raw: String, +} + +impl<'a> Field<'a> for MinuteField { + fn raw(&self) -> String { + self.raw.clone() + } + + fn name(&self) -> String { + "minute".to_owned() + } + fn min(&self) -> usize { + 0 + } + fn max(&self) -> usize { + 59 + } + fn selection(&self) -> Option> { + None + } + + fn convert_if_word(&self, input: &str) -> String { + input.to_owned() + } + + fn analyze(&self) -> Result { + match self.raw.as_str() { + "*" => Ok(format!("")), + _ => { + match self.format_field(false) { + + Ok(s) => Ok(format!("past {s}")), + Err(e) => Err(e) + } + }, + } + } +} \ No newline at end of file diff --git a/src/utils/cron_analyzer/mod.rs b/src/utils/cron_analyzer/mod.rs new file mode 100644 index 0000000..f3419f4 --- /dev/null +++ b/src/utils/cron_analyzer/mod.rs @@ -0,0 +1,157 @@ +pub mod day_of_month; +pub mod day_of_week; +pub mod error; +pub mod field; +pub mod hour; +pub mod minute; +pub mod month; +pub mod second; +pub mod year; + +pub use day_of_month::*; +pub use day_of_week::*; +pub use error::*; +pub use field::Field; +pub use hour::*; +pub use minute::*; +pub use month::*; +pub use second::*; +pub use year::*; + +use regex::Regex; + +pub struct CronAnalyzer { + second: SecondField, + minute: MinuteField, + hour: HourField, + day_of_month: DayOfMonthField, + month: MonthField, + day_of_week: DayOfWeekField, + year: YearField, +} + +impl CronAnalyzer { + pub fn from_expr(expression: String) -> Result { + let split_expression = expression.trim().split_whitespace().collect::>(); + + let no_of_fields = Self::in_range(split_expression.len())?; + + let second = SecondField { + raw: split_expression[0].to_owned(), + }; + let minute = MinuteField { + raw: split_expression[1].to_owned(), + }; + let hour = HourField { + raw: split_expression[2].to_owned(), + }; + let day_of_month = DayOfMonthField { + raw: split_expression[3].to_owned(), + }; + let month = MonthField { + raw: split_expression[4].to_owned(), + }; + let day_of_week = DayOfWeekField { + raw: split_expression[5].to_owned(), + }; + let year = YearField { + raw: match no_of_fields { + 6 => "*".to_owned(), + _ => split_expression[6].to_owned(), + }, + }; + + let analyzer = CronAnalyzer { + second, + minute, + hour, + day_of_month, + month, + day_of_week, + year, + }; + + analyzer.analyze() + } + + pub fn analyze(&self) -> Result { + let second = &self.second; + let minute = &self.minute; + let hour = &self.hour; + let day_of_month = &self.day_of_month; + let month = &self.month; + let day_of_week = &self.day_of_week; + let year = &self.year; + + let days_anded = hour.raw.starts_with("*") || month.raw.starts_with("*"); + + let s = match !day_of_month.analyze()?.is_empty() && !day_of_week.analyze()?.is_empty() { + false => "".to_owned(), + true => match days_anded { + false => "and".to_owned(), + true => "if it's".to_owned(), + }, + }; + + let re = Regex::new(r"^0*\d\d?$").unwrap(); + + let time: Option<[String; 3]> = + match re.is_match(&second.raw) && re.is_match(&minute.raw) && re.is_match(&hour.raw) { + true => { + let second = format!("0{}", &second.raw); + let minute = format!("0{}", &minute.raw); + let hour = format!("0{}", &hour.raw); + Some([ + second[second.len() - 2..].to_owned(), + minute[minute.len() - 2..].to_owned(), + hour[hour.len() - 2..].to_owned(), + ]) + } + false => None, + }; + + Ok(match time { + Some(t) => { + format!( + "At {}:{}:{} {} {} {} {} {}", + &t[2], + &t[1], + &t[0], + &day_of_month.analyze()?, + &s, + &day_of_week.analyze()?, + &month.analyze()?, + &year.analyze()? + ) + .trim() + .to_owned() + + "." + } + None => { + format!( + "At {} {} {} {} {} {} {} {}", + &second.analyze()?, + &minute.analyze()?, + &hour.analyze()?, + &day_of_month.analyze()?, + &s, + &day_of_week.analyze()?, + &month.analyze()?, + &year.analyze()? + ) + .trim() + .to_owned() + + "." + } + }) + } + + fn in_range(check: usize) -> Result { + match check >= 6 && check <= 7 { + true => Ok(check), + false => Err(CronAnalyzerError::new(format!( + "Expression not within 6 - 7 fields" + ))), + } + } +} diff --git a/src/utils/cron_analyzer/month.rs b/src/utils/cron_analyzer/month.rs new file mode 100644 index 0000000..c306cb9 --- /dev/null +++ b/src/utils/cron_analyzer/month.rs @@ -0,0 +1,65 @@ +use super::{field::Field, error::CronAnalyzerError}; + +pub struct MonthField { + pub raw: String, +} + +impl<'a> Field<'a> for MonthField { + fn raw(&self) -> String { + self.raw.clone() + } + fn name(&self) -> String { + "month".to_owned() + } + fn min(&self) -> usize { + 1 + } + fn max(&self) -> usize { + 12 + } + fn selection(&self) -> Option> { + Some(vec![ + "", + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]) + } + + fn convert_if_word(&self, input: &str) -> String { + match input.to_lowercase().as_str() { + "jan" | "january" => "1".to_owned(), + "feb" | "february" => "2".to_owned(), + "mar" | "march" => "3".to_owned(), + "apr" | "april" => "4".to_owned(), + "may" => "5".to_owned(), + "jun" | "june" => "6".to_owned(), + "jul" | "july" => "7".to_owned(), + "aug" | "august" => "8".to_owned(), + "sep" | "september" => "9".to_owned(), + "oct" | "october" => "10".to_owned(), + "nov" | "november" => "11".to_owned(), + "dec" | "december" => "12".to_owned(), + _ => input.to_owned(), + } + } + + fn analyze(&self) -> Result { + match self.raw.as_str() { + "*" => Ok(format!("")), + _ => match self.format_field(true) { + Ok(s) => Ok(format!("in {s}")), + Err(e) => Err(e), + }, + } + } +} \ No newline at end of file diff --git a/src/utils/cron_analyzer/second.rs b/src/utils/cron_analyzer/second.rs new file mode 100644 index 0000000..9bc1532 --- /dev/null +++ b/src/utils/cron_analyzer/second.rs @@ -0,0 +1,35 @@ +use super::{error::CronAnalyzerError, field::Field}; + +pub struct SecondField { + pub raw: String, +} + +impl<'a> Field<'a> for SecondField { + fn raw(&self) -> String { + self.raw.clone() + } + + fn name(&self) -> String { + "second".to_owned() + } + fn min(&self) -> usize { + 0 + } + fn max(&self) -> usize { + 59 + } + fn selection(&self) -> Option> { + None + } + + fn convert_if_word(&self, input: &str) -> String { + input.to_owned() + } + + fn analyze(&self) -> Result { + match self.format_field(false) { + Ok(s) => Ok(format!("{s}")), + Err(e) => Err(e), + } + } +} diff --git a/src/utils/cron_analyzer/year.rs b/src/utils/cron_analyzer/year.rs new file mode 100644 index 0000000..941fb17 --- /dev/null +++ b/src/utils/cron_analyzer/year.rs @@ -0,0 +1,37 @@ +use super::{error::CronAnalyzerError, field::Field}; + +pub struct YearField { + pub raw: String, +} + +impl<'a> Field<'a> for YearField { + fn raw(&self) -> String { + self.raw.clone() + } + fn name(&self) -> String { + "year".to_owned() + } + fn min(&self) -> usize { + 1970 + } + fn max(&self) -> usize { + 2100 + } + fn selection(&self) -> Option> { + None + } + + fn convert_if_word(&self, input: &str) -> String { + input.to_owned() + } + + fn analyze(&self) -> Result { + match self.raw.as_str() { + "*" => Ok(format!("")), + _ => match self.format_field(false) { + Ok(s) => Ok(format!("in {s}")), + Err(e) => Err(e), + }, + } + } +}