Skip to content

Add a Day type to provide better handling of day value #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Nov 22, 2023
172 changes: 172 additions & 0 deletions src/day.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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<u8> for Day {
fn eq(&self, other: &u8) -> bool {
self.0.eq(other)
}
}

impl PartialOrd<u8> for Day {
fn partial_cmp(&self, other: &u8) -> Option<std::cmp::Ordering> {
self.0.partial_cmp(other)
}
}

/* -------------------------------------------------------------------------- */

impl FromStr for Day {
type Err = DayFromStrError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
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<Self::Item> {
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);
}
}

/* -------------------------------------------------------------------------- */
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
mod day;
pub mod template;

pub use day::*;
10 changes: 6 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>,
Expand Down
20 changes: 10 additions & 10 deletions src/template/aoc_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use std::{
process::{Command, Output, Stdio},
};

use crate::Day;

#[derive(Debug)]
pub enum AocCommandError {
CommandNotFound,
Expand Down Expand Up @@ -33,7 +35,7 @@ pub fn check() -> Result<(), AocCommandError> {
Ok(())
}

pub fn read(day: u8) -> Result<Output, AocCommandError> {
pub fn read(day: Day) -> Result<Output, AocCommandError> {
let puzzle_path = get_puzzle_path(day);

let args = build_args(
Expand All @@ -49,7 +51,7 @@ pub fn read(day: u8) -> Result<Output, AocCommandError> {
call_aoc_cli(&args)
}

pub fn download(day: u8) -> Result<Output, AocCommandError> {
pub fn download(day: Day) -> Result<Output, AocCommandError> {
let input_path = get_input_path(day);
let puzzle_path = get_puzzle_path(day);

Expand All @@ -72,22 +74,20 @@ pub fn download(day: u8) -> Result<Output, AocCommandError> {
Ok(output)
}

pub fn submit(day: u8, part: u8, result: &str) -> Result<Output, AocCommandError> {
pub fn submit(day: Day, part: u8, result: &str) -> Result<Output, AocCommandError> {
// workaround: the argument order is inverted for submit.
let mut args = build_args("submit", &[], day);
args.push(part.to_string());
args.push(result.to_string());
call_aoc_cli(&args)
}

fn get_input_path(day: u8) -> 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<u16> {
Expand All @@ -97,7 +97,7 @@ fn get_year() -> Option<u16> {
}
}

fn build_args(command: &str, args: &[String], day: u8) -> Vec<String> {
fn build_args(command: &str, args: &[String], day: Day) -> Vec<String> {
let mut cmd_args = args.to_vec();

if let Some(year) = get_year() {
Expand Down
28 changes: 13 additions & 15 deletions src/template/commands/all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Timings> = vec![];

(1..=25).for_each(|day| {
all_days().for_each(|day| {
if day > 1 {
println!();
}
Expand Down Expand Up @@ -56,15 +57,15 @@ impl From<std::io::Error> 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,
Expand All @@ -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<Vec<String>, Error> {
let day_padded = format!("{day:02}");

pub fn run_solution(day: Day, is_timed: bool, is_release: bool) -> Result<Vec<String>, 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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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");
Expand All @@ -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");
Expand All @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/template/commands/download.rs
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/template/commands/read.rs
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
Loading