use std::fmt::Display;
use std::fs::{File, OpenOptions, create_dir_all};
use std::io::Read;
use std::env;
use std::error::Error;
use time::{Date, Instant};
use std::io::Write;
#[cfg(feature = "colored-output")]
use colored::*;
#[cfg(feature = "config-file")]
use toml::Value;
#[derive(Debug, Copy, Clone)]
pub enum AocError {
MissingSessionId,
SpecifiedDateInFuture,
NoPuzzleOnDate,
}
impl Display for AocError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = match self {
AocError::MissingSessionId => "No session ID specified",
AocError::SpecifiedDateInFuture => "The specified puzzle date is in the future",
AocError::NoPuzzleOnDate => "There was no puzzle on the specified date",
};
write!(f, "Error: {}", msg)
}
}
impl Error for AocError {}
fn aoc_err(err: AocError) -> Result<(), Box<dyn Error>> {
Err(Box::new(err))
}
macro_rules! print_info {
($aoc_day:ident, $puzzle:expr) => {
#[cfg(feature = "colored-output")]
print!("[{} {}, {} {}, {} {}]: ",
"AoC".yellow(), $aoc_day.year,
"day".bright_cyan(), $aoc_day.day,
"part".bright_cyan(), $puzzle.part);
#[cfg(not(feature = "colored-output"))]
print!("[AoC {}, day {}, part {}]: ", $aoc_day.year, $aoc_day.day, $puzzle.part);
std::io::stdout().flush().unwrap();
}
}
macro_rules! run_solver {
($puzzle:expr, $input:expr) => {
let start_time = Instant::now();
let output = ($puzzle.solver)($input);
let elapsed = start_time.elapsed();
println!("{}", output);
let time_taken = {
let mut msg_str = String::new();
let (d, h, m, s, ms, us, ns) = (
elapsed.whole_days(),
elapsed.whole_hours() % 24,
elapsed.whole_minutes() % 60,
elapsed.whole_seconds() % 60,
elapsed.whole_milliseconds() % 1000,
elapsed.whole_microseconds() % 1000,
elapsed.whole_nanoseconds() % 1000,
);
if d > 0 {
msg_str.push_str(&format!("{}d ", d));
}
if h > 0 {
msg_str.push_str(&format!("{}h ", h));
}
if m > 0 {
msg_str.push_str(&format!("{}m ", m));
}
if s > 0 {
msg_str.push_str(&format!("{}s ", s));
}
if ms > 0 {
msg_str.push_str(&format!("{}ms ", ms));
}
if us > 0 {
msg_str.push_str(&format!("{}μs ", us));
}
if ns > 0 {
msg_str.push_str(&format!("{}ns ", ns));
}
msg_str
};
#[cfg(feature = "colored-output")]
println!("{} {}", "Finished in".bright_green(), time_taken.bright_white());
#[cfg(not(feature = "colored-output"))]
println!("Finished in {}", time_taken);
}
}
pub struct AocDay<T> {
year: i32,
day: u8,
session_id: Option<String>,
input_path: String,
serializer: fn(String) -> T,
}
pub struct Puzzle<T, D> {
part: u8,
examples: Vec<String>,
solver: fn(T) -> D,
}
impl AocDay<String> {
pub fn new(year: i32, day: u8) -> Self {
AocDay {
year,
day,
session_id: env::var("AOC_SESSION_ID").ok(),
input_path: format!("inputs/{}/day{}.txt", year, day),
serializer: |x| x,
}
}
}
impl<T> AocDay<T> {
pub fn new_with_serializer(year: i32, day: u8, serializer: fn(String) -> T) -> Self {
AocDay {
year,
day,
session_id: env::var("AOC_SESSION_ID").ok(),
input_path: format!("inputs/{}/day{}.txt", year, day),
serializer,
}
}
pub fn input(&mut self, input_path: &str) {
self.input_path = input_path.to_string();
}
pub fn with_input(mut self, input_path: &str) -> Self {
self.input(input_path);
self
}
pub fn session_id(&mut self, session_id: &str) {
self.session_id = Some(session_id.to_string());
}
pub fn with_session_id(mut self, session_id: &str) -> Self {
self.session_id(session_id);
self
}
pub fn test(&self, puzzle: &Puzzle<T, impl Display>) {
println!();
print_info!(self, puzzle);
println!();
for (i, example) in puzzle.examples.iter().enumerate() {
#[cfg(feature = "colored-output")]
print!("{} {}: ", "Example".bright_blue(), i + 1);
#[cfg(not(feature = "colored-output"))]
print!("Example {}: ", i + 1);
std::io::stdout().flush().unwrap();
run_solver!(puzzle, (self.serializer)(example.to_string()));
}
}
pub fn run(&mut self, puzzle: &Puzzle<T, impl Display>) -> Result<(), Box<dyn Error>> {
#[cfg(feature = "config-file")]
{
if self.session_id == None {
if let Ok(mut config_file) = File::open("aoc_helper.toml") {
let mut contents = String::new();
config_file.read_to_string(&mut contents).unwrap();
let value: Value = contents.parse().unwrap();
if let Some(session_id) = value.get("session-id") {
self.session_id = Some(session_id.as_str().unwrap().to_owned());
}
}
}
}
if self.session_id == None {
return aoc_err(AocError::MissingSessionId);
}
let running_date = Date::try_from_ymd(self.year, 12, self.day).unwrap();
let today = Date::today();
let max_year = if today.month() < 12 { today.year() - 1 } else { today.year() };
if running_date > Date::try_from_ymd(max_year, 12, 25).unwrap() {
return aoc_err(AocError::SpecifiedDateInFuture);
} else if self.day > 25 || running_date < Date::try_from_ymd(2015, 12, 1).unwrap() {
return aoc_err(AocError::NoPuzzleOnDate);
}
let mut input_file = match OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&self.input_path) {
Ok(file) => file,
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
create_dir_all(&self.input_path.split('/').take(2).collect::<Vec<_>>().join("/"))?;
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&self.input_path)?
} else {
return Err(Box::new(e));
}
},
};
let mut contents = String::new();
println!("{}", contents);
input_file.read_to_string(&mut contents)?;
if contents.len() == 0 {
let response = ureq::get(&format!("https://adventofcode.com/{}/day/{}/input", self.year, self.day))
.set("Cookie", &(String::from("session=") + self.session_id.as_ref().unwrap())).call();
std::io::copy(&mut response.into_reader(), &mut input_file)?;
let mut input_file = File::open(&self.input_path)?;
input_file.read_to_string(&mut contents)?;
}
print_info!(self, puzzle);
run_solver!(puzzle, (self.serializer)(contents.trim().to_string()));
Ok(())
}
}
impl<T, D: Display> Puzzle<T, D> {
pub fn new(part: u8, solver: fn(T) -> D) -> Self {
Puzzle {
part,
examples: Vec::new(),
solver,
}
}
pub fn examples<S: ToString>(&mut self, examples: &[S]) {
self.examples = examples.iter().map(|example| example.to_string()).collect();
}
pub fn with_examples<S: ToString>(mut self, examples: &[S]) -> Self {
self.examples(examples);
self
}
}