diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3b256c9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "matrix_test_assignment" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +test-case = "2.1.0" diff --git a/input.txt b/input.txt new file mode 100644 index 0000000..f0001cf --- /dev/null +++ b/input.txt @@ -0,0 +1,4 @@ +30 1 /bin/run_me_daily +45 * /bin/run_me_hourly +* * /bin/run_me_every_minute +* 19 /bin/run_me_sixty_times diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..42b8e9c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,174 @@ +use core::fmt; +use std::{env, io::{self, BufRead}, num::ParseIntError, str, cmp::Ordering}; +use test_case::test_case; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct Time { + hour: u8, + minute: u8, +} + +impl str::FromStr for Time { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + let parts: Vec<_> = s.split(':').collect(); + + Ok(Self { + hour: parts[0].parse()?, + minute: parts[1].parse()?, + }) + } +} + +impl fmt::Display for Time { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:02}:{:02}", self.hour, self.minute) + } +} + +enum NumberOrAsterisk { + Asterisk, + Number(u8), +} + +impl str::FromStr for NumberOrAsterisk { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + if s == "*" { + return Ok(Self::Asterisk) + } + + Ok(Self::Number(s.parse()?)) + } +} + +enum Schedule { + EveryMinute, + AtMinute(u8), + AtHour(u8), + AtTime(Time), +} + +struct CronTask { + schedule: Schedule, + command: String, +} + +impl str::FromStr for CronTask { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + let parts: Vec<_> = s.split(char::is_whitespace).filter(|&x| !x.is_empty()).collect(); + let maybe_minute: NumberOrAsterisk = parts[0].parse()?; + let maybe_hour: NumberOrAsterisk = parts[1].parse()?; + let command = parts[2].to_owned(); + + Ok(Self { + schedule: match (maybe_hour, maybe_minute) { + (NumberOrAsterisk::Asterisk, NumberOrAsterisk::Asterisk) => Schedule::EveryMinute, + (NumberOrAsterisk::Number(hour), NumberOrAsterisk::Asterisk) => Schedule::AtHour(hour), + (NumberOrAsterisk::Asterisk, NumberOrAsterisk::Number(minute)) => Schedule::AtMinute(minute), + (NumberOrAsterisk::Number(hour), NumberOrAsterisk::Number(minute)) => Schedule::AtTime(Time { hour, minute }) + }, + command, + }) + } +} + +#[derive(Debug, PartialEq, Eq)] +enum NextRunTime { + Today(Time), + Tomorrow(Time), +} + +impl fmt::Display for NextRunTime { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Today(time) => write!(f, "{} today", time), + Self::Tomorrow(time) => write!(f, "{} tomorrow", time), + } + } +} + +struct NextRun { + time: NextRunTime, + command: String, +} + +impl fmt::Display for NextRun { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} - {}", self.time, self.command) + } +} + +#[test_case(Schedule::EveryMinute, "12:34".parse().unwrap() => NextRunTime::Today( "12:34".parse().unwrap()))] +#[test_case(Schedule::AtHour(11), "12:34".parse().unwrap() => NextRunTime::Tomorrow("11:00".parse().unwrap()))] +#[test_case(Schedule::AtHour(12), "12:34".parse().unwrap() => NextRunTime::Today( "12:34".parse().unwrap()))] +#[test_case(Schedule::AtHour(13), "12:34".parse().unwrap() => NextRunTime::Today( "13:00".parse().unwrap()))] +#[test_case(Schedule::AtMinute(33), "12:34".parse().unwrap() => NextRunTime::Today( "13:33".parse().unwrap()))] +#[test_case(Schedule::AtMinute(34), "12:34".parse().unwrap() => NextRunTime::Today( "12:34".parse().unwrap()))] +#[test_case(Schedule::AtMinute(35), "12:34".parse().unwrap() => NextRunTime::Today( "12:35".parse().unwrap()))] +#[test_case(Schedule::AtMinute(33), "23:34".parse().unwrap() => NextRunTime::Tomorrow("00:33".parse().unwrap()))] +#[test_case(Schedule::AtMinute(34), "23:34".parse().unwrap() => NextRunTime::Today( "23:34".parse().unwrap()))] +#[test_case(Schedule::AtMinute(35), "23:34".parse().unwrap() => NextRunTime::Today( "23:35".parse().unwrap()))] +#[test_case(Schedule::AtTime("11:33".parse().unwrap()), "12:34".parse().unwrap() => NextRunTime::Tomorrow("11:33".parse().unwrap()))] +#[test_case(Schedule::AtTime("11:34".parse().unwrap()), "12:34".parse().unwrap() => NextRunTime::Tomorrow("11:34".parse().unwrap()))] +#[test_case(Schedule::AtTime("11:35".parse().unwrap()), "12:34".parse().unwrap() => NextRunTime::Tomorrow("11:35".parse().unwrap()))] +#[test_case(Schedule::AtTime("12:33".parse().unwrap()), "12:34".parse().unwrap() => NextRunTime::Tomorrow("12:33".parse().unwrap()))] +#[test_case(Schedule::AtTime("12:34".parse().unwrap()), "12:34".parse().unwrap() => NextRunTime::Today( "12:34".parse().unwrap()))] +#[test_case(Schedule::AtTime("12:35".parse().unwrap()), "12:34".parse().unwrap() => NextRunTime::Today( "12:35".parse().unwrap()))] +#[test_case(Schedule::AtTime("13:33".parse().unwrap()), "12:34".parse().unwrap() => NextRunTime::Today( "13:33".parse().unwrap()))] +#[test_case(Schedule::AtTime("13:34".parse().unwrap()), "12:34".parse().unwrap() => NextRunTime::Today( "13:34".parse().unwrap()))] +#[test_case(Schedule::AtTime("13:35".parse().unwrap()), "12:34".parse().unwrap() => NextRunTime::Today( "13:35".parse().unwrap()))] +fn get_next_run_time(schedule: Schedule, current_time: Time) -> NextRunTime { + match schedule { + Schedule::EveryMinute => NextRunTime::Today(current_time), + Schedule::AtHour(hour) => match hour.cmp(¤t_time.hour) { + Ordering::Less => NextRunTime::Tomorrow(Time { hour, minute: 0 }), + Ordering::Equal => NextRunTime::Today(current_time), + Ordering::Greater => NextRunTime::Today(Time { hour, minute: 0 }), + }, + Schedule::AtMinute(minute) => match minute.cmp(¤t_time.minute) { + Ordering::Less => match current_time.hour { + 23 => NextRunTime::Tomorrow(Time { hour: 0, minute }), + _ => NextRunTime::Today(Time { hour: current_time.hour + 1, minute }), + }, + Ordering::Equal => NextRunTime::Today(current_time), + Ordering::Greater => NextRunTime::Today(Time { hour: current_time.hour, minute } ) + }, + Schedule::AtTime(time) => match time.cmp(¤t_time) { + Ordering::Less => NextRunTime::Tomorrow(time), + _ => NextRunTime::Today(time), + } + } +} + +fn get_next_run(task: CronTask, current_time: Time) -> NextRun { + NextRun { + command: task.command, + time: get_next_run_time(task.schedule, current_time), + } +} + +#[test_case("30 1 /bin/run_me_daily", "16:10".parse().unwrap() => "01:30 tomorrow - /bin/run_me_daily")] +#[test_case("45 * /bin/run_me_hourly", "16:10".parse().unwrap() => "16:45 today - /bin/run_me_hourly")] +#[test_case("* * /bin/run_me_every_minute", "16:10".parse().unwrap() => "16:10 today - /bin/run_me_every_minute")] +#[test_case("* 19 /bin/run_me_sixty_times", "16:10".parse().unwrap() => "19:00 today - /bin/run_me_sixty_times")] +#[test_case("0 0 all_zeroes", "16:10".parse().unwrap() => "00:00 tomorrow - all_zeroes")] +#[test_case("1 1 leading_zeroes", "16:10".parse().unwrap() => "01:01 tomorrow - leading_zeroes")] +#[test_case("12 12 no_zeroes", "16:10".parse().unwrap() => "12:12 tomorrow - no_zeroes")] +fn handle_line(line: &str, current_time: Time) -> String { + return format!("{}", get_next_run(line.parse().unwrap(), current_time)) +} + +fn main() { + let args: Vec<_> = env::args().collect(); + let time = args[1].parse().unwrap(); + let stdin = io::stdin(); + let lines = stdin.lock().lines(); + for line in lines { + println!("{}", handle_line(&line.unwrap(), time)) + } +}