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}