utils/framework.rs
1use crate::date::Date;
2use std::fmt::{Debug, Display};
3
4/// Common trait implemented by puzzles to provide the puzzle's [`Date`].
5///
6/// [`year!`](crate::year!) implements this automatically.
7pub trait PuzzleDate {
8 const DATE: Date;
9}
10
11/// Trait implemented by puzzles to provide example inputs and answers.
12///
13/// [`examples!`](crate::examples!) implements this automatically.
14pub trait PuzzleExamples<P1: Debug + Display + 'static, P2: Debug + Display + 'static> {
15 const EXAMPLES: &'static [(&'static str, Option<P1>, Option<P2>)];
16}
17
18/// Macro to generate the crate root for each year crate, implementing common items.
19///
20/// For each day, the module is declared, the struct re-exported and the [`PuzzleDate`] trait
21/// implemented.
22///
23/// A `puzzle!` macro is defined and exported, which takes one or more callback macro paths and a
24/// list of arguments captured as `tt` fragments. The macro expands to calling the first callback
25/// with the remaining callback paths and the provided arguments followed by the year number, crate
26/// name and a list of day numbers and structs. These macros are then chained across all year
27/// crates to implement [`aoc::all_puzzles!`](../aoc/macro.all_puzzles.html).
28///
29/// Running `cargo xtask update` will automatically update the list of days inside macro invocations
30/// in files matching `crates/year????/src/lib.rs`.
31///
32/// # Examples
33///
34/// ```ignore
35/// utils::year!(2015 => year2015, ${
36/// 1 => day01::Day01,
37/// 2 => day02::Day02,
38/// });
39/// ```
40#[macro_export]
41macro_rules! year {
42 ($year:literal => $crate_name:ident, $dollar:tt{$(
43 $day:literal => $day_mod:ident::$day_struct:ident$(<$lifetime:lifetime>)?,
44 )+}) => {
45 $(
46 mod $day_mod;
47 #[doc = concat!("[", $year, " Day ", $day, "](https://adventofcode.com/", $year, "/day/", $day, "):")]
48 pub use $day_mod::$day_struct;
49 impl $crate::PuzzleDate for $day_struct$(<$lifetime>)? {
50 #[doc = concat!($year, " Day ", $day)]
51 const DATE: $crate::date::Date = $crate::date::Date::new(
52 $crate::date::Year::new_const::<$year>(),
53 $crate::date::Day::new_const::<$day>()
54 ).unwrap();
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. The `part1` and `part2` tests are only
97/// generated if an example has an answer for that part.
98///
99/// # Examples
100///
101/// Adding examples to a `Day01` puzzle where `part1` returns [`u32`] and `part2` returns [`u64`].
102/// The first example has correct answers defined for both parts. The second and third examples
103/// are only applicable to `part1` and `part2` of the puzzle respectively.
104///
105/// ```ignore
106/// examples!(Day01 -> (u32, u64) [
107/// {input: "ABCDEF", part1: 30, part2: 342},
108/// {input: "AAAAAA", part1: 21},
109/// {input: "ABC123", part2: 853},
110/// ]);
111/// ```
112///
113/// Example inputs can also be included from the crate's examples directory by using `file` instead
114/// of `input`:
115///
116/// ```ignore
117/// examples!(Day01 -> (u32, u64) [
118/// {input: "Short example", part1: 27},
119/// {file: "day01_example.txt", part2: 483},
120/// ]);
121/// ```
122#[macro_export]
123macro_rules! examples {
124 ($day:ident$(<$lifetime:lifetime>)? -> ($p1:ty, $p2:ty) [$($($tail:tt,)+)?]) => {
125 impl $crate::PuzzleExamples<$p1, $p2> for $day$(<$lifetime>)? {
126 #[allow(clippy::allow_attributes, clippy::unreadable_literal)]
127 const EXAMPLES: &'static [(&'static str, Option<$p1>, Option<$p2>)] = &[$($(
128 $crate::examples!(@item $tail)
129 ),+)?];
130 }
131
132 $(
133 // Gate the invocation, not the generated module, to skip expansion in non-test builds
134 #[cfg(test)]
135 $crate::examples!(@tests $day [] [] $($tail)+);
136 )?
137 };
138
139 // Helper rules to only generate each part's test if an example has an answer for that part,
140 // using the answer's tuple index as the condition for each part
141 (@tests $day:ident [$($idx1:tt)?] [$($idx2:tt)?]
142 {$key:ident: $input:literal, part1: $a:expr, part2: $b:expr $(,)?} $($rest:tt)*) => {
143 $crate::examples!(@tests $day [1] [2]);
144 };
145 (@tests $day:ident [$($idx1:tt)?] [$($idx2:tt)?]
146 {$key:ident: $input:literal, part1: $a:expr $(,)?} $($rest:tt)*) => {
147 $crate::examples!(@tests $day [1] [$($idx2)?] $($rest)*);
148 };
149 (@tests $day:ident [$($idx1:tt)?] [$($idx2:tt)?]
150 {$key:ident: $input:literal, part2: $b:expr $(,)?} $($rest:tt)*) => {
151 $crate::examples!(@tests $day [$($idx1)?] [2] $($rest)*);
152 };
153 (@tests $day:ident [$($idx1:tt)?] [$($idx2:tt)?]) => {
154 mod example_tests {
155 use $crate::{PuzzleExamples, input::InputType};
156 use super::$day;
157
158 #[test]
159 fn new() {
160 for (i, example) in $day::EXAMPLES.iter().enumerate() {
161 let (lf, crlf) = $crate::input::to_lf_crlf(example.0);
162
163 let solution = $day::new(&lf, InputType::Example);
164 assert!(
165 solution.is_ok(),
166 "new failed for example {i}: {:?}",
167 example.0,
168 );
169
170 if let Some(crlf) = crlf {
171 let solution = $day::new(&crlf, InputType::Example);
172 assert!(
173 solution.is_ok(),
174 "new failed for example {i} with CRLF line endings: {:?}",
175 example.0,
176 );
177 }
178 }
179 }
180
181 $(
182 #[test]
183 fn part1() {
184 for (i, example) in $day::EXAMPLES.iter().enumerate() {
185 if let Some(expected) = example.$idx1 {
186 let (lf, crlf) = $crate::input::to_lf_crlf(example.0);
187
188 let solution = $day::new(&lf, InputType::Example).unwrap();
189 assert_eq!(
190 solution.part1(),
191 expected,
192 "part 1 incorrect for example {i}: {:?}",
193 example.0,
194 );
195
196 if let Some(crlf) = crlf {
197 let solution = $day::new(&crlf, InputType::Example).unwrap();
198 assert_eq!(
199 solution.part1(),
200 expected,
201 "part 1 incorrect for example {i} with CRLF line endings: {:?}",
202 example.0,
203 );
204 }
205 }
206 }
207 }
208 )?
209
210 $(
211 #[test]
212 fn part2() {
213 for (i, example) in $day::EXAMPLES.iter().enumerate() {
214 if let Some(expected) = example.$idx2 {
215 let (lf, crlf) = $crate::input::to_lf_crlf(example.0);
216
217 let solution = $day::new(&lf, InputType::Example).unwrap();
218 assert_eq!(
219 solution.part2(),
220 expected,
221 "part 2 incorrect for example {i}: {:?}",
222 example.0,
223 );
224
225 if let Some(crlf) = crlf {
226 let solution = $day::new(&crlf, InputType::Example).unwrap();
227 assert_eq!(
228 solution.part2(),
229 expected,
230 "part 2 incorrect for example {i} with CRLF line endings: {:?}",
231 example.0,
232 );
233 }
234 }
235 }
236 }
237 )?
238 }
239 };
240
241 (@item {input: $str:literal, part1: $p1:expr, part2: $p2:expr $(,)?}) => {
242 ($str, Some($p1), Some($p2))
243 };
244 (@item {input: $str:literal, part1: $p1:expr $(,)?}) => {
245 ($str, Some($p1), None)
246 };
247 (@item {input: $str:literal, part2: $p2:expr $(,)?}) => {
248 ($str, None, Some($p2))
249 };
250 (@item {file: $file:literal, part1: $p1:expr, part2: $p2:expr $(,)?}) => {
251 (
252 $crate::input::strip_final_newline(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/", $file))),
253 Some($p1),
254 Some($p2),
255 )
256 };
257 (@item {file: $file:literal, part1: $p1:expr $(,)?}) => {
258 (
259 $crate::input::strip_final_newline(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/", $file))),
260 Some($p1),
261 None,
262 )
263 };
264 (@item {file: $file:literal, part2: $p2:expr $(,)?}) => {
265 (
266 $crate::input::strip_final_newline(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/", $file))),
267 None,
268 Some($p2),
269 )
270 };
271}