diff --git a/src/day.rs b/src/day.rs new file mode 100644 index 0000000..4122bbd --- /dev/null +++ b/src/day.rs @@ -0,0 +1,100 @@ +use std::error::Error; +use std::fmt::Display; +use std::str::FromStr; + +/* -------------------------------------------------------------------------- */ + +/// A valid day number of advent (i.e. an integer in range 1 to 25). +/// +/// # Display +/// This value is display as a two digit number. +/// +/// ``` +/// let day = Day::new(8).unwrap(); +/// assert_eq!(day.to_string(), "08") +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Day(u8); + +impl Day { + pub fn new(day: u8) -> Option { + if day == 0 || day > 25 { + return None; + } + Some(Self(day)) + } + + fn new_unchecked(day: u8) -> Self { + debug_assert!(day != 0 && day <= 25); + Self(day) + } +} + +impl Display for Day { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:02}", self.0) + } +} + +impl PartialEq for Day { + fn eq(&self, other: &u8) -> bool { + self.0.eq(other) + } +} + +impl PartialOrd for Day { + fn partial_cmp(&self, other: &u8) -> Option { + self.0.partial_cmp(other) + } +} + +/* -------------------------------------------------------------------------- */ + +impl FromStr for Day { + type Err = DayFromStrError; + + fn from_str(s: &str) -> Result { + let day = s.parse().map_err(|_| DayFromStrError)?; + Self::new(day).ok_or(DayFromStrError) + } +} + +#[derive(Debug)] +pub struct DayFromStrError; + +impl Error for DayFromStrError {} + +impl Display for DayFromStrError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("expecting a number between 1 and 25") + } +} + +/* -------------------------------------------------------------------------- */ + +/// An iterator that yield every day of advent from the 1st to the 25th. +pub fn every_day() -> EveryDay { + EveryDay { current: 1 } +} + +/// An iterator that yield every day of advent from the 1st to the 25th. +pub struct EveryDay { + current: u8, +} + +impl Iterator for EveryDay { + type Item = Day; + + fn next(&mut self) -> Option { + if self.current > 25 { + return None; + } + // NOTE: the iterator start at 1 and we have verified that the value is not above 25. + let day = Day::new_unchecked(self.current); + self.current += 1; + + Some(day) + } +} + +/* -------------------------------------------------------------------------- */ diff --git a/src/lib.rs b/src/lib.rs index 612b5b9..04f64f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,4 @@ +mod day; pub mod template; + +pub use day::*; diff --git a/src/main.rs b/src/main.rs index 91a042c..23ba03c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,18 +4,20 @@ use args::{parse, AppArguments}; mod args { use std::process; + use advent_of_code::Day; + pub enum AppArguments { Download { - day: u8, + day: Day, }, Read { - day: u8, + day: Day, }, Scaffold { - day: u8, + day: Day, }, Solve { - day: u8, + day: Day, release: bool, time: bool, submit: Option, diff --git a/src/template/aoc_cli.rs b/src/template/aoc_cli.rs index 08ce9fe..e7aab8b 100644 --- a/src/template/aoc_cli.rs +++ b/src/template/aoc_cli.rs @@ -4,6 +4,8 @@ use std::{ process::{Command, Output, Stdio}, }; +use crate::Day; + #[derive(Debug)] pub enum AocCommandError { CommandNotFound, @@ -33,7 +35,7 @@ pub fn check() -> Result<(), AocCommandError> { Ok(()) } -pub fn read(day: u8) -> Result { +pub fn read(day: Day) -> Result { let puzzle_path = get_puzzle_path(day); let args = build_args( @@ -49,7 +51,7 @@ pub fn read(day: u8) -> Result { call_aoc_cli(&args) } -pub fn download(day: u8) -> Result { +pub fn download(day: Day) -> Result { let input_path = get_input_path(day); let puzzle_path = get_puzzle_path(day); @@ -72,7 +74,7 @@ pub fn download(day: u8) -> Result { Ok(output) } -pub fn submit(day: u8, part: u8, result: &str) -> Result { +pub fn submit(day: Day, part: u8, result: &str) -> Result { // workaround: the argument order is inverted for submit. let mut args = build_args("submit", &[], day); args.push(part.to_string()); @@ -80,14 +82,12 @@ pub fn submit(day: u8, part: u8, result: &str) -> Result String { - let day_padded = format!("{day:02}"); - format!("data/inputs/{day_padded}.txt") +fn get_input_path(day: Day) -> String { + format!("data/inputs/{day}.txt") } -fn get_puzzle_path(day: u8) -> String { - let day_padded = format!("{day:02}"); - format!("data/puzzles/{day_padded}.md") +fn get_puzzle_path(day: Day) -> String { + format!("data/puzzles/{day}.md") } fn get_year() -> Option { @@ -97,7 +97,7 @@ fn get_year() -> Option { } } -fn build_args(command: &str, args: &[String], day: u8) -> Vec { +fn build_args(command: &str, args: &[String], day: Day) -> Vec { let mut cmd_args = args.to_vec(); if let Some(year) = get_year() { diff --git a/src/template/commands/all.rs b/src/template/commands/all.rs index 7214b4a..449b62f 100644 --- a/src/template/commands/all.rs +++ b/src/template/commands/all.rs @@ -4,11 +4,12 @@ use crate::template::{ readme_benchmarks::{self, Timings}, ANSI_BOLD, ANSI_ITALIC, ANSI_RESET, }; +use crate::{every_day, Day}; pub fn handle(is_release: bool, is_timed: bool) { let mut timings: Vec = vec![]; - (1..=25).for_each(|day| { + every_day().for_each(|day| { if day > 1 { println!(); } @@ -56,15 +57,15 @@ impl From for Error { } #[must_use] -pub fn get_path_for_bin(day: usize) -> String { - let day_padded = format!("{day:02}"); - format!("./src/bin/{day_padded}.rs") +pub fn get_path_for_bin(day: Day) -> String { + format!("./src/bin/{day}.rs") } /// All solutions live in isolated binaries. /// This module encapsulates interaction with these binaries, both invoking them as well as parsing the timing output. mod child_commands { use super::{get_path_for_bin, Error}; + use crate::Day; use std::{ io::{BufRead, BufReader}, path::Path, @@ -73,18 +74,13 @@ mod child_commands { }; /// Run the solution bin for a given day - pub fn run_solution( - day: usize, - is_timed: bool, - is_release: bool, - ) -> Result, Error> { - let day_padded = format!("{day:02}"); - + pub fn run_solution(day: Day, is_timed: bool, is_release: bool) -> Result, Error> { // skip command invocation for days that have not been scaffolded yet. if !Path::new(&get_path_for_bin(day)).exists() { return Ok(vec![]); } + let day_padded = day.to_string(); let mut args = vec!["run", "--quiet", "--bin", &day_padded]; if is_release { @@ -129,7 +125,7 @@ mod child_commands { Ok(output) } - pub fn parse_exec_time(output: &[String], day: usize) -> super::Timings { + pub fn parse_exec_time(output: &[String], day: Day) -> super::Timings { let mut timings = super::Timings { day, part_1: None, diff --git a/src/template/commands/download.rs b/src/template/commands/download.rs index 56beead..76ad635 100644 --- a/src/template/commands/download.rs +++ b/src/template/commands/download.rs @@ -1,7 +1,8 @@ use crate::template::aoc_cli; +use crate::Day; use std::process; -pub fn handle(day: u8) { +pub fn handle(day: Day) { if aoc_cli::check().is_err() { eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it."); process::exit(1); diff --git a/src/template/commands/read.rs b/src/template/commands/read.rs index 65edcde..01316f8 100644 --- a/src/template/commands/read.rs +++ b/src/template/commands/read.rs @@ -1,8 +1,9 @@ use std::process; use crate::template::aoc_cli; +use crate::Day; -pub fn handle(day: u8) { +pub fn handle(day: Day) { if aoc_cli::check().is_err() { eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it."); process::exit(1); diff --git a/src/template/commands/scaffold.rs b/src/template/commands/scaffold.rs index 6a3d9a1..d4df4b9 100644 --- a/src/template/commands/scaffold.rs +++ b/src/template/commands/scaffold.rs @@ -4,6 +4,8 @@ use std::{ process, }; +use crate::Day; + const MODULE_TEMPLATE: &str = r#"pub fn part_one(input: &str) -> Option { None } @@ -40,12 +42,10 @@ fn create_file(path: &str) -> Result { OpenOptions::new().write(true).create(true).open(path) } -pub fn handle(day: u8) { - let day_padded = format!("{day:02}"); - - let input_path = format!("data/inputs/{day_padded}.txt"); - let example_path = format!("data/examples/{day_padded}.txt"); - let module_path = format!("src/bin/{day_padded}.rs"); +pub fn handle(day: Day) { + let input_path = format!("data/inputs/{day}.txt"); + let example_path = format!("data/examples/{day}.txt"); + let module_path = format!("src/bin/{day}.rs"); let mut file = match safe_create_file(&module_path) { Ok(file) => file, @@ -86,8 +86,5 @@ pub fn handle(day: u8) { } println!("---"); - println!( - "🎄 Type `cargo solve {}` to run your solution.", - &day_padded - ); + println!("🎄 Type `cargo solve {}` to run your solution.", day); } diff --git a/src/template/commands/solve.rs b/src/template/commands/solve.rs index 8c65702..50b7000 100644 --- a/src/template/commands/solve.rs +++ b/src/template/commands/solve.rs @@ -1,9 +1,9 @@ use std::process::{Command, Stdio}; -pub fn handle(day: u8, release: bool, time: bool, submit_part: Option) { - let day_padded = format!("{day:02}"); +use crate::Day; - let mut cmd_args = vec!["run".to_string(), "--bin".to_string(), day_padded]; +pub fn handle(day: Day, release: bool, time: bool, submit_part: Option) { + let mut cmd_args = vec!["run".to_string(), "--bin".to_string(), day.to_string()]; if release { cmd_args.push("--release".to_string()); diff --git a/src/template/readme_benchmarks.rs b/src/template/readme_benchmarks.rs index 6f11b5a..8d32477 100644 --- a/src/template/readme_benchmarks.rs +++ b/src/template/readme_benchmarks.rs @@ -2,6 +2,8 @@ /// The approach taken is similar to how `aoc-readme-stars` handles this. use std::{fs, io}; +use crate::Day; + static MARKER: &str = ""; #[derive(Debug)] @@ -18,7 +20,7 @@ impl From for Error { #[derive(Clone)] pub struct Timings { - pub day: usize, + pub day: Day, pub part_1: Option, pub part_2: Option, pub total_nanos: f64, @@ -29,9 +31,9 @@ pub struct TablePosition { pos_end: usize, } -#[must_use] pub fn get_path_for_bin(day: usize) -> String { - let day_padded = format!("{day:02}"); - format!("./src/bin/{day_padded}.rs") +#[must_use] +pub fn get_path_for_bin(day: Day) -> String { + format!("./src/bin/{day}.rs") } fn locate_table(readme: &str) -> Result { diff --git a/src/template/runner.rs b/src/template/runner.rs index 6557ee5..5e6a9b3 100644 --- a/src/template/runner.rs +++ b/src/template/runner.rs @@ -1,5 +1,6 @@ /// Encapsulates code that interacts with solution functions. use crate::template::{aoc_cli, ANSI_ITALIC, ANSI_RESET}; +use crate::Day; use std::fmt::Display; use std::io::{stdout, Write}; use std::process::Output; @@ -8,7 +9,7 @@ use std::{cmp, env, process}; use super::ANSI_BOLD; -pub fn run_part(func: impl Fn(I) -> Option, input: I, day: u8, part: u8) { +pub fn run_part(func: impl Fn(I) -> Option, input: I, day: Day, part: u8) { let part_str = format!("Part {part}"); let (result, duration, samples) = @@ -131,7 +132,7 @@ fn print_result(result: &Option, part: &str, duration_str: &str) /// 2. aoc-cli is installed. fn submit_result( result: T, - day: u8, + day: Day, part: u8, ) -> Option> { let args: Vec = env::args().collect();