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}