diff --git a/Cargo.toml b/Cargo.toml index 79a156b..a995edb 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..136f112 --- /dev/null +++ b/src/components/cron_tab.rs @@ -0,0 +1,39 @@ +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 { + let analysis = match CronAnalyzer::from_expr(expr.clone()) { + Ok(analysis) => analysis, + Err(e) => format!("{e}") + }; + cron_result.set(analysis) + } + }, + } + 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/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), + }, + } + } +} 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::*;