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            #[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}