diff --git a/.cargo/config.toml b/.cargo/config.toml index cf0d80d..b2bd4bd 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,6 +3,7 @@ today = "run --quiet --release --features today -- today" scaffold = "run --quiet --release -- scaffold" download = "run --quiet --release -- download" read = "run --quiet --release -- read" +switch-year = "run --quiet --release -- switch-year" solve = "run --quiet --release -- solve" all = "run --quiet --release -- all" diff --git a/Cargo.lock b/Cargo.lock index 9504be6..0ea832c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,7 @@ version = "0.11.0" dependencies = [ "chrono", "dhat", + "fs_extra", "pico-args", "tinyjson", ] @@ -126,6 +127,12 @@ dependencies = [ "thousands", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "gimli" version = "0.28.1" diff --git a/Cargo.toml b/Cargo.toml index 038a1a3..d822c7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,15 +16,16 @@ debug = 1 [features] dhat-heap = ["dhat"] -today = ["chrono"] +today = [] test_lib = [] [dependencies] # Template dependencies -chrono = { version = "0.4.38", optional = true } +chrono = "0.4.38" dhat = { version = "0.3.3", optional = true } pico-args = "0.5.0" tinyjson = "2.5.1" +fs_extra = "1.3.0" # Solution dependencies diff --git a/src/main.rs b/src/main.rs index 2a360fc..5c18686 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ -use advent_of_code::template::commands::{all, download, read, scaffold, solve, time}; +use advent_of_code::template::commands::{all, download, read, scaffold, solve, switchyear, time}; +use advent_of_code::template::{ANSI_BOLD, ANSI_RESET}; use args::{parse, AppArguments}; #[cfg(feature = "today")] @@ -7,7 +8,7 @@ use advent_of_code::template::Day; use std::process; mod args { - use advent_of_code::template::Day; + use advent_of_code::template::{Day, Year}; use std::process; pub enum AppArguments { @@ -38,6 +39,9 @@ mod args { }, #[cfg(feature = "today")] Today, + SwitchYear { + year: Year, + }, } pub fn parse() -> Result> { @@ -76,6 +80,9 @@ mod args { }, #[cfg(feature = "today")] Some("today") => AppArguments::Today, + Some("switch-year") => AppArguments::SwitchYear { + year: args.free_from_str()?, + }, Some(x) => { eprintln!("Unknown command: {x}"); process::exit(1); @@ -96,6 +103,10 @@ mod args { } fn main() { + println!( + "🎄{ANSI_BOLD} Advent of Code {} {ANSI_RESET}🎄", + std::env::var("AOC_YEAR").unwrap() + ); match parse() { Err(err) => { eprintln!("Error: {err}"); @@ -126,6 +137,7 @@ fn main() { AppArguments::Today => { match Day::today() { Some(day) => { + switchyear::handle_today(); scaffold::handle(day, false); download::handle(day); read::handle(day) @@ -139,6 +151,9 @@ fn main() { } }; } + AppArguments::SwitchYear { year } => { + switchyear::handle(year); + } }, }; } diff --git a/src/template/commands/mod.rs b/src/template/commands/mod.rs index 36be280..7b632f2 100644 --- a/src/template/commands/mod.rs +++ b/src/template/commands/mod.rs @@ -3,4 +3,5 @@ pub mod download; pub mod read; pub mod scaffold; pub mod solve; +pub mod switchyear; pub mod time; diff --git a/src/template/commands/switchyear.rs b/src/template/commands/switchyear.rs new file mode 100644 index 0000000..5bdd9a8 --- /dev/null +++ b/src/template/commands/switchyear.rs @@ -0,0 +1,89 @@ +use crate::template::year::Year; +use std::{collections::HashSet, env, fs, path::PathBuf}; + +extern crate fs_extra; + +pub fn handle(year: Year) { + let env_year = Year::__new_unchecked(env::var("AOC_YEAR").unwrap().parse().unwrap()); + if year == env_year { + println!("🔔 You are already in the year you want to switch to."); + } else { + switch_to_year(year, env_year); + println!("🎄 Switched to year {}.", year.into_inner()); + } +} + +#[cfg(feature = "today")] +pub fn handle_today() { + let year = Year::this_year().unwrap(); + let env_year = Year::new(env::var("AOC_YEAR").unwrap().parse().unwrap()).unwrap(); + if year != env_year { + switch_to_year(year, env_year); + println!( + "🎄 Automatically switched to this year: {}.", + year.into_inner() + ); + } +} + +fn clean_folder(path: PathBuf) { + let paths = fs::read_dir(path).unwrap(); + let mut files = HashSet::new(); + for path in paths { + let path = path.unwrap().path(); + if path.is_file() && path.file_name().unwrap() != ".keep" { + files.insert(path); + } + } + for file in files { + fs::remove_file(file).unwrap(); + } +} + +pub fn switch_to_year(year: Year, previous_year: Year) { + let cwd = env::current_dir().unwrap(); + + // Move src and data files to years/ + let src = cwd.join("src"); + let data = cwd.join("data"); + let bin = src.join("bin"); + let examples = data.join("examples"); + let inputs = data.join("inputs"); + let puzzles = data.join("puzzles"); + let years = cwd.join("years"); + let destination = years.join(previous_year.into_inner().to_string()); + + let default_copy = fs_extra::dir::CopyOptions::new(); + fs_extra::dir::create(&destination, true).unwrap(); + fs_extra::dir::move_dir(&bin, &destination, &default_copy).unwrap(); + fs_extra::dir::move_dir(&examples, &destination, &default_copy).unwrap(); + clean_folder(inputs); + clean_folder(puzzles); + + // Move years/ to src and data files + let source = years.join(year.into_inner().to_string()); + if source.exists() { + let source_bin = source.join("bin"); + let source_examples = source.join("examples"); + fs_extra::dir::move_dir(&source_bin, &src, &default_copy).unwrap(); + fs_extra::dir::move_dir(&source_examples, &data, &default_copy).unwrap(); + fs_extra::dir::remove(&source).unwrap(); + } else { + fs::create_dir(&bin).unwrap(); + fs::create_dir(&examples).unwrap(); + fs::write(bin.join(".keep"), "").unwrap(); + fs::write(examples.join(".keep"), "").unwrap(); + } + + // Set the environment variable + std::env::set_var("AOC_YEAR", year.into_inner().to_string()); + + // Write Cargo.toml + let config_toml = cwd.join(".cargo").join("config.toml"); + let config_toml_content = fs::read_to_string(&config_toml).unwrap(); + let config_toml_updated_content = config_toml_content.replace( + &previous_year.into_inner().to_string(), + &year.into_inner().to_string(), + ); + fs::write(config_toml, config_toml_updated_content).unwrap(); +} diff --git a/src/template/mod.rs b/src/template/mod.rs index dd8e4c0..85e3637 100644 --- a/src/template/mod.rs +++ b/src/template/mod.rs @@ -5,11 +5,13 @@ pub mod commands; pub mod runner; pub use day::*; +pub use year::*; mod day; mod readme_benchmarks; mod run_multi; mod timings; +mod year; pub const ANSI_ITALIC: &str = "\x1b[3m"; pub const ANSI_BOLD: &str = "\x1b[1m"; diff --git a/src/template/year.rs b/src/template/year.rs new file mode 100644 index 0000000..f98b1b8 --- /dev/null +++ b/src/template/year.rs @@ -0,0 +1,99 @@ +use std::error::Error; +use std::fmt::Display; +use std::str::FromStr; + +extern crate chrono; +use chrono::{Datelike, FixedOffset, Utc}; + +const SERVER_UTC_OFFSET: i32 = -5; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Year(i32); + +impl Year { + /// Creates a [`Year`] from the provided value if it's in the valid range, + /// returns [`None`] otherwise. + pub fn new(year: i32) -> Option { + if 2015 <= year && year <= Year::last_year().into_inner() { + Some(Self(year)) + } else { + None + } + } + + // Not part of the public API + #[doc(hidden)] + pub const fn __new_unchecked(year: i32) -> Self { + Self(year) + } + + /// Converts the [`year`] into an [`i32`]. + pub fn into_inner(self) -> i32 { + self.0 + } + + pub fn last_year() -> Self { + let offset = FixedOffset::east_opt(SERVER_UTC_OFFSET * 3600).unwrap(); + let today = Utc::now().with_timezone(&offset); + if today.month() == 12 { + Self::__new_unchecked(today.year()) + } else { + // December is not here yet, so last AoC was last year + Self::__new_unchecked(today.year() - 1) + } + } + + /// Returns the current year. + pub fn this_year() -> Option { + let offset = FixedOffset::east_opt(SERVER_UTC_OFFSET * 3600)?; + let today = Utc::now().with_timezone(&offset); + Self::new(today.year()) + } +} + +impl Display for Year { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:04}", self.0) + } +} + +impl PartialEq for Year { + fn eq(&self, other: &i32) -> bool { + self.0.eq(other) + } +} + +impl PartialOrd for Year { + fn partial_cmp(&self, other: &i32) -> Option { + self.0.partial_cmp(other) + } +} + +/* -------------------------------------------------------------------------- */ + +impl FromStr for Year { + type Err = YearFromStrError; + + fn from_str(s: &str) -> Result { + let year = s.parse().map_err(|_| YearFromStrError)?; + Self::new(year).ok_or(YearFromStrError) + } +} + +/// An error which can be returned when parsing a [`year`]. +#[derive(Debug)] +pub struct YearFromStrError; + +impl Error for YearFromStrError {} + +impl Display for YearFromStrError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str( + format!( + "expecting a year number between 2015 and {}", + Year::last_year() + ) + .as_str(), + ) + } +} diff --git a/years/.keep b/years/.keep new file mode 100644 index 0000000..e69de29