feat: implement benching, refactor template
This commit is contained in:
parent
d10ec0573e
commit
be568dc6dd
20 changed files with 952 additions and 524 deletions
|
@ -1,10 +1,11 @@
|
|||
[alias]
|
||||
scaffold = "run --bin scaffold --quiet --release -- "
|
||||
download = "run --bin download --quiet --release -- "
|
||||
read = "run --bin read --quiet --release -- "
|
||||
scaffold = "run --quiet --release -- scaffold"
|
||||
download = "run --quiet --release -- download"
|
||||
read = "run --quiet --release -- read"
|
||||
|
||||
solve = "run --bin solve --quiet --release -- "
|
||||
all = "run"
|
||||
solve = "run --quiet --release -- solve"
|
||||
all = "run --quiet --release -- all"
|
||||
time = "run --quiet --release -- all --release --time"
|
||||
|
||||
[env]
|
||||
AOC_YEAR = "2022"
|
||||
|
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -16,5 +16,7 @@ target/
|
|||
|
||||
# Advent of Code
|
||||
# @see https://old.reddit.com/r/adventofcode/comments/k99rod/sharing_input_data_were_we_requested_not_to/gf2ukkf/?context=3
|
||||
/src/inputs
|
||||
!/src/inputs/.keep
|
||||
/data
|
||||
!/data/inputs/.keep
|
||||
!/data/examples/.keep
|
||||
!/data/puzzles/.keep
|
||||
|
|
|
@ -6,6 +6,8 @@ Solutions for [Advent of Code](https://adventofcode.com/) in [Rust](https://www.
|
|||
|
||||
<!--- advent_readme_stars table --->
|
||||
|
||||
<!--- benchmarking table --->
|
||||
|
||||
---
|
||||
|
||||
## Template setup
|
||||
|
|
0
src/bin/.keep
Normal file
0
src/bin/.keep
Normal file
|
@ -1,44 +0,0 @@
|
|||
/*
|
||||
* This file contains template code.
|
||||
* There is no need to edit this file unless you want to change template functionality.
|
||||
*/
|
||||
use advent_of_code::aoc_cli;
|
||||
use std::process;
|
||||
|
||||
struct Args {
|
||||
day: u8,
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<Args, pico_args::Error> {
|
||||
let mut args = pico_args::Arguments::from_env();
|
||||
Ok(Args {
|
||||
day: args.free_from_str()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = match parse_args() {
|
||||
Ok(args) => args,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to process arguments: {e}");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if aoc_cli::check().is_err() {
|
||||
eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it.");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
match aoc_cli::download(args.day) {
|
||||
Ok(cmd_output) => {
|
||||
if !cmd_output.status.success() {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("failed to spawn aoc-cli: {e}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
/*
|
||||
* This file contains template code.
|
||||
* There is no need to edit this file unless you want to change template functionality.
|
||||
*/
|
||||
use advent_of_code::aoc_cli;
|
||||
use std::process;
|
||||
|
||||
struct Args {
|
||||
day: u8,
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<Args, pico_args::Error> {
|
||||
let mut args = pico_args::Arguments::from_env();
|
||||
Ok(Args {
|
||||
day: args.free_from_str()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = match parse_args() {
|
||||
Ok(args) => args,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to process arguments: {e}");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if aoc_cli::check().is_err() {
|
||||
eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it.");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
match aoc_cli::read(args.day) {
|
||||
Ok(cmd_output) => {
|
||||
if !cmd_output.status.success() {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("failed to spawn aoc-cli: {e}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* This file contains template code.
|
||||
* There is no need to edit this file unless you want to change template functionality.
|
||||
*/
|
||||
|
||||
use std::process::{self, Command, Stdio};
|
||||
|
||||
struct Args {
|
||||
day: u8,
|
||||
release: bool,
|
||||
submit: Option<u8>,
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<Args, pico_args::Error> {
|
||||
let mut args = pico_args::Arguments::from_env();
|
||||
Ok(Args {
|
||||
day: args.free_from_str()?,
|
||||
release: args.contains("--release"),
|
||||
submit: args.opt_value_from_str("--submit")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn run_solution(day: u8, release: bool, submit_part: Option<u8>) -> Result<(), std::io::Error> {
|
||||
let day_padded = format!("{:02}", day);
|
||||
|
||||
let mut cmd_args = vec!["run".to_string(), "--bin".to_string(), day_padded];
|
||||
|
||||
if release {
|
||||
cmd_args.push("--release".to_string());
|
||||
}
|
||||
|
||||
if let Some(submit_part) = submit_part {
|
||||
cmd_args.push("--".to_string());
|
||||
cmd_args.push("--submit".to_string());
|
||||
cmd_args.push(submit_part.to_string())
|
||||
}
|
||||
|
||||
let mut cmd = Command::new("cargo")
|
||||
.args(&cmd_args)
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()?;
|
||||
|
||||
cmd.wait()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = match parse_args() {
|
||||
Ok(args) => args,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to process arguments: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
run_solution(args.day, args.release, args.submit).unwrap();
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
/*
|
||||
* Use this file if you want to extract helpers from your solutions.
|
||||
* Example import from this file: `use advent_of_code::helpers::example_fn;`.
|
||||
*/
|
291
src/lib.rs
291
src/lib.rs
|
@ -1,290 +1 @@
|
|||
/*
|
||||
* This file contains template code.
|
||||
* There is no need to edit this file unless you want to change template functionality.
|
||||
* Prefer `./helpers.rs` if you want to extract code from your solutions.
|
||||
*/
|
||||
use std::env;
|
||||
use std::fs;
|
||||
|
||||
pub mod helpers;
|
||||
|
||||
pub const ANSI_ITALIC: &str = "\x1b[3m";
|
||||
pub const ANSI_BOLD: &str = "\x1b[1m";
|
||||
pub const ANSI_RESET: &str = "\x1b[0m";
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! solve {
|
||||
($day:expr, $part:expr, $solver:ident, $input:expr) => {{
|
||||
use advent_of_code::{ANSI_BOLD, ANSI_ITALIC, ANSI_RESET, aoc_cli};
|
||||
use std::fmt::Display;
|
||||
use std::time::Instant;
|
||||
use std::env;
|
||||
use std::process;
|
||||
|
||||
fn submit_if_requested<T: Display>(result: T) {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
if args.contains(&"--submit".into()) {
|
||||
if aoc_cli::check().is_err() {
|
||||
eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it.");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
if args.len() < 3 {
|
||||
eprintln!("Unexpected command-line input. Format: cargo solve 1 --submit 1");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let part_index = args.iter().position(|x| x == "--submit").unwrap() + 1;
|
||||
let part_submit = match args[part_index].parse::<u8>() {
|
||||
Ok(x) => x,
|
||||
Err(_) => {
|
||||
eprintln!("Unexpected command-line input. Format: cargo solve 1 --submit 1");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if part_submit == $part {
|
||||
println!("Submitting puzzle answer for part {}...", $part);
|
||||
aoc_cli::submit($day, $part, result).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_result<T: Display>(func: impl FnOnce(&str) -> Option<T>, input: &str) {
|
||||
let timer = Instant::now();
|
||||
let result = func(input);
|
||||
let elapsed = timer.elapsed();
|
||||
match result {
|
||||
Some(result) => {
|
||||
println!(
|
||||
"{} {}(elapsed: {:.2?}){}",
|
||||
result, ANSI_ITALIC, elapsed, ANSI_RESET
|
||||
);
|
||||
submit_if_requested(result);
|
||||
}
|
||||
None => {
|
||||
println!("not solved.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("🎄 {}Part {}{} 🎄", ANSI_BOLD, $part, ANSI_RESET);
|
||||
print_result($solver, $input);
|
||||
}};
|
||||
}
|
||||
|
||||
pub fn read_file(folder: &str, day: u8) -> String {
|
||||
let cwd = env::current_dir().unwrap();
|
||||
|
||||
let filepath = cwd.join("src").join(folder).join(format!("{day:02}.txt"));
|
||||
|
||||
let f = fs::read_to_string(filepath);
|
||||
f.expect("could not open input file")
|
||||
}
|
||||
|
||||
fn parse_time(val: &str, postfix: &str) -> f64 {
|
||||
val.split(postfix).next().unwrap().parse().unwrap()
|
||||
}
|
||||
|
||||
pub fn parse_exec_time(output: &str) -> f64 {
|
||||
output.lines().fold(0_f64, |acc, l| {
|
||||
if !l.contains("elapsed:") {
|
||||
acc
|
||||
} else {
|
||||
let timing = l.split("(elapsed: ").last().unwrap();
|
||||
// use `contains` istd. of `ends_with`: string may contain ANSI escape sequences.
|
||||
// for possible time formats, see: https://github.com/rust-lang/rust/blob/1.64.0/library/core/src/time.rs#L1176-L1200
|
||||
if timing.contains("ns)") {
|
||||
acc // range below rounding precision.
|
||||
} else if timing.contains("µs)") {
|
||||
acc + parse_time(timing, "µs") / 1000_f64
|
||||
} else if timing.contains("ms)") {
|
||||
acc + parse_time(timing, "ms")
|
||||
} else if timing.contains("s)") {
|
||||
acc + parse_time(timing, "s") * 1000_f64
|
||||
} else {
|
||||
acc
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// copied from: https://github.com/rust-lang/rust/blob/1.64.0/library/std/src/macros.rs#L328-L333
|
||||
#[cfg(test)]
|
||||
macro_rules! assert_approx_eq {
|
||||
($a:expr, $b:expr) => {{
|
||||
let (a, b) = (&$a, &$b);
|
||||
assert!(
|
||||
(*a - *b).abs() < 1.0e-6,
|
||||
"{} is not approximately equal to {}",
|
||||
*a,
|
||||
*b
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_exec_time() {
|
||||
assert_approx_eq!(
|
||||
parse_exec_time(&format!(
|
||||
"🎄 Part 1 🎄\n0 (elapsed: 74.13ns){}\n🎄 Part 2 🎄\n0 (elapsed: 50.00ns){}",
|
||||
ANSI_RESET, ANSI_RESET
|
||||
)),
|
||||
0_f64
|
||||
);
|
||||
|
||||
assert_approx_eq!(
|
||||
parse_exec_time("🎄 Part 1 🎄\n0 (elapsed: 755µs)\n🎄 Part 2 🎄\n0 (elapsed: 700µs)"),
|
||||
1.455_f64
|
||||
);
|
||||
|
||||
assert_approx_eq!(
|
||||
parse_exec_time("🎄 Part 1 🎄\n0 (elapsed: 70µs)\n🎄 Part 2 🎄\n0 (elapsed: 1.45ms)"),
|
||||
1.52_f64
|
||||
);
|
||||
|
||||
assert_approx_eq!(
|
||||
parse_exec_time(
|
||||
"🎄 Part 1 🎄\n0 (elapsed: 10.3s)\n🎄 Part 2 🎄\n0 (elapsed: 100.50ms)"
|
||||
),
|
||||
10400.50_f64
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub mod aoc_cli {
|
||||
use std::{
|
||||
fmt::Display,
|
||||
process::{Command, Output, Stdio},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AocCliError {
|
||||
CommandNotFound,
|
||||
CommandNotCallable,
|
||||
BadExitStatus(Output),
|
||||
IoError,
|
||||
}
|
||||
|
||||
impl Display for AocCliError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AocCliError::CommandNotFound => write!(f, "aoc-cli is not present in environment."),
|
||||
AocCliError::CommandNotCallable => write!(f, "aoc-cli could not be called."),
|
||||
AocCliError::BadExitStatus(_) => {
|
||||
write!(f, "aoc-cli exited with a non-zero status.")
|
||||
}
|
||||
AocCliError::IoError => write!(f, "could not write output files to file system."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check() -> Result<(), AocCliError> {
|
||||
Command::new("aoc")
|
||||
.arg("-V")
|
||||
.output()
|
||||
.map_err(|_| AocCliError::CommandNotFound)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read(day: u8) -> Result<Output, AocCliError> {
|
||||
// TODO: output local puzzle if present.
|
||||
let puzzle_path = get_puzzle_path(day);
|
||||
|
||||
let args = build_args(
|
||||
"read",
|
||||
&[
|
||||
"--description-only".into(),
|
||||
"--puzzle-file".into(),
|
||||
puzzle_path,
|
||||
],
|
||||
day,
|
||||
);
|
||||
|
||||
call_aoc_cli(&args)
|
||||
}
|
||||
|
||||
pub fn download(day: u8) -> Result<Output, AocCliError> {
|
||||
let input_path = get_input_path(day);
|
||||
let puzzle_path = get_puzzle_path(day);
|
||||
|
||||
let args = build_args(
|
||||
"download",
|
||||
&[
|
||||
"--overwrite".into(),
|
||||
"--input-file".into(),
|
||||
input_path.to_string(),
|
||||
"--puzzle-file".into(),
|
||||
puzzle_path.to_string(),
|
||||
],
|
||||
day,
|
||||
);
|
||||
|
||||
let output = call_aoc_cli(&args)?;
|
||||
|
||||
if output.status.success() {
|
||||
println!("---");
|
||||
println!("🎄 Successfully wrote input to \"{}\".", &input_path);
|
||||
println!("🎄 Successfully wrote puzzle to \"{}\".", &puzzle_path);
|
||||
Ok(output)
|
||||
} else {
|
||||
Err(AocCliError::BadExitStatus(output))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn submit<T: Display>(day: u8, part: u8, result: T) -> Result<Output, AocCliError> {
|
||||
// workaround: the argument order is inverted for submit.
|
||||
let mut args = build_args("submit", &[], day);
|
||||
args.push(part.to_string());
|
||||
args.push(result.to_string());
|
||||
call_aoc_cli(&args)
|
||||
}
|
||||
|
||||
fn get_input_path(day: u8) -> String {
|
||||
let day_padded = format!("{day:02}");
|
||||
format!("src/inputs/{day_padded}.txt")
|
||||
}
|
||||
|
||||
fn get_puzzle_path(day: u8) -> String {
|
||||
let day_padded = format!("{day:02}");
|
||||
format!("src/puzzles/{day_padded}.md")
|
||||
}
|
||||
|
||||
fn get_year() -> Option<u16> {
|
||||
match std::env::var("AOC_YEAR") {
|
||||
Ok(x) => x.parse().ok().or(None),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_args(command: &str, args: &[String], day: u8) -> Vec<String> {
|
||||
let mut cmd_args = args.to_vec();
|
||||
|
||||
if let Some(year) = get_year() {
|
||||
cmd_args.push("--year".into());
|
||||
cmd_args.push(year.to_string());
|
||||
}
|
||||
|
||||
cmd_args.append(&mut vec!["--day".into(), day.to_string(), command.into()]);
|
||||
|
||||
cmd_args
|
||||
}
|
||||
|
||||
fn call_aoc_cli(args: &[String]) -> Result<Output, AocCliError> {
|
||||
if cfg!(debug_assertions) {
|
||||
println!("Calling >aoc with: {}", args.join(" "));
|
||||
}
|
||||
|
||||
Command::new("aoc")
|
||||
.args(args)
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.output()
|
||||
.map_err(|_| AocCliError::CommandNotCallable)
|
||||
}
|
||||
}
|
||||
pub mod template;
|
||||
|
|
132
src/main.rs
132
src/main.rs
|
@ -1,45 +1,93 @@
|
|||
/*
|
||||
* This file contains template code.
|
||||
* There is no need to edit this file unless you want to change template functionality.
|
||||
*/
|
||||
use advent_of_code::{ANSI_BOLD, ANSI_ITALIC, ANSI_RESET};
|
||||
use std::process::Command;
|
||||
use advent_of_code::template::commands::{
|
||||
all::all_handler, download::download_handler, read::read_handler, scaffold::scaffold_handler,
|
||||
solve::solve_handler,
|
||||
};
|
||||
use args::{parse_args, AppArgs};
|
||||
|
||||
mod args {
|
||||
use std::process;
|
||||
|
||||
pub enum AppArgs {
|
||||
Download {
|
||||
day: u8,
|
||||
},
|
||||
Read {
|
||||
day: u8,
|
||||
},
|
||||
Scaffold {
|
||||
day: u8,
|
||||
},
|
||||
Solve {
|
||||
day: u8,
|
||||
release: bool,
|
||||
time: bool,
|
||||
submit: Option<u8>,
|
||||
},
|
||||
All {
|
||||
release: bool,
|
||||
time: bool,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn parse_args() -> Result<AppArgs, Box<dyn std::error::Error>> {
|
||||
let mut args = pico_args::Arguments::from_env();
|
||||
|
||||
let app_args = match args.subcommand()?.as_deref() {
|
||||
Some("all") => AppArgs::All {
|
||||
release: args.contains("--release"),
|
||||
time: args.contains("--time"),
|
||||
},
|
||||
Some("download") => AppArgs::Download {
|
||||
day: args.free_from_str()?,
|
||||
},
|
||||
Some("read") => AppArgs::Read {
|
||||
day: args.free_from_str()?,
|
||||
},
|
||||
Some("scaffold") => AppArgs::Scaffold {
|
||||
day: args.free_from_str()?,
|
||||
},
|
||||
Some("solve") => AppArgs::Solve {
|
||||
day: args.free_from_str()?,
|
||||
release: args.contains("--release"),
|
||||
submit: args.opt_value_from_str("--submit")?,
|
||||
time: args.contains("--time"),
|
||||
},
|
||||
Some(x) => {
|
||||
eprintln!("Unknown command: {}", x);
|
||||
process::exit(1);
|
||||
}
|
||||
None => {
|
||||
eprintln!("No command specified.");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let remaining = args.finish();
|
||||
if !remaining.is_empty() {
|
||||
eprintln!("Warning: unknown argument(s): {:?}.", remaining);
|
||||
}
|
||||
|
||||
Ok(app_args)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let total: f64 = (1..=25)
|
||||
.map(|day| {
|
||||
let day = format!("{day:02}");
|
||||
|
||||
let mut args = vec!["run", "--bin", &day];
|
||||
if cfg!(not(debug_assertions)) {
|
||||
args.push("--release");
|
||||
}
|
||||
|
||||
let cmd = Command::new("cargo").args(&args).output().unwrap();
|
||||
|
||||
println!("----------");
|
||||
println!("{ANSI_BOLD}| Day {day} |{ANSI_RESET}");
|
||||
println!("----------");
|
||||
|
||||
let output = String::from_utf8(cmd.stdout).unwrap();
|
||||
let is_empty = output.is_empty();
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
if is_empty {
|
||||
"Not solved."
|
||||
} else {
|
||||
output.trim()
|
||||
}
|
||||
);
|
||||
|
||||
if is_empty {
|
||||
0_f64
|
||||
} else {
|
||||
advent_of_code::parse_exec_time(&output)
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
|
||||
println!("{ANSI_BOLD}Total:{ANSI_RESET} {ANSI_ITALIC}{total:.2}ms{ANSI_RESET}");
|
||||
match parse_args() {
|
||||
Err(err) => {
|
||||
eprintln!("Error: {}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok(args) => match args {
|
||||
AppArgs::All { release, time } => all_handler(release, time),
|
||||
AppArgs::Download { day } => download_handler(day),
|
||||
AppArgs::Read { day } => read_handler(day),
|
||||
AppArgs::Scaffold { day } => scaffold_handler(day),
|
||||
AppArgs::Solve {
|
||||
day,
|
||||
release,
|
||||
time,
|
||||
submit,
|
||||
} => solve_handler(day, release, time, submit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
127
src/template/aoc_cli.rs
Normal file
127
src/template/aoc_cli.rs
Normal file
|
@ -0,0 +1,127 @@
|
|||
/// Wrapper module around the "aoc-cli" command-line.
|
||||
use std::{
|
||||
fmt::Display,
|
||||
process::{Command, Output, Stdio},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AocCliError {
|
||||
CommandNotFound,
|
||||
CommandNotCallable,
|
||||
BadExitStatus(Output),
|
||||
IoError,
|
||||
}
|
||||
|
||||
impl Display for AocCliError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AocCliError::CommandNotFound => write!(f, "aoc-cli is not present in environment."),
|
||||
AocCliError::CommandNotCallable => write!(f, "aoc-cli could not be called."),
|
||||
AocCliError::BadExitStatus(_) => {
|
||||
write!(f, "aoc-cli exited with a non-zero status.")
|
||||
}
|
||||
AocCliError::IoError => write!(f, "could not write output files to file system."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check() -> Result<(), AocCliError> {
|
||||
Command::new("aoc")
|
||||
.arg("-V")
|
||||
.output()
|
||||
.map_err(|_| AocCliError::CommandNotFound)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read(day: u8) -> Result<Output, AocCliError> {
|
||||
let puzzle_path = get_puzzle_path(day);
|
||||
|
||||
let args = build_args(
|
||||
"read",
|
||||
&[
|
||||
"--description-only".into(),
|
||||
"--puzzle-file".into(),
|
||||
puzzle_path,
|
||||
],
|
||||
day,
|
||||
);
|
||||
|
||||
call_aoc_cli(&args)
|
||||
}
|
||||
|
||||
pub fn download(day: u8) -> Result<Output, AocCliError> {
|
||||
let input_path = get_input_path(day);
|
||||
let puzzle_path = get_puzzle_path(day);
|
||||
|
||||
let args = build_args(
|
||||
"download",
|
||||
&[
|
||||
"--overwrite".into(),
|
||||
"--input-file".into(),
|
||||
input_path.to_string(),
|
||||
"--puzzle-file".into(),
|
||||
puzzle_path.to_string(),
|
||||
],
|
||||
day,
|
||||
);
|
||||
|
||||
let output = call_aoc_cli(&args)?;
|
||||
println!("---");
|
||||
println!("🎄 Successfully wrote input to \"{}\".", &input_path);
|
||||
println!("🎄 Successfully wrote puzzle to \"{}\".", &puzzle_path);
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn submit(day: u8, part: u8, result: &str) -> Result<Output, AocCliError> {
|
||||
// workaround: the argument order is inverted for submit.
|
||||
let mut args = build_args("submit", &[], day);
|
||||
args.push(part.to_string());
|
||||
args.push(result.to_string());
|
||||
call_aoc_cli(&args)
|
||||
}
|
||||
|
||||
fn get_input_path(day: u8) -> String {
|
||||
let day_padded = format!("{:02}", day);
|
||||
format!("data/inputs/{}.txt", day_padded)
|
||||
}
|
||||
|
||||
fn get_puzzle_path(day: u8) -> String {
|
||||
let day_padded = format!("{:02}", day);
|
||||
format!("data/puzzles/{}.md", day_padded)
|
||||
}
|
||||
|
||||
fn get_year() -> Option<u16> {
|
||||
match std::env::var("AOC_YEAR") {
|
||||
Ok(x) => x.parse().ok().or(None),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_args(command: &str, args: &[String], day: u8) -> Vec<String> {
|
||||
let mut cmd_args = args.to_vec();
|
||||
|
||||
if let Some(year) = get_year() {
|
||||
cmd_args.push("--year".into());
|
||||
cmd_args.push(year.to_string());
|
||||
}
|
||||
|
||||
cmd_args.append(&mut vec!["--day".into(), day.to_string(), command.into()]);
|
||||
|
||||
cmd_args
|
||||
}
|
||||
|
||||
fn call_aoc_cli(args: &[String]) -> Result<Output, AocCliError> {
|
||||
// println!("Calling >aoc with: {}", args.join(" "));
|
||||
let output = Command::new("aoc")
|
||||
.args(args)
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.output()
|
||||
.map_err(|_| AocCliError::CommandNotCallable)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(output)
|
||||
} else {
|
||||
Err(AocCliError::BadExitStatus(output))
|
||||
}
|
||||
}
|
261
src/template/commands/all.rs
Normal file
261
src/template/commands/all.rs
Normal file
|
@ -0,0 +1,261 @@
|
|||
use std::io;
|
||||
|
||||
use crate::template::{
|
||||
readme_benchmarks::{self, Timings},
|
||||
ANSI_BOLD, ANSI_ITALIC, ANSI_RESET,
|
||||
};
|
||||
|
||||
pub fn all_handler(is_release: bool, is_timed: bool) {
|
||||
let mut timings: Vec<Timings> = vec![];
|
||||
|
||||
(1..=25).for_each(|day| {
|
||||
if day > 1 {
|
||||
println!();
|
||||
}
|
||||
|
||||
println!("{}Day {}{}", ANSI_BOLD, day, ANSI_RESET);
|
||||
println!("------");
|
||||
|
||||
let output = child_commands::run_solution(day, is_timed, is_release).unwrap();
|
||||
|
||||
if output.is_empty() {
|
||||
println!("Not solved.");
|
||||
} else {
|
||||
let val = child_commands::parse_exec_time(&output, day);
|
||||
timings.push(val);
|
||||
}
|
||||
});
|
||||
|
||||
if is_timed {
|
||||
let total_millis = timings.iter().map(|x| x.total_nanos).sum::<f64>() / 1000000_f64;
|
||||
|
||||
println!(
|
||||
"\n{}Total:{} {}{:.2}ms{}",
|
||||
ANSI_BOLD, ANSI_RESET, ANSI_ITALIC, total_millis, ANSI_RESET
|
||||
);
|
||||
|
||||
if is_release {
|
||||
match readme_benchmarks::update(timings, total_millis) {
|
||||
Ok(_) => println!("Successfully updated README with benchmarks."),
|
||||
Err(_) => {
|
||||
eprintln!("Failed to update readme with benchmarks.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
BrokenPipe,
|
||||
Parser(String),
|
||||
IO(io::Error),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Error::IO(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_path_for_bin(day: usize) -> String {
|
||||
let day_padded = format!("{:02}", day);
|
||||
format!("./src/bin/{}.rs", day_padded)
|
||||
}
|
||||
|
||||
/// All solutions live in isolated binaries.
|
||||
/// This module encapsulates interaction with these binaries, both invoking them as well as parsing the timing output.
|
||||
mod child_commands {
|
||||
use super::{get_path_for_bin, Error};
|
||||
use std::{
|
||||
io::{BufRead, BufReader},
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
thread,
|
||||
};
|
||||
|
||||
/// Run the solution bin for a given day
|
||||
pub fn run_solution(
|
||||
day: usize,
|
||||
is_timed: bool,
|
||||
is_release: bool,
|
||||
) -> Result<Vec<String>, Error> {
|
||||
let day_padded = format!("{:02}", day);
|
||||
|
||||
// skip command invocation for days that have not been scaffolded yet.
|
||||
if !Path::new(&get_path_for_bin(day)).exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut args = vec!["run", "--quiet", "--bin", &day_padded];
|
||||
|
||||
if is_release {
|
||||
args.push("--release");
|
||||
}
|
||||
|
||||
if is_timed {
|
||||
// mirror `--time` flag to child invocations.
|
||||
args.push("--");
|
||||
args.push("--time");
|
||||
}
|
||||
|
||||
// spawn child command with piped stdout/stderr.
|
||||
// forward output to stdout/stderr while grabbing stdout lines.
|
||||
|
||||
let mut cmd = Command::new("cargo")
|
||||
.args(&args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let stdout = BufReader::new(cmd.stdout.take().ok_or(super::Error::BrokenPipe)?);
|
||||
let stderr = BufReader::new(cmd.stderr.take().ok_or(super::Error::BrokenPipe)?);
|
||||
|
||||
let mut output = vec![];
|
||||
|
||||
let thread = thread::spawn(move || {
|
||||
stderr.lines().for_each(|line| {
|
||||
eprintln!("{}", line.unwrap());
|
||||
});
|
||||
});
|
||||
|
||||
for line in stdout.lines() {
|
||||
let line = line.unwrap();
|
||||
println!("{}", line);
|
||||
output.push(line);
|
||||
}
|
||||
|
||||
thread.join().unwrap();
|
||||
cmd.wait()?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn parse_exec_time(output: &[String], day: usize) -> super::Timings {
|
||||
let mut timings = super::Timings {
|
||||
day,
|
||||
part_1: None,
|
||||
part_2: None,
|
||||
total_nanos: 0_f64,
|
||||
};
|
||||
|
||||
output
|
||||
.iter()
|
||||
.filter_map(|l| {
|
||||
if !l.contains(" samples)") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (timing_str, nanos) = match parse_time(l) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
eprintln!("Could not parse timings from line: {l}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let part = l.split(':').next()?;
|
||||
Some((part, timing_str, nanos))
|
||||
})
|
||||
.for_each(|(part, timing_str, nanos)| {
|
||||
if part.contains("Part 1") {
|
||||
timings.part_1 = Some(timing_str.into());
|
||||
} else if part.contains("Part 2") {
|
||||
timings.part_2 = Some(timing_str.into());
|
||||
}
|
||||
|
||||
timings.total_nanos += nanos;
|
||||
});
|
||||
|
||||
timings
|
||||
}
|
||||
|
||||
fn parse_to_float(s: &str, postfix: &str) -> Option<f64> {
|
||||
s.split(postfix).next()?.parse().ok()
|
||||
}
|
||||
|
||||
fn parse_time(line: &str) -> Option<(&str, f64)> {
|
||||
// for possible time formats, see: https://github.com/rust-lang/rust/blob/1.64.0/library/core/src/time.rs#L1176-L1200
|
||||
let str_timing = line
|
||||
.split(" samples)")
|
||||
.next()?
|
||||
.split('(')
|
||||
.last()?
|
||||
.split('@')
|
||||
.next()?
|
||||
.trim();
|
||||
|
||||
let parsed_timing = match str_timing {
|
||||
s if s.contains("ns") => s.split("ns").next()?.parse::<f64>().ok(),
|
||||
s if s.contains("µs") => parse_to_float(s, "µs").map(|x| x * 1000_f64),
|
||||
s if s.contains("ms") => parse_to_float(s, "ms").map(|x| x * 1000000_f64),
|
||||
s => parse_to_float(s, "s").map(|x| x * 1000000000_f64),
|
||||
}?;
|
||||
|
||||
Some((str_timing, parsed_timing))
|
||||
}
|
||||
|
||||
/// copied from: https://github.com/rust-lang/rust/blob/1.64.0/library/std/src/macros.rs#L328-L333
|
||||
#[cfg(test)]
|
||||
macro_rules! assert_approx_eq {
|
||||
($a:expr, $b:expr) => {{
|
||||
let (a, b) = (&$a, &$b);
|
||||
assert!(
|
||||
(*a - *b).abs() < 1.0e-6,
|
||||
"{} is not approximately equal to {}",
|
||||
*a,
|
||||
*b
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_exec_time;
|
||||
|
||||
#[test]
|
||||
fn test_well_formed() {
|
||||
let res = parse_exec_time(
|
||||
&[
|
||||
"Part 1: 0 (74.13ns @ 100000 samples)".into(),
|
||||
"Part 2: 10 (74.13ms @ 99999 samples)".into(),
|
||||
"".into(),
|
||||
],
|
||||
1,
|
||||
);
|
||||
assert_approx_eq!(res.total_nanos, 74137374.13_f64);
|
||||
assert_eq!(res.part_1.unwrap(), "74.13ns");
|
||||
assert_eq!(res.part_2.unwrap(), "74.13ms");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_patterns_in_input() {
|
||||
let res = parse_exec_time(
|
||||
&[
|
||||
"Part 1: @ @ @ ( ) ms (2s @ 5 samples)".into(),
|
||||
"Part 2: 10s (100ms @ 1 samples)".into(),
|
||||
"".into(),
|
||||
],
|
||||
1,
|
||||
);
|
||||
assert_approx_eq!(res.total_nanos, 3100000000_f64);
|
||||
assert_eq!(res.part_1.unwrap(), "2s");
|
||||
assert_eq!(res.part_2.unwrap(), "100ms");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_parts() {
|
||||
let res = parse_exec_time(
|
||||
&[
|
||||
"Part 1: ✖ ".into(),
|
||||
"Part 2: ✖ ".into(),
|
||||
"".into(),
|
||||
],
|
||||
1,
|
||||
);
|
||||
assert_approx_eq!(res.total_nanos, 1000000_f64);
|
||||
assert_eq!(res.part_1.is_none(), true);
|
||||
assert_eq!(res.part_2.is_none(), true);
|
||||
}
|
||||
}
|
||||
}
|
14
src/template/commands/download.rs
Normal file
14
src/template/commands/download.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
use crate::template::aoc_cli;
|
||||
use std::process;
|
||||
|
||||
pub fn download_handler(day: u8) {
|
||||
if aoc_cli::check().is_err() {
|
||||
eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it.");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
if let Err(e) = aoc_cli::download(day) {
|
||||
eprintln!("failed to call aoc-cli: {}", e);
|
||||
process::exit(1);
|
||||
};
|
||||
}
|
5
src/template/commands/mod.rs
Normal file
5
src/template/commands/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub mod all;
|
||||
pub mod download;
|
||||
pub mod read;
|
||||
pub mod scaffold;
|
||||
pub mod solve;
|
15
src/template/commands/read.rs
Normal file
15
src/template/commands/read.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use std::process;
|
||||
|
||||
use crate::template::aoc_cli;
|
||||
|
||||
pub fn read_handler(day: u8) {
|
||||
if aoc_cli::check().is_err() {
|
||||
eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it.");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
if let Err(e) = aoc_cli::read(day) {
|
||||
eprintln!("failed to call aoc-cli: {}", e);
|
||||
process::exit(1);
|
||||
};
|
||||
}
|
|
@ -1,7 +1,3 @@
|
|||
/*
|
||||
* This file contains template code.
|
||||
* There is no need to edit this file unless you want to change template functionality.
|
||||
*/
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::Write,
|
||||
|
@ -16,11 +12,7 @@ pub fn part_two(input: &str) -> Option<u32> {
|
|||
None
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let input = &advent_of_code::read_file("inputs", DAY);
|
||||
advent_of_code::solve!(DAY, 1, part_one, input);
|
||||
advent_of_code::solve!(DAY, 2, part_two, input);
|
||||
}
|
||||
advent_of_code::main!(DAY);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
@ -28,23 +20,18 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_part_one() {
|
||||
let input = advent_of_code::read_file("examples", DAY);
|
||||
assert_eq!(part_one(&input), None);
|
||||
let result = part_one(&advent_of_code::template::read_file("examples", DAY));
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_part_two() {
|
||||
let input = advent_of_code::read_file("examples", DAY);
|
||||
assert_eq!(part_two(&input), None);
|
||||
let result = part_two(&advent_of_code::template::read_file("examples", DAY));
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
fn parse_args() -> Result<u8, pico_args::Error> {
|
||||
let mut args = pico_args::Arguments::from_env();
|
||||
args.free_from_str()
|
||||
}
|
||||
|
||||
fn safe_create_file(path: &str) -> Result<File, std::io::Error> {
|
||||
OpenOptions::new().write(true).create_new(true).open(path)
|
||||
}
|
||||
|
@ -53,25 +40,17 @@ fn create_file(path: &str) -> Result<File, std::io::Error> {
|
|||
OpenOptions::new().write(true).create(true).open(path)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let day = match parse_args() {
|
||||
Ok(day) => day,
|
||||
Err(_) => {
|
||||
eprintln!("Need to specify a day (as integer). example: `cargo scaffold 7`");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
pub fn scaffold_handler(day: u8) {
|
||||
let day_padded = format!("{:02}", day);
|
||||
|
||||
let day_padded = format!("{day:02}");
|
||||
|
||||
let input_path = format!("src/inputs/{day_padded}.txt");
|
||||
let example_path = format!("src/examples/{day_padded}.txt");
|
||||
let module_path = format!("src/bin/{day_padded}.rs");
|
||||
let input_path = format!("data/inputs/{}.txt", day_padded);
|
||||
let example_path = format!("data/examples/{}.txt", day_padded);
|
||||
let module_path = format!("src/bin/{}.rs", day_padded);
|
||||
|
||||
let mut file = match safe_create_file(&module_path) {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create module file: {e}");
|
||||
eprintln!("Failed to create module file: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
@ -81,7 +60,7 @@ fn main() {
|
|||
println!("Created module file \"{}\"", &module_path);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to write module contents: {e}");
|
||||
eprintln!("Failed to write module contents: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
@ -91,7 +70,7 @@ fn main() {
|
|||
println!("Created empty input file \"{}\"", &input_path);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create input file: {e}");
|
||||
eprintln!("Failed to create input file: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
@ -101,7 +80,7 @@ fn main() {
|
|||
println!("Created empty example file \"{}\"", &example_path);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create example file: {e}");
|
||||
eprintln!("Failed to create example file: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
31
src/template/commands/solve.rs
Normal file
31
src/template/commands/solve.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use std::process::{Command, Stdio};
|
||||
|
||||
pub fn solve_handler(day: u8, release: bool, time: bool, submit_part: Option<u8>) {
|
||||
let day_padded = format!("{:02}", day);
|
||||
|
||||
let mut cmd_args = vec!["run".to_string(), "--bin".to_string(), day_padded];
|
||||
|
||||
if release {
|
||||
cmd_args.push("--release".to_string());
|
||||
}
|
||||
|
||||
cmd_args.push("--".to_string());
|
||||
|
||||
if let Some(submit_part) = submit_part {
|
||||
cmd_args.push("--submit".to_string());
|
||||
cmd_args.push(submit_part.to_string())
|
||||
}
|
||||
|
||||
if time {
|
||||
cmd_args.push("--time".to_string());
|
||||
}
|
||||
|
||||
let mut cmd = Command::new("cargo")
|
||||
.args(&cmd_args)
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
cmd.wait().unwrap();
|
||||
}
|
34
src/template/mod.rs
Normal file
34
src/template/mod.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use std::{env, fs};
|
||||
|
||||
pub mod aoc_cli;
|
||||
pub mod commands;
|
||||
pub mod readme_benchmarks;
|
||||
pub mod runner;
|
||||
|
||||
pub const ANSI_ITALIC: &str = "\x1b[3m";
|
||||
pub const ANSI_BOLD: &str = "\x1b[1m";
|
||||
pub const ANSI_RESET: &str = "\x1b[0m";
|
||||
|
||||
/// Helper function that reads a text file to a string.
|
||||
pub fn read_file(folder: &str, day: u8) -> String {
|
||||
let cwd = env::current_dir().unwrap();
|
||||
let filepath = cwd
|
||||
.join("data")
|
||||
.join(folder)
|
||||
.join(format!("{:02}.txt", day));
|
||||
let f = fs::read_to_string(filepath);
|
||||
f.expect("could not open input file")
|
||||
}
|
||||
|
||||
/// main! produces a block setting up the input and runner for each part.
|
||||
#[macro_export]
|
||||
macro_rules! main {
|
||||
($day:expr) => {
|
||||
fn main() {
|
||||
use advent_of_code::template::runner::*;
|
||||
let input = advent_of_code::template::read_file("inputs", $day);
|
||||
run_part(part_one,&input, $day, 1);
|
||||
run_part(part_two, &input, $day, 2);
|
||||
}
|
||||
};
|
||||
}
|
183
src/template/readme_benchmarks.rs
Normal file
183
src/template/readme_benchmarks.rs
Normal file
|
@ -0,0 +1,183 @@
|
|||
/// Module that updates the readme me with timing information.
|
||||
/// The approach taken is similar to how `aoc-readme-stars` handles this.
|
||||
use std::{fs, io};
|
||||
|
||||
static MARKER: &str = "<!--- benchmarking table --->";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Parser(String),
|
||||
IO(io::Error),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Error::IO(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Timings {
|
||||
pub day: usize,
|
||||
pub part_1: Option<String>,
|
||||
pub part_2: Option<String>,
|
||||
pub total_nanos: f64,
|
||||
}
|
||||
|
||||
pub struct TablePosition {
|
||||
pos_start: usize,
|
||||
pos_end: usize,
|
||||
}
|
||||
|
||||
pub fn get_path_for_bin(day: usize) -> String {
|
||||
let day_padded = format!("{:02}", day);
|
||||
format!("./src/bin/{}.rs", day_padded)
|
||||
}
|
||||
|
||||
fn locate_table(readme: &str) -> Result<TablePosition, Error> {
|
||||
let matches: Vec<_> = readme.match_indices(MARKER).collect();
|
||||
|
||||
if matches.len() > 2 {
|
||||
return Err(Error::Parser(
|
||||
"{}: too many occurences of marker in README.".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let pos_start = matches
|
||||
.first()
|
||||
.map(|m| m.0)
|
||||
.ok_or_else(|| Error::Parser("Could not find table start position.".into()))?;
|
||||
|
||||
let pos_end = matches
|
||||
.last()
|
||||
.map(|m| m.0 + m.1.len())
|
||||
.ok_or_else(|| Error::Parser("Could not find table end position.".into()))?;
|
||||
|
||||
Ok(TablePosition { pos_start, pos_end })
|
||||
}
|
||||
|
||||
fn construct_table(prefix: &str, timings: Vec<Timings>, total_millis: f64) -> String {
|
||||
let header = format!("{prefix} Benchmarks");
|
||||
|
||||
let mut lines: Vec<String> = vec![
|
||||
MARKER.into(),
|
||||
header,
|
||||
"".into(),
|
||||
"| Day | Part 1 | Part 2 |".into(),
|
||||
"| :---: | :---: | :---: |".into(),
|
||||
];
|
||||
|
||||
timings.into_iter().for_each(|timing| {
|
||||
let path = get_path_for_bin(timing.day);
|
||||
lines.push(format!(
|
||||
"| [Day {}]({}) | `{}` | `{}` |",
|
||||
timing.day,
|
||||
path,
|
||||
timing.part_1.unwrap_or_else(|| "-".into()),
|
||||
timing.part_2.unwrap_or_else(|| "-".into())
|
||||
));
|
||||
});
|
||||
|
||||
lines.push("".into());
|
||||
lines.push(format!("**Total: {:.2}ms**", total_millis));
|
||||
lines.push(MARKER.into());
|
||||
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
fn update_content(s: &mut String, timings: Vec<Timings>, total_millis: f64) -> Result<(), Error> {
|
||||
let positions = locate_table(s)?;
|
||||
let table = construct_table("##", timings, total_millis);
|
||||
s.replace_range(positions.pos_start..positions.pos_end, &table);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update(timings: Vec<Timings>, total_millis: f64) -> Result<(), Error> {
|
||||
let path = "README.md";
|
||||
let mut readme = String::from_utf8_lossy(&fs::read(path)?).to_string();
|
||||
update_content(&mut readme, timings, total_millis)?;
|
||||
fs::write(path, &readme)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{update_content, Timings, MARKER};
|
||||
|
||||
fn get_mock_timings() -> Vec<Timings> {
|
||||
vec![
|
||||
Timings {
|
||||
day: 1,
|
||||
part_1: Some("10ms".into()),
|
||||
part_2: Some("20ms".into()),
|
||||
total_nanos: 3e+10,
|
||||
},
|
||||
Timings {
|
||||
day: 2,
|
||||
part_1: Some("30ms".into()),
|
||||
part_2: Some("40ms".into()),
|
||||
total_nanos: 7e+10,
|
||||
},
|
||||
Timings {
|
||||
day: 4,
|
||||
part_1: Some("40ms".into()),
|
||||
part_2: Some("50ms".into()),
|
||||
total_nanos: 9e+10,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn errors_if_marker_not_present() {
|
||||
let mut s = "# readme".to_string();
|
||||
update_content(&mut s, get_mock_timings(), 190.0).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn errors_if_too_many_markers_present() {
|
||||
let mut s = format!("{} {} {}", MARKER, MARKER, MARKER);
|
||||
update_content(&mut s, get_mock_timings(), 190.0).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn updates_empty_benchmarks() {
|
||||
let mut s = format!("foo\nbar\n{}{}\nbaz", MARKER, MARKER);
|
||||
update_content(&mut s, get_mock_timings(), 190.0).unwrap();
|
||||
assert_eq!(s.contains("## Benchmarks"), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn updates_existing_benchmarks() {
|
||||
let mut s = format!("foo\nbar\n{}{}\nbaz", MARKER, MARKER);
|
||||
update_content(&mut s, get_mock_timings(), 190.0).unwrap();
|
||||
update_content(&mut s, get_mock_timings(), 190.0).unwrap();
|
||||
assert_eq!(s.matches(MARKER).collect::<Vec<&str>>().len(), 2);
|
||||
assert_eq!(s.matches("## Benchmarks").collect::<Vec<&str>>().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_benchmarks() {
|
||||
let mut s = format!("foo\nbar\n{}\n{}\nbaz", MARKER, MARKER);
|
||||
update_content(&mut s, get_mock_timings(), 190.0).unwrap();
|
||||
let expected = [
|
||||
"foo",
|
||||
"bar",
|
||||
"<!--- benchmarking table --->",
|
||||
"## Benchmarks",
|
||||
"",
|
||||
"| Day | Parser | Part 1 | Part 2 |",
|
||||
"| :---: | :---: | :---: | :---: |",
|
||||
"| [Day 1](./src/bin/01.rs) | `-` | `10ms` | `20ms` |",
|
||||
"| [Day 2](./src/bin/02.rs) | `-` | `30ms` | `40ms` |",
|
||||
"| [Day 4](./src/bin/04.rs) | `-` | `40ms` | `50ms` |",
|
||||
"",
|
||||
"**Total: 190.00ms**",
|
||||
"<!--- benchmarking table --->",
|
||||
"baz",
|
||||
]
|
||||
.join("\n");
|
||||
assert_eq!(s, expected);
|
||||
}
|
||||
}
|
165
src/template/runner.rs
Normal file
165
src/template/runner.rs
Normal file
|
@ -0,0 +1,165 @@
|
|||
/// Encapsulates code that interacts with solution functions.
|
||||
use crate::template::{aoc_cli, ANSI_ITALIC, ANSI_RESET};
|
||||
use std::fmt::Display;
|
||||
use std::io::{stdout, Write};
|
||||
use std::process::Output;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{cmp, env, process};
|
||||
|
||||
use super::ANSI_BOLD;
|
||||
|
||||
pub fn run_part<I: Clone, T: Display>(func: impl Fn(I) -> Option<T>, input: I, day: u8, part: u8) {
|
||||
let part_str = format!("Part {}", part);
|
||||
|
||||
let (result, duration, samples) =
|
||||
run_timed(func, input, |result| print_result(result, &part_str, ""));
|
||||
|
||||
print_result(&result, &part_str, &format_duration(&duration, samples));
|
||||
|
||||
if let Some(result) = result {
|
||||
submit_result(result, day, part);
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a solution part. The behavior differs depending on whether we are running a release or debug build:
|
||||
/// 1. in debug, the function is executed once.
|
||||
/// 2. in release, the function is benched (approx. 1 second of execution time or 10 samples, whatever take longer.)
|
||||
fn run_timed<I: Clone, T>(
|
||||
func: impl Fn(I) -> T,
|
||||
input: I,
|
||||
hook: impl Fn(&T),
|
||||
) -> (T, Duration, u128) {
|
||||
let timer = Instant::now();
|
||||
let result = func(input.clone());
|
||||
let base_time = timer.elapsed();
|
||||
|
||||
hook(&result);
|
||||
|
||||
let run = match std::env::args().any(|x| x == "--time") {
|
||||
true => bench(func, input, &base_time),
|
||||
false => (base_time, 1),
|
||||
};
|
||||
|
||||
(result, run.0, run.1)
|
||||
}
|
||||
|
||||
fn bench<I: Clone, T>(func: impl Fn(I) -> T, input: I, base_time: &Duration) -> (Duration, u128) {
|
||||
let mut stdout = stdout();
|
||||
|
||||
print!(" > {}benching{}", ANSI_ITALIC, ANSI_RESET);
|
||||
let _ = stdout.flush();
|
||||
|
||||
let bench_iterations = cmp::min(
|
||||
100000,
|
||||
cmp::max(
|
||||
Duration::from_secs(1).as_nanos() / cmp::max(base_time.as_nanos(), 10),
|
||||
10,
|
||||
),
|
||||
);
|
||||
|
||||
let mut timers: Vec<Duration> = vec![];
|
||||
|
||||
for _ in 0..bench_iterations {
|
||||
// need a clone here to make the borrow checker happy.
|
||||
let cloned = input.clone();
|
||||
let timer = Instant::now();
|
||||
func(cloned);
|
||||
timers.push(timer.elapsed());
|
||||
}
|
||||
|
||||
(
|
||||
Duration::from_nanos(average_duration(&timers) as u64),
|
||||
bench_iterations as u128,
|
||||
)
|
||||
}
|
||||
|
||||
fn average_duration(numbers: &[Duration]) -> u128 {
|
||||
numbers.iter().map(|d| d.as_nanos()).sum::<u128>() / numbers.len() as u128
|
||||
}
|
||||
|
||||
fn format_duration(duration: &Duration, samples: u128) -> String {
|
||||
if samples == 1 {
|
||||
format!(" ({:.1?})", duration)
|
||||
} else {
|
||||
format!(" ({:.1?} @ {} samples)", duration, samples)
|
||||
}
|
||||
}
|
||||
|
||||
fn print_result<T: Display>(result: &Option<T>, part: &str, duration_str: &str) {
|
||||
let is_intermediate_result = duration_str.is_empty();
|
||||
|
||||
match result {
|
||||
Some(result) => {
|
||||
if result.to_string().contains('\n') {
|
||||
let str = format!("{}: ▼ {}", part, duration_str);
|
||||
if is_intermediate_result {
|
||||
print!("{}", str);
|
||||
} else {
|
||||
print!("\r");
|
||||
println!("{}", str);
|
||||
println!("{}", result);
|
||||
}
|
||||
} else {
|
||||
let str = format!(
|
||||
"{}: {}{}{}{}",
|
||||
part, ANSI_BOLD, result, ANSI_RESET, duration_str
|
||||
);
|
||||
if is_intermediate_result {
|
||||
print!("{}", str);
|
||||
} else {
|
||||
print!("\r");
|
||||
println!("{}", str);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if is_intermediate_result {
|
||||
print!("{}: ✖", part);
|
||||
} else {
|
||||
print!("\r");
|
||||
println!("{}: ✖ ", part);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the arguments passed to `solve` and try to submit one part of the solution if:
|
||||
/// 1. we are in `--release` mode.
|
||||
/// 2. aoc-cli is installed.
|
||||
fn submit_result<T: Display>(
|
||||
result: T,
|
||||
day: u8,
|
||||
part: u8,
|
||||
) -> Option<Result<Output, aoc_cli::AocCliError>> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
if !args.contains(&"--submit".into()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if args.len() < 3 {
|
||||
eprintln!("Unexpected command-line input. Format: cargo solve 1 --submit 1");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let part_index = args.iter().position(|x| x == "--submit").unwrap() + 1;
|
||||
let part_submit = match args[part_index].parse::<u8>() {
|
||||
Ok(x) => x,
|
||||
Err(_) => {
|
||||
eprintln!("Unexpected command-line input. Format: cargo solve 1 --submit 1");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if part_submit != part {
|
||||
return None;
|
||||
}
|
||||
|
||||
if aoc_cli::check().is_err() {
|
||||
eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it.");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
println!("Submitting result via aoc-cli...");
|
||||
Some(aoc_cli::submit(day, part, &result.to_string()))
|
||||
}
|
Loading…
Add table
Reference in a new issue