aoc_helper/
lib.rs

1//! # AOC Helper documentation
2//!
3//! AOC Helper is a crate to make solving and sharing aoc problems in rust
4//! easier. It is inspired by [cargo-aoc](https://github.com/gobanos/cargo-aoc).
5//! `cargo-aoc` is a binary that will also take care of compilation, while this
6//! crate is a library you can import and use in your code. This aims to make it
7//! easier to share and debug other people's code, and to perhaps be easier to
8//! set up.
9//!
10//! ## Usage
11//!
12//! To get started, add `aoc-helper` to the `dependencies` section in your
13//! `Cargo.toml`:
14//!
15//! ~~~~
16//! [dependencies]
17//! aoc-helper = "0.2.1"
18//! ~~~~
19//!
20//! You also need to provide a session ID for `aoc-helper` to be able to
21//! download your input files. The session ID is stored in a cookie called
22//! `session` in your browser on the [aoc website](https://adventofcode.com) if
23//! you're logged in. You can provide the session ID through an
24//! environment variable with the name `AOC_SESSION_ID`, through the
25//! `session_id` functions on `Helper`, or by using an `aoc_helper.toml` file.
26//!
27//! If you're using an `aoc_helper.toml` file, you need to specify the `config-file` feature and
28//! specify your session ID in `aoc_helper.toml` like this:
29//!
30//! ~~~~
31//! session-id = "82942d3671962a9f17f8f81723d45b0fcdf8b3b5bf6w3954f02101fa5de1420b6ecd30ed550133f32d6a5c00233076af"
32//! ~~~~
33//!
34//! Then, create an instance of [`AocDay`](./struct.AocDay.html). Look at its
35//! documentation for information. The output is by default colored. You can
36//! disable this by disabling the default features.
37
38use std::fmt::Display;
39use std::fs::{File, OpenOptions, create_dir_all};
40use std::io::Read;
41use std::env;
42use std::error::Error;
43
44use time::{Date, Instant};
45use std::io::Write;
46#[cfg(feature = "colored-output")]
47use colored::*;
48#[cfg(feature = "config-file")]
49use toml::Value;
50
51#[derive(Debug, Copy, Clone)]
52pub enum AocError {
53    MissingSessionId,
54    SpecifiedDateInFuture,
55    NoPuzzleOnDate,
56}
57
58impl Display for AocError {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        let msg = match self {
61            AocError::MissingSessionId => "No session ID specified",
62            AocError::SpecifiedDateInFuture => "The specified puzzle date is in the future",
63            AocError::NoPuzzleOnDate => "There was no puzzle on the specified date",
64        };
65        write!(f, "Error: {}", msg)
66    }
67}
68
69impl Error for AocError {}
70
71fn aoc_err(err: AocError) -> Result<(), Box<dyn Error>> {
72    Err(Box::new(err))
73}
74
75macro_rules! print_info {
76    ($aoc_day:ident, $puzzle:expr) => {
77        #[cfg(feature = "colored-output")]
78        print!("[{} {}, {} {}, {} {}]: ",
79                "AoC".yellow(), $aoc_day.year,
80                "day".bright_cyan(), $aoc_day.day,
81                "part".bright_cyan(), $puzzle.part);
82        #[cfg(not(feature = "colored-output"))]
83        print!("[AoC {}, day {}, part {}]: ", $aoc_day.year, $aoc_day.day, $puzzle.part);
84        std::io::stdout().flush().unwrap();
85    }
86}
87
88macro_rules! run_solver {
89    ($puzzle:expr, $input:expr) => {
90        let start_time = Instant::now();
91        let output = ($puzzle.solver)($input);
92        let elapsed = start_time.elapsed();
93        println!("{}", output);
94
95        let time_taken = {
96            let mut msg_str = String::new();
97            let (d, h, m, s, ms, us, ns) = (
98                elapsed.whole_days(),
99                elapsed.whole_hours() % 24,
100                elapsed.whole_minutes() % 60,
101                elapsed.whole_seconds() % 60,
102                elapsed.whole_milliseconds() % 1000,
103                elapsed.whole_microseconds() % 1000,
104                elapsed.whole_nanoseconds() % 1000,
105            );
106            if d > 0 {
107                msg_str.push_str(&format!("{}d ", d));
108            }
109            if h > 0 {
110                msg_str.push_str(&format!("{}h ", h));
111            }
112            if m > 0 {
113                msg_str.push_str(&format!("{}m ", m));
114            }
115            if s > 0 {
116                msg_str.push_str(&format!("{}s ", s));
117            }
118            if ms > 0 {
119                msg_str.push_str(&format!("{}ms ", ms));
120            }
121            if us > 0 {
122                msg_str.push_str(&format!("{}μs ", us));
123            }
124            if ns > 0 {
125                msg_str.push_str(&format!("{}ns ", ns));
126            }
127            msg_str
128        };
129        #[cfg(feature = "colored-output")]
130        println!("{} {}", "Finished in".bright_green(), time_taken.bright_white());
131        #[cfg(not(feature = "colored-output"))]
132        println!("Finished in {}", time_taken);
133    }
134}
135
136/// The `AocDay` struct stores information for an aoc day.
137///
138/// You can provide an optional serializer function to serialize the input data
139/// into a custom type. The serializer, and the solver functions if you're not
140/// using a custom serializer function, take `String`s as input.
141pub struct AocDay<T> {
142    year: i32,
143    day: u8,
144    session_id: Option<String>,
145    input_path: String,
146    serializer: fn(String) -> T,
147}
148
149/// The `Puzzle` struct stores information for an aoc puzzle. Two puzzles
150/// release each aoc day.
151pub struct Puzzle<T, D> {
152    part: u8,
153    examples: Vec<String>,
154    solver: fn(T) -> D,
155}
156
157impl AocDay<String> {
158    /// Create a new `AocDay` instance for the provided year and day.
159    ///
160    /// # Example
161    ///
162    /// ~~~~
163    /// use aoc_helper::AocDay;
164    ///
165    /// // Create a new AocDay instance for day 12 of aoc 2015
166    /// let day_12 = AocDay::new(2015, 12);
167    /// ~~~~
168    pub fn new(year: i32, day: u8) -> Self {
169        AocDay {
170            year,
171            day,
172            session_id: env::var("AOC_SESSION_ID").ok(),
173            input_path: format!("inputs/{}/day{}.txt", year, day),
174            serializer: |x| x,
175        }
176    }
177}
178
179impl<T> AocDay<T> {
180    /// Create a new `AocDay` instance for the provided year and day, with a
181    /// custom serializer function.
182    ///
183    /// # Example
184    ///
185    /// ~~~~
186    /// use aoc_helper::AocDay;
187    ///
188    /// let day_2 = AocDay::new_with_serializer(2017, 2, |input| input.split_whitespace().collect::<Vec<_>>());
189    /// ~~~~
190    pub fn new_with_serializer(year: i32, day: u8, serializer: fn(String) -> T) -> Self {
191        AocDay {
192            year,
193            day,
194            session_id: env::var("AOC_SESSION_ID").ok(),
195            input_path: format!("inputs/{}/day{}.txt", year, day),
196            serializer,
197        }
198    }
199
200    /// Provide a custom input file
201    ///
202    /// # Example
203    ///
204    /// ~~~~
205    /// use aoc_helper::AocDay;
206    ///
207    /// let mut day_2 = AocDay::new(2017, 2);
208    /// day_2.input("path/to/my/input/file.txt");
209    /// ~~~~
210    pub fn input(&mut self, input_path: &str) {
211        self.input_path = input_path.to_string();
212    }
213
214    /// Chainable version of [`AocDay::input()`](./struct.AocDay.html#method.input)
215    ///
216    /// # Example
217    ///
218    /// ~~~~
219    /// use aoc_helper::AocDay;
220    ///
221    /// let day_2 = AocDay::new(2017, 2)
222    ///     .with_input("path/to/my/input/file.txt");
223    /// ~~~~
224    pub fn with_input(mut self, input_path: &str) -> Self {
225        self.input(input_path);
226        self
227    }
228
229    /// Provide the session ID
230    ///
231    /// # Example
232    ///
233    /// ~~~~
234    /// use aoc_helper::AocDay;
235    ///
236    /// let mut day_8 = AocDay::new(2015, 8);
237    /// day_8.session_id("82942d3671962a9f17f8f81723d45b0fcdf8b3b5bf6w3954f02101fa5de1420b6ecd30ed550133f32d6a5c00233076af");
238    /// ~~~~
239    pub fn session_id(&mut self, session_id: &str) {
240        self.session_id = Some(session_id.to_string());
241    }
242
243    /// Chainable version of [`AocDay::session_id()`](./struct.AocDay.html#method.session_id)
244    ///
245    /// # Example
246    ///
247    /// ~~~~
248    /// use aoc_helper::AocDay;
249    ///
250    /// let date_8 = AocDay::new(2015, 8)
251    ///     .with_session_id("82942d3671962a9f17f8f81723d45b0fcdf8b3b5bf6w3954f02101fa5de1420b6ecd30ed550133f32d6a5c00233076af");
252    /// ~~~~
253    pub fn with_session_id(mut self, session_id: &str) -> Self {
254        self.session_id(session_id);
255        self
256    }
257
258    /// Run a solver function on some example inputs. The function and the
259    /// inputs should be provided using a
260    /// [`Puzzle`](./struct.Puzzle.html)
261    /// instance.
262    ///
263    /// # Example
264    ///
265    /// ~~~~
266    /// use aoc_helper::{AocDay, Puzzle};
267    ///
268    /// let day_5 = AocDay::new(2019, 5);
269    /// day_5.test(
270    ///     &Puzzle::new(1, |x: String| x.chars().filter(|&y| y == 'z').count())
271    ///         .with_examples(&["test", "cases"])
272    /// );
273    /// ~~~~
274    pub fn test(&self, puzzle: &Puzzle<T, impl Display>) {
275        println!();
276        print_info!(self, puzzle);
277        println!();
278        for (i, example) in puzzle.examples.iter().enumerate() {
279            #[cfg(feature = "colored-output")]
280            print!("{} {}: ", "Example".bright_blue(), i + 1);
281            #[cfg(not(feature = "colored-output"))]
282            print!("Example {}: ", i + 1);
283            std::io::stdout().flush().unwrap();
284            run_solver!(puzzle, (self.serializer)(example.to_string()));
285        }
286    }
287
288    /// Run a solver function on the day's input. The function should be
289    /// provided using a
290    /// [`Puzzle`](./struct.Puzzle.html)
291    /// instance.
292    ///
293    /// # Example
294    ///
295    /// ~~~~
296    /// use aoc_helper::{AocDay, Puzzle};
297    ///
298    /// let mut day_5 = AocDay::new(2019, 5);
299    /// let part_1 = Puzzle::new(
300    ///         1,
301    ///         |x: String| x.chars().filter(|&y| y == 'z').count()
302    ///     )
303    ///     .with_examples(&["foo", "bar", "baz"]);
304    /// let part_2 = Puzzle::new(
305    ///         2,
306    ///         |x: String| x.chars().filter(|&y| y != 'z').count()
307    ///     )
308    ///     .with_examples(&["fubar", "bazz", "fubaz"]);
309    /// day_5.test(&part_1);
310    /// day_5.test(&part_2);
311    /// day_5.run(&part_1);
312    /// day_5.run(&part_2);
313    /// ~~~~
314    pub fn run(&mut self, puzzle: &Puzzle<T, impl Display>) -> Result<(), Box<dyn Error>> {
315        #[cfg(feature = "config-file")]
316        {
317            if self.session_id == None {
318                // Try to get session ID from config file
319                if let Ok(mut config_file) = File::open("aoc_helper.toml") {
320                    let mut contents = String::new();
321                    config_file.read_to_string(&mut contents).unwrap();
322                    let value: Value = contents.parse().unwrap();
323                    if let Some(session_id) = value.get("session-id") {
324                        self.session_id = Some(session_id.as_str().unwrap().to_owned());
325                    }
326                }
327            }
328        }
329        if self.session_id == None {
330            return aoc_err(AocError::MissingSessionId);
331        }
332
333        let running_date = Date::try_from_ymd(self.year, 12, self.day).unwrap();
334        // Due to timezone differences, this will be a little more lenient than the aoc website in accepting dates
335        let today = Date::today();
336        let max_year = if today.month() < 12 { today.year() - 1 } else { today.year() };
337        if running_date > Date::try_from_ymd(max_year, 12, 25).unwrap() {
338            return aoc_err(AocError::SpecifiedDateInFuture);
339        } else if self.day > 25 || running_date < Date::try_from_ymd(2015, 12, 1).unwrap() {
340            return aoc_err(AocError::NoPuzzleOnDate);
341        }
342
343        let mut input_file = match OpenOptions::new()
344            .read(true)
345            .write(true)
346            .create(true)
347            .open(&self.input_path) {
348            Ok(file) => file,
349            Err(e) => {
350                if e.kind() == std::io::ErrorKind::NotFound {
351                    create_dir_all(&self.input_path.split('/').take(2).collect::<Vec<_>>().join("/"))?;
352                    OpenOptions::new()
353                        .read(true)
354                        .write(true)
355                        .create(true)
356                        .open(&self.input_path)?
357                } else {
358                    return Err(Box::new(e));
359                }
360            },
361        };
362
363        let mut contents = String::new();
364        println!("{}", contents);
365        input_file.read_to_string(&mut contents)?;
366        if contents.len() == 0 {
367            // Get the input from the website
368            let response = ureq::get(&format!("https://adventofcode.com/{}/day/{}/input", self.year, self.day))
369                .set("Cookie", &(String::from("session=") + self.session_id.as_ref().unwrap())).call();
370            std::io::copy(&mut response.into_reader(), &mut input_file)?;
371            let mut input_file = File::open(&self.input_path)?;
372            input_file.read_to_string(&mut contents)?;
373        }
374
375        print_info!(self, puzzle);
376        run_solver!(puzzle, (self.serializer)(contents.trim().to_string()));
377
378        Ok(())
379    }
380}
381
382impl<T, D: Display> Puzzle<T, D> {
383    /// Create a new `Puzzle` instance for the provided part number and solver
384    /// function.
385    ///
386    /// # Example
387    ///
388    /// ~~~~
389    /// use aoc_helper::Puzzle;
390    ///
391    /// let part_1 = Puzzle::new(1, |x| x.lines().count());
392    /// ~~~~
393    pub fn new(part: u8, solver: fn(T) -> D) -> Self {
394        Puzzle {
395            part,
396            examples: Vec::new(),
397            solver,
398        }
399    }
400
401    /// Provide some example inputs for a `Puzzle` instance that the solver
402    /// function will be given when
403    /// [`AocDay::test()`](./struct.AocDay.html#method.test)
404    /// is called.
405    ///
406    /// # Example
407    ///
408    /// ~~~~
409    /// use aoc_helper::Puzzle;
410    ///
411    /// let mut part_2 = Puzzle::new(2, |x| x.lines().nth(0).unwrap());
412    /// part_2.examples(&["example\ninput", "foo\nbar"]);
413    /// ~~~~
414    pub fn examples<S: ToString>(&mut self, examples: &[S]) {
415        self.examples = examples.iter().map(|example| example.to_string()).collect();
416    }
417
418    /// Chainable version of [`Puzzle::examples()`](./struct.Puzzle.html#method.examples)
419    ///
420    /// # Example
421    ///
422    /// ~~~~
423    /// use aoc_helper::Puzzle;
424    ///
425    /// let part_2 = Puzzle::new(2, |x| lines().nth(0).unwrap())
426    ///     .with_examples(&["example\ninput", "foo\nbar"]);
427    /// ~~~~
428    pub fn with_examples<S: ToString>(mut self, examples: &[S]) -> Self {
429        self.examples(examples);
430        self
431    }
432}