Skip to content

Commit 6d9bf34

Browse files
authored
feat: use checked Day type instead of integers for days (#35)
1 parent 6653e85 commit 6d9bf34

File tree

12 files changed

+248
-64
lines changed

12 files changed

+248
-64
lines changed

src/day.rs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
use std::error::Error;
2+
use std::fmt::Display;
3+
use std::str::FromStr;
4+
5+
/// A valid day number of advent (i.e. an integer in range 1 to 25).
6+
///
7+
/// # Display
8+
/// This value displays as a two digit number.
9+
///
10+
/// ```
11+
/// # use advent_of_code::Day;
12+
/// let day = Day::new(8).unwrap();
13+
/// assert_eq!(day.to_string(), "08")
14+
/// ```
15+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16+
pub struct Day(u8);
17+
18+
impl Day {
19+
/// Creates a [`Day`] from the provided value if it's in the valid range,
20+
/// returns [`None`] otherwise.
21+
pub fn new(day: u8) -> Option<Self> {
22+
if day == 0 || day > 25 {
23+
return None;
24+
}
25+
Some(Self(day))
26+
}
27+
28+
// Not part of the public API
29+
#[doc(hidden)]
30+
pub const fn __new_unchecked(day: u8) -> Self {
31+
Self(day)
32+
}
33+
34+
/// Converts the [`Day`] into an [`u8`].
35+
pub fn into_inner(self) -> u8 {
36+
self.0
37+
}
38+
}
39+
40+
impl Display for Day {
41+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42+
write!(f, "{:02}", self.0)
43+
}
44+
}
45+
46+
impl PartialEq<u8> for Day {
47+
fn eq(&self, other: &u8) -> bool {
48+
self.0.eq(other)
49+
}
50+
}
51+
52+
impl PartialOrd<u8> for Day {
53+
fn partial_cmp(&self, other: &u8) -> Option<std::cmp::Ordering> {
54+
self.0.partial_cmp(other)
55+
}
56+
}
57+
58+
/* -------------------------------------------------------------------------- */
59+
60+
impl FromStr for Day {
61+
type Err = DayFromStrError;
62+
63+
fn from_str(s: &str) -> Result<Self, Self::Err> {
64+
let day = s.parse().map_err(|_| DayFromStrError)?;
65+
Self::new(day).ok_or(DayFromStrError)
66+
}
67+
}
68+
69+
/// An error which can be returned when parsing a [`Day`].
70+
#[derive(Debug)]
71+
pub struct DayFromStrError;
72+
73+
impl Error for DayFromStrError {}
74+
75+
impl Display for DayFromStrError {
76+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77+
f.write_str("expecting a day number between 1 and 25")
78+
}
79+
}
80+
81+
/* -------------------------------------------------------------------------- */
82+
83+
/// An iterator that yields every day of advent from the 1st to the 25th.
84+
pub fn all_days() -> AllDays {
85+
AllDays::new()
86+
}
87+
88+
/// An iterator that yields every day of advent from the 1st to the 25th.
89+
pub struct AllDays {
90+
current: u8,
91+
}
92+
93+
impl AllDays {
94+
#[allow(clippy::new_without_default)]
95+
pub fn new() -> Self {
96+
Self { current: 1 }
97+
}
98+
}
99+
100+
impl Iterator for AllDays {
101+
type Item = Day;
102+
103+
fn next(&mut self) -> Option<Self::Item> {
104+
if self.current > 25 {
105+
return None;
106+
}
107+
// NOTE: the iterator starts at 1 and we have verified that the value is not above 25.
108+
let day = Day(self.current);
109+
self.current += 1;
110+
111+
Some(day)
112+
}
113+
}
114+
115+
/* -------------------------------------------------------------------------- */
116+
117+
/// Creates a [`Day`] value in a const context.
118+
#[macro_export]
119+
macro_rules! day {
120+
($day:expr) => {{
121+
const _ASSERT: () = assert!(
122+
$day != 0 && $day <= 25,
123+
concat!(
124+
"invalid day number `",
125+
$day,
126+
"`, expecting a value between 1 and 25"
127+
),
128+
);
129+
$crate::Day::__new_unchecked($day)
130+
}};
131+
}
132+
133+
/* -------------------------------------------------------------------------- */
134+
135+
#[cfg(feature = "test_lib")]
136+
mod tests {
137+
use super::{all_days, Day};
138+
139+
#[test]
140+
fn all_days_iterator() {
141+
let mut iter = all_days();
142+
143+
assert_eq!(iter.next(), Some(Day(1)));
144+
assert_eq!(iter.next(), Some(Day(2)));
145+
assert_eq!(iter.next(), Some(Day(3)));
146+
assert_eq!(iter.next(), Some(Day(4)));
147+
assert_eq!(iter.next(), Some(Day(5)));
148+
assert_eq!(iter.next(), Some(Day(6)));
149+
assert_eq!(iter.next(), Some(Day(7)));
150+
assert_eq!(iter.next(), Some(Day(8)));
151+
assert_eq!(iter.next(), Some(Day(9)));
152+
assert_eq!(iter.next(), Some(Day(10)));
153+
assert_eq!(iter.next(), Some(Day(11)));
154+
assert_eq!(iter.next(), Some(Day(12)));
155+
assert_eq!(iter.next(), Some(Day(13)));
156+
assert_eq!(iter.next(), Some(Day(14)));
157+
assert_eq!(iter.next(), Some(Day(15)));
158+
assert_eq!(iter.next(), Some(Day(16)));
159+
assert_eq!(iter.next(), Some(Day(17)));
160+
assert_eq!(iter.next(), Some(Day(18)));
161+
assert_eq!(iter.next(), Some(Day(19)));
162+
assert_eq!(iter.next(), Some(Day(20)));
163+
assert_eq!(iter.next(), Some(Day(21)));
164+
assert_eq!(iter.next(), Some(Day(22)));
165+
assert_eq!(iter.next(), Some(Day(23)));
166+
assert_eq!(iter.next(), Some(Day(24)));
167+
assert_eq!(iter.next(), Some(Day(25)));
168+
assert_eq!(iter.next(), None);
169+
}
170+
}
171+
172+
/* -------------------------------------------------------------------------- */

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1+
mod day;
12
pub mod template;
3+
4+
pub use day::*;

