Skip to main content

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            const EXAMPLES: &'static [(&'static str, Option<$p1>, Option<$p2>)] = &[$($(
127                $crate::examples!(@item $tail)
128            ),+)?];
129        }
130
131        $(
132        // Gate the invocation, not the generated module, to skip expansion in non-test builds
133        #[cfg(test)]
134        $crate::examples!(@tests $day [] [] $($tail)+);
135        )?
136    };
137
138    // Helper rules to only generate each part's test if an example has an answer for that part,
139    // using the answer's tuple index as the condition for each part
140    (@tests $day:ident [$($idx1:tt)?] [$($idx2:tt)?]
141        {$key:ident: $input:literal, part1: $a:expr, part2: $b:expr $(,)?} $($rest:tt)*) => {
142        $crate::examples!(@tests $day [1] [2]);
143    };
144    (@tests $day:ident [$($idx1:tt)?] [$($idx2:tt)?]
145        {$key:ident: $input:literal, part1: $a:expr $(,)?} $($rest:tt)*) => {
146        $crate::examples!(@tests $day [1] [$($idx2)?] $($rest)*);
147    };
148    (@tests $day:ident [$($idx1:tt)?] [$($idx2:tt)?]
149        {$key:ident: $input:literal, part2: $b:expr $(,)?} $($rest:tt)*) => {
150        $crate::examples!(@tests $day [$($idx1)?] [2] $($rest)*);
151    };
152    (@tests $day:ident [$($idx1:tt)?] [$($idx2:tt)?]) => {
153        mod example_tests {
154            use $crate::{PuzzleExamples, input::InputType};
155            use super::$day;
156
157            #[test]
158            fn new() {
159                for (i, example) in $day::EXAMPLES.iter().enumerate() {
160                    let (lf, crlf) = $crate::input::to_lf_crlf(example.0);
161
162                    let solution = $day::new(&lf, InputType::Example);
163                    assert!(
164                        solution.is_ok(),
165                        "new failed for example {i}: {:?}",
166                        example.0,
167                    );
168
169                    if let Some(crlf) = crlf {
170                        let solution = $day::new(&crlf, InputType::Example);
171                        assert!(
172                            solution.is_ok(),
173                            "new failed for example {i} with CRLF line endings: {:?}",
174                            example.0,
175                        );
176                    }
177                }
178            }
179
180            $(
181            #[test]
182            fn part1() {
183                for (i, example) in $day::EXAMPLES.iter().enumerate() {
184                    if let Some(expected) = example.$idx1 {
185                        let (lf, crlf) = $crate::input::to_lf_crlf(example.0);
186
187                        let solution = $day::new(&lf, InputType::Example).unwrap();
188                        assert_eq!(
189                            solution.part1(),
190                            expected,
191                            "part 1 incorrect for example {i}: {:?}",
192                            example.0,
193                        );
194
195                        if let Some(crlf) = crlf {
196                            let solution = $day::new(&crlf, InputType::Example).unwrap();
197                            assert_eq!(
198                                solution.part1(),
199                                expected,
200                                "part 1 incorrect for example {i} with CRLF line endings: {:?}",
201                                example.0,
202                            );
203                        }
204                    }
205                }
206            }
207            )?
208
209            $(
210            #[test]
211            fn part2() {
212                for (i, example) in $day::EXAMPLES.iter().enumerate() {
213                    if let Some(expected) = example.$idx2 {
214                        let (lf, crlf) = $crate::input::to_lf_crlf(example.0);
215
216                        let solution = $day::new(&lf, InputType::Example).unwrap();
217                        assert_eq!(
218                            solution.part2(),
219                            expected,
220                            "part 2 incorrect for example {i}: {:?}",
221                            example.0,
222                        );
223
224                        if let Some(crlf) = crlf {
225                            let solution = $day::new(&crlf, InputType::Example).unwrap();
226                            assert_eq!(
227                                solution.part2(),
228                                expected,
229                                "part 2 incorrect for example {i} with CRLF line endings: {:?}",
230                                example.0,
231                            );
232                        }
233                    }
234                }
235            }
236            )?
237        }
238    };
239
240    (@item {input: $str:literal, part1: $p1:expr, part2: $p2:expr $(,)?}) => {
241        ($str, Some($p1), Some($p2))
242    };
243    (@item {input: $str:literal, part1: $p1:expr $(,)?}) => {
244        ($str, Some($p1), None)
245    };
246    (@item {input: $str:literal, part2: $p2:expr $(,)?}) => {
247        ($str, None, Some($p2))
248    };
249    (@item {file: $file:literal, part1: $p1:expr, part2: $p2:expr $(,)?}) => {
250        (
251            $crate::input::strip_final_newline(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/", $file))),
252            Some($p1),
253            Some($p2),
254        )
255    };
256    (@item {file: $file:literal, part1: $p1:expr $(,)?}) => {
257        (
258            $crate::input::strip_final_newline(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/", $file))),
259            Some($p1),
260            None,
261        )
262    };
263    (@item {file: $file:literal, part2: $p2:expr $(,)?}) => {
264        (
265            $crate::input::strip_final_newline(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/", $file))),
266            None,
267            Some($p2),
268        )
269    };
270}