utils/framework.rs
1use crate::date::{Day, Year};
2use std::fmt::{Debug, Display};
3
4/// Common trait implemented by puzzles to provide [`Year`] and [`Day`].
5///
6/// [`year!`](crate::year!) implements this automatically.
7pub trait Puzzle {
8 const YEAR: Year;
9 const DAY: Day;
10}
11
12/// Trait implemented by puzzles to provide example inputs and answers.
13///
14/// [`examples!`](crate::examples!) implements this automatically.
15pub trait PuzzleExamples<P1: Debug + Display + 'static, P2: Debug + Display + 'static> {
16 const EXAMPLES: &'static [(&'static str, Option<P1>, Option<P2>)];
17}
18
19/// Macro to generate the crate root for each year crate, implementing common items.
20///
21/// For each day, the module is declared, the struct re-exported and the [`Puzzle`] trait
22/// implemented.
23///
24/// A `puzzle!` macro is defined and exported, which takes one or more callback macro paths and a
25/// list of arguments captured as `tt` fragments. The macro expands to calling the first callback
26/// with the remaining callback paths and the provided arguments followed by the year number, crate
27/// name and a list of day numbers and structs. These macros are then chained across all year
28/// crates to implement [`aoc::all_puzzles!`](../aoc/macro.all_puzzles.html).
29///
30/// Running `cargo xtask update` will automatically update the list of days inside macro invocations
31/// in files matching `crates/year????/src/lib.rs`.
32///
33/// # Examples
34///
35/// ```ignore
36/// utils::year!(2015 => year2015, ${
37/// 1 => day01::Day01,
38/// 2 => day02::Day02,
39/// });
40/// ```
41#[macro_export]
42macro_rules! year {
43 ($year:literal => $crate_name:ident, $dollar:tt{$(
44 $day:literal => $day_mod:ident::$day_struct:ident$(<$lifetime:lifetime>)?,
45 )+}) => {
46 $(
47 mod $day_mod;
48 #[doc = concat!("[", $year, " Day ", $day, "](https://adventofcode.com/", $year, "/day/", $day, "):")]
49 pub use $day_mod::$day_struct;
50 impl $crate::Puzzle for $day_struct$(<$lifetime>)? {
51 #[doc = concat!("Year ", $year)]
52 const YEAR: $crate::date::Year = $crate::date::Year::new_const::<$year>();
53 #[doc = concat!("Day ", $day)]
54 const DAY: $crate::date::Day = $crate::date::Day::new_const::<$day>();
55 }
56 )+
57
58 /// Macro which supplies a list of implemented puzzle solutions in this crate.
59 ///
60 /// Automatically generated by [utils::year!]. Refer to its documentation for more details.
61 #[macro_export]
62 macro_rules! puzzles {
63 (
64 [$dollar callback:path $dollar(,$dollar($dollar callbacks:path),+)?]
65 $dollar ($dollar args:tt)*
66 ) => {
67 $dollar callback!{
68 $dollar([$dollar($dollar callbacks),+])?
69 $dollar($dollar args)*
70 $year => $crate_name{$(
71 $day => $day_struct,
72 )+}
73 }
74 }
75 }
76 };
77}
78
79/// Version of the `puzzles!` macro generated by [`year!`] which appends no extra arguments.
80#[macro_export]
81macro_rules! puzzles_noop {
82 ([$callback:path $(,$($callbacks:path),+)?] $($args:tt)*) => {
83 $callback!{
84 $([$($callbacks),+])?
85 $($args)*
86 }
87 };
88}
89
90/// Macro to generate a list of examples, implement [`PuzzleExamples`] and add example tests.
91///
92/// The provided types for `part1` and `part2` don't have to match the types returned by the day's
93/// functions, but they must be comparable with [`PartialEq`]. For functions returning [`String`]
94/// `&'static str` should be used.
95///
96/// If no examples are provided, tests aren't generated.
97///
98/// # Examples
99///
100/// Adding examples to a `Day01` puzzle where `part1` returns [`u32`] and `part2` returns [`u64`].
101/// The first example has correct answers defined for both parts. The second and third examples
102/// are only applicable to `part1` and `part2` of the puzzle respectively.
103///
104/// ```ignore
105/// examples!(Day01 -> (u32, u64) [
106/// {input: "ABCDEF", part1: 30, part2: 342},
107/// {input: "AAAAAA", part1: 21},
108/// {input: "ABC123", part2: 853},
109/// ]);
110/// ```
111///
112/// Example inputs can also be included from the crate's examples directory by using `file` instead
113/// of `input`:
114///
115/// ```ignore
116/// examples!(Day01 -> (u32, u64) [
117/// {input: "Short example", part1: 27},
118/// {file: "day01_example.txt", part2: 483},
119/// ]);
120/// ```
121#[macro_export]
122macro_rules! examples {
123 ($day:ident$(<$lifetime:lifetime>)? -> ($p1:ty, $p2:ty) [$($($tail:tt,)+)?]) => {
124 impl $crate::PuzzleExamples<$p1, $p2> for $day$(<$lifetime>)? {
125 const EXAMPLES: &'static [(&'static str, Option<$p1>, Option<$p2>)] = &[$($(
126 $crate::examples!(@item $tail)
127 ),+)?];
128 }
129
130 $(
131 #[cfg(test)]
132 mod example_tests {
133 use $crate::{PuzzleExamples, input::InputType};
134 use super::$day;
135 $crate::examples!(@ignore $($tail)+);
136
137 #[test]
138 fn new() {
139 for (i, example) in $day::EXAMPLES.iter().enumerate() {
140 let solution = $day::new(example.0, InputType::Example);
141 assert!(
142 solution.is_ok(),
143 "new failed for example {i}: {:?}",
144 example.0,
145 );
146 }
147 }
148
149 #[test]
150 fn part1() {
151 for (i, example) in $day::EXAMPLES.iter().enumerate() {
152 if let Some(expected) = example.1 {
153 let solution = $day::new(example.0, InputType::Example).unwrap();
154 assert_eq!(
155 solution.part1(),
156 expected,
157 "part 1 incorrect for example {i}: {:?}",
158 example.0,
159 );
160 }
161 }
162 }
163
164 #[test]
165 fn part2() {
166 for (i, example) in $day::EXAMPLES.iter().enumerate() {
167 if let Some(expected) = example.2 {
168 let solution = $day::new(example.0, InputType::Example).unwrap();
169 assert_eq!(
170 solution.part2(),
171 expected,
172 "part 2 incorrect for example {i}: {:?}",
173 example.0,
174 );
175 }
176 }
177 }
178 }
179 )?
180 };
181
182 (@item {input: $str:literal, part1: $p1:literal, part2: $p2:expr $(,)?}) => {
183 ($str, Some($p1), Some($p2))
184 };
185 (@item {input: $str:literal, part1: $p1:literal $(,)?}) => {
186 ($str, Some($p1), None)
187 };
188 (@item {input: $str:literal, part2: $p2:expr $(,)?}) => {
189 ($str, None, Some($p2))
190 };
191 (@item {file: $file:literal, part1: $p1:literal, part2: $p2:expr $(,)?}) => {
192 (
193 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/", $file)).trim_ascii_end(),
194 Some($p1),
195 Some($p2),
196 )
197 };
198 (@item {file: $file:literal, part1: $p1:literal $(,)?}) => {
199 (
200 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/", $file)).trim_ascii_end(),
201 Some($p1),
202 None,
203 )
204 };
205 (@item {file: $file:literal, part2: $p2:expr $(,)?}) => {
206 (
207 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/", $file)).trim_ascii_end(),
208 None,
209 Some($p2),
210 )
211 };
212 (@ignore $($tail:tt)*) => {};
213}