advent_of_code_rust_runner/
lib.rs1#![doc = include_str!("../README.md")]
2
3use std::{process, time::Duration};
4
5use anyhow::anyhow;
6use clap::{CommandFactory, Parser};
7use time::{Month, OffsetDateTime, UtcOffset};
8
9mod day;
10mod environment;
11
12pub use day::{Day, DayImplementation};
13pub use anyhow::{Context, Result};
14use day::TestCaseResult;
15
16const DAY_SEPARATOR: &str = "-----------------------";
17
18#[derive(Parser, Debug)]
19#[command(
20 name = "aoc-runner",
21 about = "Run Advent of Code solutions"
22)]
23pub struct RunnerArgs {
24 #[arg(
26 short = 'd',
27 long = "day",
28 value_parser = clap::value_parser!(u8),
29 conflicts_with = "all_days"
30 )]
31 pub specific_day: Option<u8>,
32
33 #[arg(short = 'a', long = "all", conflicts_with = "specific_day")]
35 pub all_days: bool,
36
37 #[arg(short = 'k', long = "skip-tests", conflicts_with = "tests_only")]
39 pub skip_tests: bool,
40
41 #[arg(short = 't', long = "tests-only", conflicts_with = "skip_tests")]
43 pub tests_only: bool,
44
45 #[arg(
47 short = 's',
48 long = "stats",
49 default_value_t = 1,
50 value_parser = clap::value_parser!(usize)
51 )]
52 pub num_runs: usize,
53}
54
55#[derive(Debug, Clone, Copy, Default)]
56struct RunStats {
57 min: Duration,
58 max: Duration,
59 median: Duration,
60 mean: Duration,
61}
62
63pub struct Runner {
64 env: environment::AOCEnvironment,
65 days: Vec<Box<dyn Day>>
66}
67
68impl Runner {
69 pub fn new(year: &str, days: Vec<Box<dyn Day>>) -> Result<Self> {
70 let env = environment::AOCEnvironment::initialize(year)?;
71 Ok(Self {
72 env,
73 days
74 })
75 }
76
77 pub fn run_with_args(&self, args: RunnerArgs) -> ! {
79 if let Err(e) = self.run_inner(args) {
80 eprintln!("error: {e}");
81 let _ = RunnerArgs::command().print_help();
82 eprintln!();
83 process::exit(1);
84 }
85 process::exit(0);
86 }
87
88 pub fn run(&self) -> ! {
89 let args = RunnerArgs::parse();
90 self.run_with_args(args)
91 }
92
93 fn run_inner(&self, args: RunnerArgs) -> Result<()> {
94 let max_day = self.max_day()?;
95 if let Some(day) = args.specific_day
96 && (day == 0 || day > max_day)
97 {
98 return Err(anyhow!("Day must be between 1 and {}", max_day));
99 }
100 if args.num_runs == 0 {
101 return Err(anyhow!("Stats runs must be at least 1"));
102 }
103
104 if args.all_days {
105 self.run_all_days(&args)
106 } else {
107 let day_to_run = match args.specific_day {
108 Some(d) => d,
109 None => self
110 .current_aoc_day()
111 .context("No day specified; only allowed when running the configured year in December (AoC time)")?,
112 };
113 self.run_single_day(day_to_run, &args)
114 }
115 }
116
117 fn current_aoc_day(&self) -> Option<u8> {
118 let max_day = self.max_day().ok()?;
119 let offset = UtcOffset::from_hms(-5, 0, 0).ok()?;
121 let now = OffsetDateTime::now_utc().to_offset(offset);
122 if now.year().to_string() == self.env.year && now.month() == Month::December {
123 let today = now.day();
124 (today <= max_day).then_some(today)
125 } else {
126 None
127 }
128 }
129
130 fn run_all_days(&self, args: &RunnerArgs) -> Result<()> {
131 let mut medians = Vec::with_capacity(self.days.len());
132 let mut totals = RunStats::default();
133 let mut max_time = Duration::ZERO;
134
135 for (idx, day) in self.days.iter().enumerate() {
136 let stats = self.run_day(day.as_ref(), args)?;
137 medians.push(stats.median);
138 totals.min += stats.min;
139 totals.max += stats.max;
140 totals.mean += stats.mean;
141 totals.median += stats.median;
142 if stats.median > max_time {
143 max_time = stats.median;
144 }
145 println!("Day {} complete\n", idx + 1);
146 }
147
148 println!("{DAY_SEPARATOR}");
149 if !args.tests_only && max_time > Duration::ZERO {
150 if args.num_runs < 2 {
151 println!("Total time: {:?}", totals.median);
152 } else {
153 println!(
154 "Total time: {:?} median, {:?} mean, {:?} min, {:?} max",
155 totals.median, totals.mean, totals.min, totals.max
156 );
157 }
158
159 for threshold in (1..=10).rev().map(|t| t as f32 / 10.0) {
160 print!("| ");
161 for t in &medians {
162 if max_time.as_secs_f64() > 0.0
163 && (t.as_secs_f64() / max_time.as_secs_f64()) >= threshold as f64
164 {
165 print!("#");
166 } else {
167 print!(" ");
168 }
169 }
170 println!();
171 }
172 print!("|-");
173 println!("{}", "-".repeat(self.days.len()));
174 }
175 Ok(())
176 }
177
178 fn run_single_day(&self, day_number: u8, args: &RunnerArgs) -> Result<()> {
179 let day = self
180 .days
181 .iter()
182 .find(|d| d.day() == day_number)
183 .with_context(|| format!("Day {} not registered", day_number))?;
184 self.run_day(day.as_ref(), args).map(|_| ())
185 }
186
187 fn run_day(&self, day_impl: &dyn Day, args: &RunnerArgs) -> Result<RunStats> {
188 println!("{DAY_SEPARATOR}");
189 println!("Day {}", day_impl.day());
190
191 let max_day = self.max_day()?;
192 if day_impl.day() > max_day {
193 return Err(anyhow!(
194 "Day {} is not valid for {} (max {})",
195 day_impl.day(),
196 self.env.year,
197 max_day
198 ));
199 }
200
201 if let Some(today) = self.current_aoc_day()
203 && today < day_impl.day()
204 {
205 println!("Skipping - day not published yet");
206 return Ok(RunStats::default());
207 }
208
209 if !args.skip_tests {
210 match day_impl.test_day() {
211 Ok(test_result) => {
212 self.print_test_result("Part 1", &test_result.part1);
213 self.print_test_result("Part 2", &test_result.part2);
214 }
215 Err(e) => log::warn!("Day {}: failed to run tests: {}", day_impl.day(), e),
216 }
217 }
218
219 if args.tests_only {
220 return Ok(RunStats::default());
221 }
222
223 let input = self
224 .env
225 .fetch_input(day_impl.day())
226 .with_context(|| format!("Failed to fetch input for day {}", day_impl.day()))?;
227
228 if args.num_runs < 2 {
229 let res = day_impl
230 .execute_day(input.as_str())
231 .with_context(|| format!("Day {} execution failed", day_impl.day()))?;
232 let total = res.part_1_time + res.part_2_time;
233 println!(
234 "Part 1 real: {} ({:?})\nPart 2 real: {} ({:?})\nTotal time: {:?}",
235 res.part_1_result, res.part_1_time, res.part_2_result, res.part_2_time, total
236 );
237 return Ok(RunStats {
238 min: total,
239 max: total,
240 median: total,
241 mean: total,
242 });
243 }
244
245 let mut results = Vec::with_capacity(args.num_runs);
246 for _ in 0..args.num_runs {
247 results.push(
248 day_impl
249 .execute_day(input.as_str())
250 .with_context(|| format!("Day {} execution failed", day_impl.day()))?,
251 );
252 }
253
254 let mut p1_times: Vec<_> = results.iter().map(|r| r.part_1_time).collect();
255 let mut p2_times: Vec<_> = results.iter().map(|r| r.part_2_time).collect();
256
257 let p1_stats = build_stats(&mut p1_times);
258 let p2_stats = build_stats(&mut p2_times);
259 let totals = RunStats {
260 min: p1_stats.min + p2_stats.min,
261 max: p1_stats.max + p2_stats.max,
262 median: p1_stats.median + p2_stats.median,
263 mean: p1_stats.mean + p2_stats.mean,
264 };
265
266 println!(
267 "Part 1: {} (median {:?}, mean {:?}, min {:?}, max {:?})",
268 results[0].part_1_result, p1_stats.median, p1_stats.mean, p1_stats.min, p1_stats.max
269 );
270 println!(
271 "Part 2: {} (median {:?}, mean {:?}, min {:?}, max {:?})",
272 results[0].part_2_result, p2_stats.median, p2_stats.mean, p2_stats.min, p2_stats.max
273 );
274 println!(
275 "Total time: median {:?}, mean {:?}, min {:?}, max {:?}",
276 totals.median, totals.mean, totals.min, totals.max
277 );
278
279 Ok(totals)
280 }
281
282 fn print_test_result(&self, label: &str, result: &TestCaseResult) {
283 match result {
284 TestCaseResult::NotExecuted => println!("{label} test: (skipped)"),
285 TestCaseResult::Passed(t) => println!("{label} test: CORRECT ({t:?})"),
286 TestCaseResult::Failed(expected, actual) => {
287 println!("{label} test: INCORRECT");
288 println!(" Expected: {expected}");
289 println!(" Received: {actual}");
290 }
291 }
292 }
293
294 fn max_day(&self) -> Result<u8> {
295 let year: u32 = self
296 .env
297 .year
298 .parse()
299 .with_context(|| format!("Invalid year value {}", self.env.year))?;
300 Ok(if year >= 2025 { 12 } else { 25 })
301 }
302}
303
304fn build_stats(times: &mut [Duration]) -> RunStats {
305 if times.is_empty() {
306 return RunStats::default();
307 }
308 times.sort();
309 let min = times[0];
310 let max = *times.last().unwrap();
311 let median = if times.len() % 2 == 1 {
312 times[times.len() / 2]
313 } else {
314 (times[times.len() / 2 - 1] + times[times.len() / 2]) / 2
315 };
316 let sum: Duration = times.iter().copied().sum();
317 let mean = sum / (times.len() as u32);
318 RunStats {
319 min,
320 max,
321 median,
322 mean,
323 }
324}