src/main.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@ use args::{parse, AppArguments};
44
mod args {
55
use std::process;
66

7+
use advent_of_code::Day;
8+
79
pub enum AppArguments {
810
Download {
9-
day: u8,
11+
day: Day,
1012
},
1113
Read {
12-
day: u8,
14+
day: Day,
1315
},
1416
Scaffold {
15-
day: u8,
17+
day: Day,
1618
},
1719
Solve {
18-
day: u8,
20+
day: Day,
1921
release: bool,
2022
time: bool,
2123
submit: Option<u8>,

src/template/aoc_cli.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use std::{
44
process::{Command, Output, Stdio},
55
};
66

7+
use crate::Day;
8+
79
#[derive(Debug)]
810
pub enum AocCommandError {
911
CommandNotFound,
@@ -33,7 +35,7 @@ pub fn check() -> Result<(), AocCommandError> {
3335
Ok(())
3436
}
3537

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

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

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

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

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

83-
fn get_input_path(day: u8) -> String {
84-
let day_padded = format!("{day:02}");
85-
format!("data/inputs/{day_padded}.txt")
85+
fn get_input_path(day: Day) -> String {
86+
format!("data/inputs/{day}.txt")
8687
}
8788

88-
fn get_puzzle_path(day: u8) -> String {
89-
let day_padded = format!("{day:02}");
90-
format!("data/puzzles/{day_padded}.md")
89+
fn get_puzzle_path(day: Day) -> String {
90+
format!("data/puzzles/{day}.md")
9191
}
9292

9393
fn get_year() -> Option<u16> {
@@ -97,7 +97,7 @@ fn get_year() -> Option<u16> {
9797
}
9898
}
9999

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

103103
if let Some(year) = get_year() {

src/template/commands/all.rs

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ use crate::template::{
44
readme_benchmarks::{self, Timings},
55
ANSI_BOLD, ANSI_ITALIC, ANSI_RESET,
66
};
7+
use crate::{all_days, Day};
78

89
pub fn handle(is_release: bool, is_timed: bool) {
910
let mut timings: Vec<Timings> = vec![];
1011

11-
(1..=25).for_each(|day| {
12+
all_days().for_each(|day| {
1213
if day > 1 {
1314
println!();
1415
}
@@ -56,15 +57,15 @@ impl From<std::io::Error> for Error {
5657
}
5758

5859
#[must_use]
59-
pub fn get_path_for_bin(day: usize) -> String {
60-
let day_padded = format!("{day:02}");
61-
format!("./src/bin/{day_padded}.rs")
60+
pub fn get_path_for_bin(day: Day) -> String {
61+
format!("./src/bin/{day}.rs")
6262
}
6363

6464
/// All solutions live in isolated binaries.
6565
/// This module encapsulates interaction with these binaries, both invoking them as well as parsing the timing output.
6666
mod child_commands {
6767
use super::{get_path_for_bin, Error};
68+
use crate::Day;
6869
use std::{
6970
io::{BufRead, BufReader},
7071
path::Path,
@@ -73,18 +74,13 @@ mod child_commands {
7374
};
7475

7576
/// Run the solution bin for a given day
76-
pub fn run_solution(
77-
day: usize,
78-
is_timed: bool,
79-
is_release: bool,
80-
) -> Result<Vec<String>, Error> {
81-
let day_padded = format!("{day:02}");
82-
77+
pub fn run_solution(day: Day, is_timed: bool, is_release: bool) -> Result<Vec<String>, Error> {
8378
// skip command invocation for days that have not been scaffolded yet.
8479
if !Path::new(&get_path_for_bin(day)).exists() {
8580
return Ok(vec![]);
8681
}
8782

83+
let day_padded = day.to_string();
8884
let mut args = vec!["run", "--quiet", "--bin", &day_padded];
8985

9086
if is_release {
@@ -129,7 +125,7 @@ mod child_commands {
129125
Ok(output)
130126
}
131127

132-
pub fn parse_exec_time(output: &[String], day: usize) -> super::Timings {
128+
pub fn parse_exec_time(output: &[String], day: Day) -> super::Timings {
133129
let mut timings = super::Timings {
134130
day,
135131
part_1: None,
@@ -208,6 +204,8 @@ mod child_commands {
208204
mod tests {
209205
use super::parse_exec_time;
210206

207+
use crate::day;
208+
211209
#[test]
212210
fn test_well_formed() {
213211
let res = parse_exec_time(
@@ -216,7 +214,7 @@ mod child_commands {
216214
"Part 2: 10 (74.13ms @ 99999 samples)".into(),
217215
"".into(),
218216
],
219-
1,
217+
day!(1),
220218
);
221219
assert_approx_eq!(res.total_nanos, 74130074.13_f64);
222220
assert_eq!(res.part_1.unwrap(), "74.13ns");
@@ -231,7 +229,7 @@ mod child_commands {
231229
"Part 2: 10s (100ms @ 1 samples)".into(),
232230
"".into(),
233231
],
234-
1,
232+
day!(1),
235233
);
236234
assert_approx_eq!(res.total_nanos, 2100000000_f64);
237235
assert_eq!(res.part_1.unwrap(), "2s");
@@ -246,7 +244,7 @@ mod child_commands {
246244
"Part 2: ✖ ".into(),
247245
"".into(),
248246
],
249-
1,
247+
day!(1),
250248
);
251249
assert_approx_eq!(res.total_nanos, 0_f64);
252250
assert_eq!(res.part_1.is_none(), true);

src/template/commands/download.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use crate::template::aoc_cli;
2+
use crate::Day;
23
use std::process;
34

4-
pub fn handle(day: u8) {
5+
pub fn handle(day: Day) {
56
if aoc_cli::check().is_err() {
67
eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it.");
78
process::exit(1);

src/template/commands/read.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use std::process;
22

33
use crate::template::aoc_cli;
4+
use crate::Day;
45

5-
pub fn handle(day: u8) {
6+
pub fn handle(day: Day) {
67
if aoc_cli::check().is_err() {
78
eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it.");
89
process::exit(1);

0 commit comments

Comments
 (0)