diff --git a/src/day.rs b/src/day.rs new file mode 100644 index 0000000..5148797 --- /dev/null +++ b/src/day.rs @@ -0,0 +1,172 @@ +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 displays as a two digit number. +/// +/// ``` +/// # use advent_of_code::Day; +/// 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 { + /// Creates a [`Day`] from the provided value if it's in the valid range, + /// returns [`None`] otherwise. + pub fn new(day: u8) -> Option { + if day == 0 || day > 25 { + return None; + } + Some(Self(day)) + } + + // Not part of the public API + #[doc(hidden)] + pub const fn __new_unchecked(day: u8) -> Self { + Self(day) + } + + /// Converts the [`Day`] into an [`u8`]. + pub fn into_inner(self) -> u8 { + self.0 + } +} + +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) + } +} + +/// An error which can be returned when parsing a [`Day`]. +#[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 day number between 1 and 25") + } +} + +/* -------------------------------------------------------------------------- */ + +/// An iterator that yields every day of advent from the 1st to the 25th. +pub fn all_days() -> AllDays { + AllDays::new() +} + +/// An iterator that yields every day of advent from the 1st to the 25th. +pub struct AllDays { + current: u8, +} + +impl AllDays { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { current: 1 } + } +} + +impl Iterator for AllDays { + type Item = Day; + + fn next(&mut self) -> Option { + if self.current > 25 { + return None; + } + // NOTE: the iterator starts at 1 and we have verified that the value is not above 25. + let day = Day(self.current); + self.current += 1; + + Some(day) + } +} + +/* -------------------------------------------------------------------------- */ + +/// Creates a [`Day`] value in a const context. +#[macro_export] +macro_rules! day { + ($day:expr) => {{ + const _ASSERT: () = assert!( + $day != 0 && $day <= 25, + concat!( + "invalid day number `", + $day, + "`, expecting a value between 1 and 25" + ), + ); + $crate::Day::__new_unchecked($day) + }}; +} + +/* -------------------------------------------------------------------------- */ + +#[cfg(feature = "test_lib")] +mod tests { + use super::{all_days, Day}; + + #[test] + fn all_days_iterator() { + let mut iter = all_days(); + + assert_eq!(iter.next(), Some(Day(1))); + assert_eq!(iter.next(), Some(Day(2))); + assert_eq!(iter.next(), Some(Day(3))); + assert_eq!(iter.next(), Some(Day(4))); + assert_eq!(iter.next(), Some(Day(5))); + assert_eq!(iter.next(), Some(Day(6))); + assert_eq!(iter.next(), Some(Day(7))); + assert_eq!(iter.next(), Some(Day(8))); + assert_eq!(iter.next(), Some(Day(9))); + assert_eq!(iter.next(), Some(Day(10))); + assert_eq!(iter.next(), Some(Day(11))); + assert_eq!(iter.next(), Some(Day(12))); + assert_eq!(iter.next(), Some(Day(13))); + assert_eq!(iter.next(), Some(Day(14))); + assert_eq!(iter.next(), Some(Day(15))); + assert_eq!(iter.next(), Some(Day(16))); + assert_eq!(iter.next(), Some(Day(17))); + assert_eq!(iter.next(), Some(Day(18))); + assert_eq!(iter.next(), Some(Day(19))); + assert_eq!(iter.next(), Some(Day(20))); + assert_eq!(iter.next(), Some(Day(21))); + assert_eq!(iter.next(), Some(Day(22))); + assert_eq!(iter.next(), Some(Day(23))); + assert_eq!(iter.next(), Some(Day(24))); + assert_eq!(iter.next(), Some(Day(25))); + assert_eq!(iter.next(), None); + } +} + +/* -------------------------------------------------------------------------- */ 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..7443322 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::{all_days, Day}; pub fn handle(is_release: bool, is_timed: bool) { let mut timings: Vec = vec![]; - (1..=25).for_each(|day| { + all_days().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, @@ -208,6 +204,8 @@ mod child_commands { mod tests { use super::parse_exec_time; + use crate::day; + #[test] fn test_well_formed() { let res = parse_exec_time( @@ -216,7 +214,7 @@ mod child_commands { "Part 2: 10 (74.13ms @ 99999 samples)".into(), "".into(), ], - 1, + day!(1), ); assert_approx_eq!(res.total_nanos, 74130074.13_f64); assert_eq!(res.part_1.unwrap(), "74.13ns"); @@ -231,7 +229,7 @@ mod child_commands { "Part 2: 10s (100ms @ 1 samples)".into(), "".into(), ], - 1, + day!(1), ); assert_approx_eq!(res.total_nanos, 2100000000_f64); assert_eq!(res.part_1.unwrap(), "2s"); @@ -246,7 +244,7 @@ mod child_commands { "Part 2: ✖ ".into(), "".into(), ], - 1, + day!(1), ); assert_approx_eq!(res.total_nanos, 0_f64); assert_eq!(res.part_1.is_none(), true); 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..382283d 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 } @@ -12,7 +14,7 @@ pub fn part_two(input: &str) -> Option { None } -advent_of_code::main!(DAY); +advent_of_code::main!(DAY_NUMBER); #[cfg(test)] mod tests { @@ -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, @@ -55,7 +55,11 @@ pub fn handle(day: u8) { } }; - match file.write_all(MODULE_TEMPLATE.replace("DAY", &day.to_string()).as_bytes()) { + match file.write_all( + MODULE_TEMPLATE + .replace("DAY_NUMBER", &day.into_inner().to_string()) + .as_bytes(), + ) { Ok(()) => { println!("Created module file \"{}\"", &module_path); } @@ -86,8 +90,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/mod.rs b/src/template/mod.rs index faddf0a..215f039 100644 --- a/src/template/mod.rs +++ b/src/template/mod.rs @@ -1,3 +1,4 @@ +use crate::Day; use std::{env, fs}; pub mod aoc_cli; @@ -10,12 +11,10 @@ pub const ANSI_BOLD: &str = "\x1b[1m"; pub const ANSI_RESET: &str = "\x1b[0m"; /// Helper function that reads a text file to a string. -#[must_use] pub fn read_file(folder: &str, day: u8) -> String { +#[must_use] +pub fn read_file(folder: &str, day: Day) -> String { let cwd = env::current_dir().unwrap(); - let filepath = cwd - .join("data") - .join(folder) - .join(format!("{day:02}.txt")); + let filepath = cwd.join("data").join(folder).join(format!("{day}.txt")); let f = fs::read_to_string(filepath); f.expect("could not open input file") } @@ -24,11 +23,14 @@ pub const ANSI_RESET: &str = "\x1b[0m"; #[macro_export] macro_rules! main { ($day:expr) => { + /// The current day. + const DAY: advent_of_code::Day = advent_of_code::day!($day); + fn main() { use advent_of_code::template::runner::*; - let input = advent_of_code::template::read_file("inputs", $day); - run_part(part_one, &input, $day, 1); - run_part(part_two, &input, $day, 2); + let input = advent_of_code::template::read_file("inputs", DAY); + run_part(part_one, &input, DAY, 1); + run_part(part_two, &input, DAY, 2); } }; } diff --git a/src/template/readme_benchmarks.rs b/src/template/readme_benchmarks.rs index 6f11b5a..c564aa4 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 { @@ -71,7 +73,7 @@ fn construct_table(prefix: &str, timings: Vec, total_millis: f64) -> St let path = get_path_for_bin(timing.day); lines.push(format!( "| [Day {}]({}) | `{}` | `{}` |", - timing.day, + timing.day.into_inner(), path, timing.part_1.unwrap_or_else(|| "-".into()), timing.part_2.unwrap_or_else(|| "-".into()) @@ -103,23 +105,24 @@ pub fn update(timings: Vec, total_millis: f64) -> Result<(), Error> { #[cfg(feature = "test_lib")] mod tests { use super::{update_content, Timings, MARKER}; + use crate::day; fn get_mock_timings() -> Vec { vec![ Timings { - day: 1, + day: day!(1), part_1: Some("10ms".into()), part_2: Some("20ms".into()), total_nanos: 3e+10, }, Timings { - day: 2, + day: day!(2), part_1: Some("30ms".into()), part_2: Some("40ms".into()), total_nanos: 7e+10, }, Timings { - day: 4, + day: day!(4), part_1: Some("40ms".into()), part_2: Some("50ms".into()), total_nanos: 9e+10, 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();