From abc1530bf3f5ee3e830b9c70853552de4c218ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Ludwig?= Date: Sun, 13 Aug 2023 12:26:59 +0200 Subject: [PATCH] day 24 finsihed --- data/day24/example01.txt | 6 + data/day24/example02.txt | 7 + data/day24/input.txt | 37 +++ src/common/abs.rs | 49 ++++ src/common/direction.rs | 8 +- src/common/mod.rs | 1 + src/common/pos2.rs | 8 +- src/days/day24/mod.rs | 530 +++++++++++++++++++++++++++++++++++++++ src/days/mod.rs | 4 +- 9 files changed, 641 insertions(+), 9 deletions(-) create mode 100644 data/day24/example01.txt create mode 100644 data/day24/example02.txt create mode 100644 data/day24/input.txt create mode 100644 src/common/abs.rs create mode 100644 src/days/day24/mod.rs diff --git a/data/day24/example01.txt b/data/day24/example01.txt new file mode 100644 index 0000000..6b9b892 --- /dev/null +++ b/data/day24/example01.txt @@ -0,0 +1,6 @@ +#.###### +#>>.<^<# +#.<..<<# +#>v.><># +#<^v^^># +######.# \ No newline at end of file diff --git a/data/day24/example02.txt b/data/day24/example02.txt new file mode 100644 index 0000000..1f3bf6d --- /dev/null +++ b/data/day24/example02.txt @@ -0,0 +1,7 @@ +#.##### +#.....# +#>....# +#.....# +#...v.# +#.....# +#####.# \ No newline at end of file diff --git a/data/day24/input.txt b/data/day24/input.txt new file mode 100644 index 0000000..b665c96 --- /dev/null +++ b/data/day24/input.txt @@ -0,0 +1,37 @@ +#.#################################################################################################### +#>^v^v>><.>>><>^v<>^v>v<>^<<>>v>>.>>^<>^.<>v.>v^>vvvv<<<<>>^v^^^>><><.^^^.<<<>><^v^vv^v<.v.v>>.vv<# +#>^><>^v^<>.<.<.vvv<^>>>v^<>>^^^><.>>>>v<<>^.>.^^<<.<><.>>>^^^>>.<># +#<.vvv^^v>^<<^^^>.^<..v.v<<^>vv<>>>v^>vv<>v<^^^^v>.<.vv>v>^<^^<>^>v^<^.^^v><>.><># +#>..<^^<>>v>><>vv^^<.v^<<^<>>vvv.vvv<>v>><<.>^>><^v.v.<>^>v># +#>^^v><^<^>^^^><^>><^^.vv>v<>v<>^>^v>>.>>^<^v^v^>v^<.vvv^^<.>>.^>><<.vvv.># +#>v^>^<>v^>>^^<>.><^v<^vvv>vv<.><>v>v<..<^>>v^v<<<v^.<<<^^^^.^v^<<>^vvv>^><.^v<^<>vv><<^<>^<^<<# +#.^vvv^^v<>v<.<<<>^><<^>^vv<<>^<<>^v.>>.<>>^<^<^>v^v>^vv.# +#^<^><^..>^.>>v>>vv<^<.>.^v^^<^v<^<.^vv>v>^>^v>>>>^^^v>^^>>>v^<# +#><>v^>^v<.<.>.<^.<>>^^>v^vvv<<^>vv<^^>^<^>v<.^.v<<.v.v^v>v><>..^^<.v^^<.<<<<<><>v><<>v><>^^^.<.<<>^v>v^>^>>>>v>.^>.v>.^>^.v<^<>v.v>v^v^^>^v>>>.<^v^^.>>>>^<>>>^>vvv^>^><<# +#>v^.v>>><>^>><>.^<^v.>.^<^>>^v<.<^^v<^><..vv<>^.^>^<^<^>><>.><<># +#>.<^^v^><<<.v^<>v^vv<^^.<>>.^>^.v.>^>.<<>v<>^v<<>vvv.v<>v>vvv^<>v^>^v<<<><# +#.>><<>vv>v..<<^^vv^>^<>^^v>v^vv>.><>^<^^>><>>v<^v^^><>.^.vv>v.^^>^vv<><.>>^^<^<><>v>.# +#><v<><^<<^><..<^>v<<^.^^<.^^v<>>^v>.^^^<v>><><.<>>^v<^.v>>vv^^^^<><>v.>.^<^v<# +#<.>vvv^.vv.>.^v<>>^^vv>.>^v^>>>vv^<^v.v^>>>>><>v>v<>v^^>.^^v>^v><<>vv^<><>>v<^# +#<^v<>>.<<>^>v<<.v^v.<^v.^<><>v<>.vv^>vv<<^^<^^^><<>^>.^v<<<^<^<^^>>vv.vv^^<<^^^v^^^^.vvv^^^^v^>^^^v<^v><<^^^.<>^><.^><^<>.v>^vv^v<<^<.<^.^>vv>^v>^<.# +#<.vv>><.<>>>><<^^<><^^v..>v<^^v><^><<^^^<<>^^>.>v><^v^v^<<.^>># +#<^vv<<>^^<<^.^vv.><^>v.>^vv.<^v^^<^<>v<<>^v^>v^^^><^>>^.>.<<^<# +#>>..^^^>^^.<>>>^><>^<<.v.^v^^<^<><>..>^<>.>^^^vv.v>.<.^^^^<^v<.# +#>>vvv^v^v^>v<^>v<.^>^^^<^<>^.><^v<<>..v><>v^>^>><^^<>.v>>>v<^v.v>v.>^vv^.^<# +#<^<>.^<>>^<^v>.^v.><.<^>.>^<>^^>>^^^.<^<>><>^>^.v^^v>vv<^.^>><.<>^.^<>.>>>v>v# +#<^^vv<<^<^.<^>^>.>><^<^^^<<>.v>v>v>v.^<v<<<>v<>^>>>.^><^^<>>>^<><^.^.v^^v^^>..# +#..v>vv^><^>^^^><>.^^><^<><>^>>^v<>><.>^>v^>^v<>><><^>v>.v^v>v<><.^^.>.v.^<# +#.v<^v.<<^v^vv<^v^>^^v^<>^>>vv<^^v^^^^<.^v<>^<<^v^<<>>.vvv.>>^v>>>vv<<><^v>># +#<..v>vv>v>>^v^vv<v<^^^>v<^^^^^>>><v<>.>v^v<><.<^.^^.# +#>>>>>^^.><^<>^<>v>.<^vv^^<>>^v><^<^>vv^<.>^v>.v>>>^.^>^v^^^^><<>><<^<.<^v<# +#<.v.^>.^<.v<^^<.<^<<^.^<>><^<<^<><.<>.><<<><^v^^v<><^<>>^>^^>vv>^<.^<>.>>>^v<# +#<>v^>^^v>>^<^>v>>>.>^.><>..>^v>><>v<>^>.^v<<<<^>v>v>^^>^<<>^>vv><<v^^vvv^<.v^<^v>^># +#><vv^<>^^v<>v<^^^^><>^^>^<<<>v.vv^<><<><.^<<<^>>^vv<.v^^>vv><<<>v.<><^^^v.# +#<><^<^^vvv^>><><<<.<.^<>>^v^>>vvv>>v..><<^^.v<><^^><^>>^<v>vvvvv...^><<^<<.>>vvv^v<^<>.^v^<^v<^^>>v<.>v.v>^>>v^<<.v<^<..>.v<<..^^^v>v<.^<.>v^v^^># +#<^>v<>^<>vv>.><^v<^.><<^v>v<^<<<>v><>^v><<vv<>vv>><>>^><>>v^.^v<.v^^v<># +#<^<^><<>v..>v^>v>.v<<<^<<^<.<.^>>vv^.v^.^^>^^<<^^>.vv<.>.^^<>.<>># +#>v>>vv<.v<><^^v>^>>>>^>vv><vv^.>>v^^vv^v^.>^v<>>>^^^>..vv^.^><..>>v<>^>^^<<.v<>v^^^v>^v># +####################################################################################################.# \ No newline at end of file diff --git a/src/common/abs.rs b/src/common/abs.rs new file mode 100644 index 0000000..181274d --- /dev/null +++ b/src/common/abs.rs @@ -0,0 +1,49 @@ +pub trait Abs { + fn abs(&self) -> Self; + fn abs_beween(&self, other: &Self) -> Self; + fn is_negative(&self) -> bool; +} + +macro_rules! signed_impl { + ($($t:ty)*) => ($( + impl Abs for $t { + #[inline] + fn abs(&self) -> $t { + if self.is_negative() { -*self } else { *self } + } + #[inline] + fn abs_beween(&self, other: &Self) -> Self { + (*self - *other).abs() + } + #[inline] + fn is_negative(&self) -> bool { + *self < 0 + } + } + )*) +} +signed_impl!(isize i8 i16 i32 i64 i128); + +macro_rules! unsigned_impl { + ($($t:ty)*) => ($( + impl Abs for $t { + #[inline] + fn abs(&self) -> $t { + *self + } + #[inline] + fn abs_beween(&self, other: &Self) -> Self { + if *self >= *other { + *self - *other + } else { + *other-*self + } + } + #[inline] + fn is_negative(&self) -> bool { + false + } + } + )*) +} +unsigned_impl!(usize u8 u16 u32 u64 u128); diff --git a/src/common/direction.rs b/src/common/direction.rs index 66faeb0..24a4a10 100644 --- a/src/common/direction.rs +++ b/src/common/direction.rs @@ -6,10 +6,10 @@ use Turn::*; #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum Direction { - East, - North, - West, - South, + East = 0, + North = 1, + West = 2, + South = 3, } impl Direction { diff --git a/src/common/mod.rs b/src/common/mod.rs index 9c8f391..c1f39da 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,3 +1,4 @@ +pub mod abs; pub mod area; pub mod direction; pub mod file; diff --git a/src/common/pos2.rs b/src/common/pos2.rs index 1ba2520..5301bc5 100644 --- a/src/common/pos2.rs +++ b/src/common/pos2.rs @@ -1,5 +1,5 @@ use super::direction::Direction; -use super::math::gcd; +use super::{abs::Abs, math::gcd}; use num_traits::{CheckedAdd, CheckedSub, Float, Num, NumCast, Signed, Zero}; use std::fmt; use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub}; @@ -119,7 +119,7 @@ where impl Pos2 where - T: Signed, + T: Num + Abs, { pub fn abs(self) -> T { self.get_x().abs() + self.get_y().abs() @@ -223,10 +223,10 @@ where impl Pos2 where - T: Num + Signed + Copy, + T: Num + Abs + Copy, { pub fn taxicab_between(self, other: Pos2) -> T { - (self - other).abs() + self.x().abs_beween(&other.x()) + self.y().abs_beween(&other.y()) } } diff --git a/src/days/day24/mod.rs b/src/days/day24/mod.rs new file mode 100644 index 0000000..5979d48 --- /dev/null +++ b/src/days/day24/mod.rs @@ -0,0 +1,530 @@ +use std::{ + collections::{BinaryHeap, HashSet}, + str::FromStr, +}; + +use crate::common::{direction::Direction, file::split_lines, math::lcm, pos2::Pos2}; + +use super::template::{DayTrait, ResultType}; +use itertools::Itertools; +use thiserror::Error; + +const DAY_NUMBER: usize = 24; + +pub struct Day; + +impl DayTrait for Day { + fn get_day_number(&self) -> usize { + DAY_NUMBER + } + + fn part1(&self, lines: &str) -> anyhow::Result { + let valley: Valley = lines.parse()?; + Ok(ResultType::Integer(valley.cross(Trip(1))? as i64)) + } + + fn part2(&self, lines: &str) -> anyhow::Result { + let valley: Valley = lines.parse()?; + Ok(ResultType::Integer(valley.cross(Trip(3))? as i64)) + } +} + +#[derive(Debug, Error)] +enum BlizzardError { + #[error("Not a valid description: {0}")] + ParsingError(String), + #[error("Illegal char: {0}")] + IllegalChar(char), + #[error("Need exacly one door. Found: {0}")] + NeedExactlyOneDoor(String), + #[error("Valley has no legal shape")] + ValleyHasIllegalShape, + #[error("No valid path was found")] + NoPathFound, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct Trip(usize); +impl Trip { + #[inline] + pub fn inc(&self) -> Trip { + Trip(self.0 + 1) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct Time(usize); + +impl Time { + #[inline] + pub fn get(&self) -> usize { + self.0 + } + + #[inline] + pub fn inc(&self) -> Time { + Time(self.0 + 1) + } +} + +struct Valley { + height: usize, + entry: usize, + exit: usize, + storm: Storm, +} + +#[derive(Debug)] +struct State { + time: Time, + trip: Trip, + position: Pos2, + start: Pos2, + target: Pos2, +} + +impl Eq for State {} + +impl PartialEq for State { + fn eq(&self, other: &Self) -> bool { + self.time == other.time + } +} + +impl PartialOrd for State { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +impl Ord for State { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.time.cmp(&other.time).reverse() + } +} + +impl State { + fn new(start: Pos2, target: Pos2) -> Self { + Self { + time: Time(0), + trip: Trip(0), + position: start, + start, + target, + } + } + + fn wait(&self) -> Self { + Self { + time: self.time.inc(), + trip: self.trip, + position: self.position, + start: self.start, + target: self.target, + } + } + + fn walk_to(&self, position: Pos2) -> Self { + if position == self.target { + Self { + time: self.time.inc(), + trip: self.trip.inc(), + position, + start: self.target, + target: self.start, + } + } else { + Self { + time: self.time.inc(), + trip: self.trip, + position, + start: self.start, + target: self.target, + } + } + } + + fn next_states(&self, valley: &Valley) -> Vec { + let frame = valley.storm.get(self.time.inc()); + + let mut states = Vec::new(); + let mut dir = Direction::East; + for _ in 0..4 { + if let Some(next_pos) = self.position.check_add(dir) { + if frame.is_empty(&next_pos) || next_pos == self.target { + states.push(self.walk_to(next_pos)) + } + } + dir = dir.turn_left(); + } + if frame.is_empty(&self.position) + || (states.is_empty() && (self.position == self.target || self.position == self.start)) + { + states.push(self.wait()); + } + states + } +} + +impl Valley { + fn doorline(input: &str) -> Result { + let doors = input + .char_indices() + .map(|(col, c)| match c { + '.' => Ok(Some(col)), + '#' => Ok(None), + _ => Err(BlizzardError::IllegalChar(c)), + }) + .filter_map_ok(|item| item) + .collect::, _>>()?; + if doors.len() != 1 { + Err(BlizzardError::NeedExactlyOneDoor(input.to_owned())) + } else { + Ok(doors[0] - 1) + } + } + + fn storm(input: &[&str]) -> Result { + let height = input.len(); + if height == 0 { + return Err(BlizzardError::ValleyHasIllegalShape); + } + let width = input[0].len(); + if width == 0 + || input[0].chars().nth(0) != Some('#') + || input[0].chars().nth(width - 1) != Some('#') + { + return Err(BlizzardError::ValleyHasIllegalShape); + } + if !input + .iter() + .map(|row| { + ( + row.len(), + row.chars().nth(0), + row.chars().nth(row.len() - 1), + ) + }) + .all_equal() + { + return Err(BlizzardError::ValleyHasIllegalShape); + } + + let raw = input + .iter() + .enumerate() + .map(|(y, row)| { + row.char_indices().filter_map(move |(x, c)| match c { + '#' => { + if x != 0 && x != row.len() - 1 { + Some(Err(BlizzardError::IllegalChar('#'))) + } else { + None + } + } + '>' => Some(Ok((Pos2::new(x - 1, y), Direction::East))), + '^' => Some(Ok((Pos2::new(x - 1, y), Direction::North))), + '<' => Some(Ok((Pos2::new(x - 1, y), Direction::West))), + 'v' => Some(Ok((Pos2::new(x - 1, y), Direction::South))), + '.' => None, + _ => Some(Err(BlizzardError::IllegalChar(c))), + }) + }) + .flatten() + .collect::, _>>()?; + + Ok(Blizzards::new(raw, width - 2, height)) + } + + fn cross(&self, trips: Trip) -> Result { + let start = State::new(self.get_entry(), self.get_exit()); + let mut queue = BinaryHeap::new(); + queue.push(start); + let mut seen = HashSet::new(); + let mut advanced = Trip(0); + + while let Some(current) = queue.pop() { + if current.trip == trips { + return Ok(current.time.get()); + } + if advanced > current.trip.inc() { + continue; + } + advanced = advanced.max(current.trip); + + let normalized_time = self.normalize_time(current.time); + let fingerprint = (normalized_time, current.trip, current.position); + if seen.contains(&fingerprint) { + continue; + } + seen.insert(fingerprint); + + queue.extend(current.next_states(&self)); + } + Err(BlizzardError::NoPathFound) + } + + fn get_entry(&self) -> Pos2 { + Pos2::new(self.entry, 0) + } + fn get_exit(&self) -> Pos2 { + Pos2::new(self.exit, self.height + 1) + } + + fn normalize_time(&self, time: Time) -> Time { + Time(time.get() % self.storm.len()) + } +} + +impl FromStr for Valley { + type Err = BlizzardError; + + fn from_str(input: &str) -> Result { + let lines = split_lines(input).collect_vec(); + if lines.len() < 3 { + return Err(BlizzardError::ParsingError(input.to_owned())); + } + let entry = Valley::doorline(lines[0])?; + let exit = Valley::doorline(lines[lines.len() - 1])?; + + let blizzards = Valley::storm(&lines[1..lines.len() - 1])?; + let height = blizzards.height; + + let storm = Storm::new(blizzards); + + Ok(Self { + entry, + exit, + height, + storm, + }) + } +} + +#[derive(Debug, Clone)] +struct BlizzardLine { + direction: Direction, + blizzards: Vec, + len: usize, +} + +impl BlizzardLine { + fn new(direction: Direction, len: usize) -> Self { + BlizzardLine { + direction, + blizzards: vec![], + len, + } + } + + fn iter(&self, pos: usize, time: usize) -> impl Iterator> + '_ { + let time = time % self.len; + self.blizzards + .iter() + .map(move |delta| match self.direction { + Direction::East => Pos2::new((time + *delta) % self.len, pos), + Direction::West => Pos2::new((self.len - time + *delta) % self.len, pos), + Direction::North => Pos2::new(pos, (self.len - time + *delta) % self.len), + Direction::South => Pos2::new(pos, (time + *delta) % self.len), + }) + } + + fn push(&mut self, x: usize) { + self.blizzards.push(x); + } +} + +struct Blizzards { + blizzards: Vec>, + width: usize, + height: usize, + period: usize, +} + +impl Blizzards { + pub fn new(raw: Vec<(Pos2, Direction)>, width: usize, height: usize) -> Self { + let mut blizzards = vec![ + vec![BlizzardLine::new(Direction::East, width); height], + vec![BlizzardLine::new(Direction::North, height); width], + vec![BlizzardLine::new(Direction::West, width); height], + vec![BlizzardLine::new(Direction::South, height); width], + ]; + + for (pos, direction) in raw { + match direction { + Direction::East | Direction::West => { + blizzards[direction as usize][pos.y()].push(pos.x()) + } + Direction::North | Direction::South => { + blizzards[direction as usize][pos.x()].push(pos.y()) + } + } + } + + Self { + blizzards, + width, + height, + period: lcm(width, height), + } + } + + pub fn frame_at_time(&self, time: usize) -> Frame { + let time = time % self.period; + let mut valley = vec![vec![true; self.width]; self.height]; + for _ in 0..4 { + for dir in self.blizzards.iter() { + for (pos, blizzard) in dir.iter().enumerate() { + for item in blizzard.iter(pos, time) { + valley[item.y()][item.x()] = false; + } + } + } + } + Frame(valley) + } +} + +struct Frame(Vec>); + +impl Frame { + fn is_empty(&self, pos: &Pos2) -> bool { + if pos.y() == 0 { + false + } else { + self.0 + .get(pos.y() - 1) + .and_then(|row| row.get(pos.x())) + .copied() + .unwrap_or(false) + } + } +} + +struct Storm(Vec); + +impl Storm { + fn new(blizzards: Blizzards) -> Self { + let mut storm = Vec::with_capacity(blizzards.period); + for time in 0..blizzards.period { + storm.push(blizzards.frame_at_time(time)); + } + Self(storm) + } + + #[inline] + #[allow(dead_code)] + pub fn len(&self) -> usize { + self.0.len() + } + + #[inline] + pub fn get(&self, time: Time) -> &Frame { + let index = time.get() % self.0.len(); + &self.0[index] + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::common::file::read_string; + use anyhow::Result; + + #[test] + fn test_part1() -> Result<()> { + let day = Day {}; + let lines = read_string(day.get_day_number(), "example01.txt")?; + let expected = ResultType::Integer(18); + let result = day.part1(&lines)?; + assert_eq!(result, expected); + + Ok(()) + } + + #[test] + fn test_part2() -> Result<()> { + let day = Day {}; + let lines = read_string(day.get_day_number(), "example01.txt")?; + let expected = ResultType::Integer(54); + let result = day.part2(&lines)?; + assert_eq!(result, expected); + + Ok(()) + } + + #[test] + fn parse() -> Result<()> { + let day = Day {}; + let lines = read_string(day.get_day_number(), "example02.txt")?; + + let valley: Valley = lines.parse()?; + assert_eq!(valley.entry, 0); + assert_eq!(valley.exit, 4); + assert_eq!(valley.height, 5); + assert_eq!(valley.storm.len(), 5); + assert_eq!(valley.get_entry(), Pos2::new(0, 0)); + assert_eq!(valley.get_exit(), Pos2::new(4, 6)); + assert_eq!(valley.storm.get(Time(3)).is_empty(&Pos2::new(3, 2)), false); + + Ok(()) + } + + #[test] + fn parse2() -> Result<()> { + let day = Day {}; + let lines = read_string(day.get_day_number(), "example01.txt")?; + + let valley: Valley = lines.parse()?; + assert_eq!(valley.entry, 0); + assert_eq!(valley.exit, 5); + assert_eq!(valley.height, 4); + assert_eq!(valley.storm.len(), 12); + assert_eq!(valley.get_entry(), Pos2::new(0, 0)); + assert_eq!(valley.get_exit(), Pos2::new(5, 5)); + let expected = vec![ + vec![true, false, false, true, false, true], + vec![false, true, true, false, false, true], + vec![false, false, true, false, false, true], + vec![false, false, true, true, false, false], + ]; + assert_eq!(valley.storm.get(Time(1)).0, expected); + + Ok(()) + } + + #[test] + fn possible_moves() -> Result<()> { + let day = Day {}; + let lines = read_string(day.get_day_number(), "example01.txt")?; + + let valley: Valley = lines.parse()?; + let position = State { + time: Time(17), + trip: Trip(0), + position: Pos2::new(5, 4), + start: Pos2::new(0, 0), + target: Pos2::new(5, 5), + }; + let mut next_states = position.next_states(&valley); + next_states.sort(); + let expected = vec![ + State { + time: Time(18), + trip: Trip(1), + position: Pos2::new(5, 5), + start: Pos2::new(5, 5), + target: Pos2::new(0, 0), + }, + State { + time: Time(18), + trip: Trip(0), + position: Pos2::new(4, 4), + start: Pos2::new(0, 0), + target: Pos2::new(5, 5), + }, + ]; + assert_eq!(next_states, expected); + Ok(()) + } +} diff --git a/src/days/mod.rs b/src/days/mod.rs index 00daa3f..35bfa86 100644 --- a/src/days/mod.rs +++ b/src/days/mod.rs @@ -21,6 +21,7 @@ mod day20; mod day21; mod day22; mod day23; +mod day24; mod template; pub use template::DayTrait; @@ -30,7 +31,7 @@ pub mod day_provider { use super::*; use thiserror::Error; - const MAX_DAY: usize = 23; + const MAX_DAY: usize = 24; pub fn get_day(day_num: usize) -> Result, ProviderError> { match day_num { @@ -57,6 +58,7 @@ pub mod day_provider { 21 => Ok(Box::new(day21::Day)), 22 => Ok(Box::new(day22::Day)), 23 => Ok(Box::new(day23::Day)), + 24 => Ok(Box::new(day24::Day)), _ => Err(ProviderError::InvalidNumber(day_num)), } }