Last active
December 2, 2023 15:23
-
-
Save koivunej/1bb76c4ac8523858f78944b801d9082a to your computer and use it in GitHub Desktop.
aoc 2023 day 02, upping the ante with spanned parsing errors
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::{io::BufRead, ops::Range, str::FromStr}; | |
use broilerplate::InputLines; | |
use spanstuff::{Spanned, SpannedErr}; | |
fn main() { | |
let stdin = std::io::stdin(); | |
let stdin = stdin.lock(); | |
let (sum_of_possible_game_ids, min_dices_power) = match solve(stdin, &[12, 13, 14]) { | |
Ok(Ok(tuple)) => tuple, | |
Ok(Err(parsing)) => { | |
eprint!("Failed to parse input:\n\n{parsing}"); | |
std::process::exit(1); | |
} | |
Err(e) => { | |
eprintln!("Reading stdin failed: {e}"); | |
std::process::exit(1); | |
} | |
}; | |
println!("{sum_of_possible_game_ids}"); | |
println!("{min_dices_power}"); | |
assert_eq!(1867, sum_of_possible_game_ids); | |
assert_eq!(84538, min_dices_power); | |
} | |
fn solve( | |
input: impl BufRead, | |
upper_bounds: &[u64; 3], | |
) -> Result<Result<(u64, u64), ParseError>, std::io::Error> { | |
let mut lines = InputLines::from(input); | |
let mut sum_of_game_ids = 0; | |
let mut min_dices_power = 0; | |
while let Some((lineno, line)) = lines.next()? { | |
let line = line.trim(); | |
let line = Spanned::full(line); | |
match solve_line(line, upper_bounds) { | |
Ok((possible_game_id, game_min_dices_power)) => { | |
if let Some(game_id) = possible_game_id { | |
sum_of_game_ids += game_id.0; | |
} | |
min_dices_power += game_min_dices_power; | |
} | |
Err(e) => return Ok(Err(e.at(lineno, lines.into_last_line()))), | |
} | |
} | |
Ok(Ok((sum_of_game_ids, min_dices_power))) | |
} | |
fn solve_line( | |
line: Spanned<&str>, | |
upper_bounds: &[u64; 3], | |
) -> Result<(Option<GameId>, u64), AnyParseError> { | |
let (game_id, results) = line | |
.split_once_str(": ") | |
.ok_or(AnyParseError::MissingDelimiter)?; | |
let game_id = GameId::try_from(game_id).map_err(AnyParseError::InvalidGameId)?; | |
let game_counts = results | |
.split_str("; ") | |
.map(|round| { | |
round.split_str(", ").map(CountedColor::try_from).try_fold( | |
[0u64; 3], | |
|mut acc, next| { | |
// represent each round of the game as "map" of color => count. I was expecting | |
// there to be multiples of "4 red, 6 red" in real input so I did it like this | |
let (color, count) = next?.into(); | |
acc[color as usize] += count; | |
Ok(acc) | |
}, | |
) | |
}) | |
.try_fold([0u64; 3], |mut acc, next| { | |
let counts = next?; | |
// compress each game into maximum of each color picked, which happened to be the | |
// phase2 result but also the answer to phase1. | |
acc.iter_mut() | |
.zip(counts) | |
.for_each(|(game, round)| *game = (*game).max(round)); | |
Ok(acc) | |
}) | |
// only the CountedColor::try_from was fallible above | |
.map_err(AnyParseError::InvalidResult)?; | |
// under the given ball counts, was this game possible | |
let possible = game_counts | |
.iter() | |
.zip(upper_bounds.iter()) | |
.all(|(game, limit)| game <= limit); | |
let possible_game_id = if possible { Some(game_id) } else { None }; | |
// this took some thinking, because I was quite sure I had the max already calculated | |
let min_dices_power = game_counts.iter().copied().product::<u64>(); | |
Ok((possible_game_id, min_dices_power)) | |
} | |
#[test] | |
fn example() { | |
let input = "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green | |
Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue | |
Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red | |
Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red | |
Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green"; | |
let (sum_of_game_ids, min_dices_power) = solve_str(input).unwrap(); | |
assert_eq!(sum_of_game_ids, 8); | |
assert_eq!(min_dices_power, 2286); | |
} | |
#[test] | |
fn upping_the_ante_error_garbage_line_without_delim() { | |
let input = "Game_1"; | |
let e = solve_str(input).unwrap_err(); | |
assert_eq!(e.lineno, 1); | |
assert_eq!(&e.line, input); | |
assert!( | |
matches!(e.error, AnyParseError::MissingDelimiter), | |
"{:?}", | |
e.error | |
); | |
} | |
#[test] | |
fn upping_the_ante_error_garbage_line_with_bad_game_prefix() { | |
let input = "Gamx 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green"; | |
let e = solve_str(input).unwrap_err(); | |
assert!( | |
matches!( | |
e.error, | |
AnyParseError::InvalidGameId(SpannedErr(InvalidGameId::InvalidPrefix, _)) | |
), | |
"{:?}", | |
e.error | |
); | |
assert_eq!(e.span(), 0..4); | |
let expected = r#" | |
1 | Gamx 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green | |
\--/ | |
Error: invalid game id: invalid prefix | |
"#; | |
assert_eq!(&expected[1..], e.to_string()); | |
} | |
#[test] | |
fn upping_the_ante_error_garbage_line_with_bad_result_without_delimiter() { | |
let input = "Game 1: 3 blue, 4_red; 1 red, 2 green, 6 blue; 2 green"; | |
// 111111111122 | |
// 0123456789012345678901 | |
let e = solve_str(input).unwrap_err(); | |
let expected = r#" | |
1 | Game 1: 3 blue, 4_red; 1 red, 2 green, 6 blue; 2 green | |
\---/ | |
Error: invalid game result: missing delimiter ' ' | |
"#; | |
assert_eq!(&expected[1..], e.to_string()); | |
assert_eq!(e.span(), 16..21); | |
} | |
#[test] | |
fn upping_the_ante_error_garbage_line_with_bad_result_bad_count() { | |
let input = "Game 1: 3 blue, X red; 1 red, 2 green, 6 blue; 2 green"; | |
// 111111111122 | |
// 0123456789012345678901 | |
let e = solve_str(input).unwrap_err(); | |
let expected = r#" | |
1 | Game 1: 3 blue, X red; 1 red, 2 green, 6 blue; 2 green | |
^ | |
Error: invalid game result: invalid count: invalid digit found in string | |
"#; | |
assert_eq!(&expected[1..], e.to_string()); | |
assert_eq!(e.span(), 16..17); | |
} | |
#[test] | |
fn upping_the_ante_error_garbage_line_with_bad_result_bad_color() { | |
let input = "Game 1: 3 blue, 4 rex; 1 red, 2 green, 6 blue; 2 green"; | |
// 111111111122 | |
// 0123456789012345678901 | |
let e = solve_str(input).unwrap_err(); | |
let expected = r#" | |
1 | Game 1: 3 blue, 4 rex; 1 red, 2 green, 6 blue; 2 green | |
\-/ | |
Error: invalid game result: invalid color | |
"#; | |
assert_eq!(&expected[1..], e.to_string()); | |
assert_eq!(e.span(), 18..21); | |
} | |
#[cfg(test)] | |
fn solve_str(s: &str) -> Result<(u64, u64), ParseError> { | |
solve(std::io::Cursor::new(s), &[12, 13, 14]).unwrap() | |
} | |
#[derive(Debug)] | |
struct ParseError { | |
line: String, | |
lineno: usize, | |
error: AnyParseError, | |
} | |
impl std::fmt::Display for ParseError { | |
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
spanstuff::pretty_fmt(self.lineno, &self.line, self.span(), &self.error, f) | |
} | |
} | |
#[derive(Debug, thiserror::Error)] | |
enum AnyParseError { | |
#[error("missing delimiter \": \"")] | |
MissingDelimiter, | |
#[error("invalid game id")] | |
InvalidGameId(#[source] SpannedErr<InvalidGameId>), | |
#[error("invalid game result")] | |
InvalidResult(#[source] SpannedErr<InvalidCountedColor>), | |
} | |
impl ParseError { | |
fn span(&self) -> Range<usize> { | |
use AnyParseError::*; | |
match &self.error { | |
MissingDelimiter => 0..self.line.len(), | |
InvalidGameId(SpannedErr(_, span)) | InvalidResult(SpannedErr(_, span)) => { | |
span.start..span.end | |
} | |
} | |
} | |
} | |
impl AnyParseError { | |
fn at(self, lineno: usize, line: String) -> ParseError { | |
ParseError { | |
line, | |
lineno, | |
error: self, | |
} | |
} | |
} | |
#[repr(u8)] | |
enum Color { | |
Red = 0, | |
Green, | |
Blue, | |
} | |
#[derive(Debug)] | |
struct InvalidColor; | |
impl FromStr for Color { | |
type Err = InvalidColor; | |
fn from_str(s: &str) -> Result<Self, Self::Err> { | |
Ok(match s { | |
"red" => Color::Red, | |
"green" => Color::Green, | |
"blue" => Color::Blue, | |
_ => return Err(InvalidColor), | |
}) | |
} | |
} | |
struct CountedColor(Color, u64); | |
#[derive(Debug, thiserror::Error)] | |
enum InvalidCountedColor { | |
#[error("missing delimiter ' '")] | |
MissingDelimiter, | |
#[error("invalid count")] | |
InvalidCount(#[source] std::num::ParseIntError), | |
#[error("invalid color")] | |
InvalidColor(InvalidColor), | |
} | |
impl<'a> TryFrom<Spanned<&'a str>> for CountedColor { | |
type Error = SpannedErr<InvalidCountedColor>; | |
fn try_from(s: Spanned<&'a str>) -> Result<Self, Self::Error> { | |
use InvalidCountedColor::*; | |
let Some((count, color)) = s.split_once_char(' ') else { | |
return Err(s.with_span(MissingDelimiter)) | |
}; | |
let count = count | |
.parse::<u64>() | |
.map_err(|e| e.map(InvalidCount))? | |
.into_inner(); | |
let color = color | |
.parse::<Color>() | |
.map_err(|e| e.map(InvalidColor))? | |
.into_inner(); | |
Ok(CountedColor(color, count)) | |
} | |
} | |
impl From<CountedColor> for (Color, u64) { | |
fn from(value: CountedColor) -> Self { | |
(value.0, value.1) | |
} | |
} | |
struct GameId(u64); | |
#[derive(Debug, thiserror::Error)] | |
enum InvalidGameId { | |
#[error("missing delimiter ' '")] | |
MissingDelimiter, | |
#[error("invalid prefix")] | |
InvalidPrefix, | |
#[error("bad game id")] | |
BadGameId(#[source] std::num::ParseIntError), | |
} | |
impl<'a> TryFrom<Spanned<&'a str>> for GameId { | |
type Error = SpannedErr<InvalidGameId>; | |
fn try_from(s: Spanned<&'a str>) -> Result<Self, Self::Error> { | |
use InvalidGameId::*; | |
let Some((game, game_id)) = s.split_once_char(' ') else { | |
return Err(s.with_span(MissingDelimiter)); | |
}; | |
let (game, span) = game.into(); | |
if game != "Game" { | |
return Err(SpannedErr(InvalidPrefix, span)); | |
} | |
let gi = game_id | |
.parse::<u64>() | |
.map_err(|e| e.map(BadGameId))? | |
.into_inner(); | |
Ok(GameId(gi)) | |
} | |
} | |
mod spanstuff { | |
use std::ops::Range; | |
use std::str::FromStr; | |
pub trait Span: Sized + std::fmt::Debug { | |
fn split(&self, at_offset: usize) -> (Self, Self); | |
fn len(&self) -> Option<usize>; | |
} | |
impl Span for Range<usize> { | |
fn split(&self, at_offset: usize) -> (Self, Self) { | |
let start = self.start; | |
let left = start..(start + at_offset); | |
let right = left.end..self.end; | |
assert!(left.start <= left.end, "{} <= {}", left.start, left.end); | |
assert!(left.end <= right.start, "{} <= {}", left.end, right.start); | |
assert!(right.start <= right.end, "{} <= {}", right.start, right.end); | |
assert_eq!(left.start, self.start); | |
assert_eq!(right.end, self.end); | |
(left, right) | |
} | |
fn len(&self) -> Option<usize> { | |
self.end.checked_sub(self.start) | |
} | |
} | |
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] | |
pub struct Spanned<T, S = Range<usize>> { | |
inner: T, | |
span: S, | |
} | |
impl<T, S> Spanned<T, S> { | |
pub fn into_inner(self) -> T { | |
self.inner | |
} | |
pub fn with_span<E>(self, err: E) -> SpannedErr<E, S> { | |
SpannedErr(err, self.span) | |
} | |
} | |
impl<T, S> From<Spanned<T, S>> for (T, S) { | |
fn from(value: Spanned<T, S>) -> Self { | |
(value.inner, value.span) | |
} | |
} | |
impl<'a, Span> Spanned<&'a str, Span> { | |
pub fn parse<T: FromStr>( | |
self, | |
) -> Result<Spanned<T, Span>, SpannedErr<<T as FromStr>::Err, Span>> { | |
match self.inner.parse::<T>() { | |
Ok(t) => Ok(Spanned { | |
inner: t, | |
span: self.span, | |
}), | |
Err(e) => Err(SpannedErr(e, self.span)), | |
} | |
} | |
} | |
impl<'a> Spanned<&'a str, Range<usize>> { | |
pub fn full(input: &'a str) -> Self { | |
Spanned { | |
inner: input, | |
span: 0..input.len(), | |
} | |
} | |
} | |
/// Transparent error, which just carries the span. | |
#[derive(Debug)] | |
pub struct SpannedErr<E, S = Range<usize>>(pub E, pub S); | |
impl<E: std::fmt::Display, Span> std::fmt::Display for SpannedErr<E, Span> { | |
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
self.0.fmt(f) | |
} | |
} | |
impl<E: std::error::Error, Span: std::fmt::Debug> std::error::Error for SpannedErr<E, Span> { | |
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { | |
self.0.source() | |
} | |
} | |
impl<E, S> SpannedErr<E, S> { | |
// this is quite unfortunate | |
pub fn map<F, E2>(self, map: F) -> SpannedErr<E2, S> | |
where | |
F: FnOnce(E) -> E2, | |
{ | |
SpannedErr(map(self.0), self.1) | |
} | |
} | |
impl<'a, S: Span> Spanned<&'a str, S> { | |
pub fn split_once_char(&'a self, delimiter: char) -> Option<(Self, Self)> { | |
let (left, right) = self.inner.split_once(delimiter)?; | |
self._reconstruct_split_once(left, right) | |
} | |
pub fn split_once_str(&'a self, delimiter: &str) -> Option<(Self, Self)> { | |
let (left, right) = self.inner.split_once(delimiter)?; | |
self._reconstruct_split_once(left, right) | |
} | |
fn _reconstruct_split_once(&self, left: &'a str, right: &'a str) -> Option<(Self, Self)> { | |
let (left_span, rem) = self.span.split(left.len()); | |
assert_ne!(rem.len().expect("we only create valid spans"), 0); | |
let (_, right_span) = rem.split(self.inner.len() - left.len() - right.len()); | |
let left = Spanned { | |
inner: left, | |
span: left_span, | |
}; | |
let right = Spanned { | |
inner: right, | |
span: right_span, | |
}; | |
Some((left, right)) | |
} | |
pub fn split_str(self, delimiter: &'a str) -> SplitStr<'a, S> { | |
SplitStr { | |
inner: self.inner.split(delimiter), | |
span: self.span, | |
delimiter_len: delimiter.len(), | |
} | |
} | |
} | |
pub struct SplitStr<'a, S> { | |
inner: std::str::Split<'a, &'a str>, | |
span: S, | |
delimiter_len: usize, | |
} | |
impl<'a, S: Span> Iterator for SplitStr<'a, S> { | |
type Item = Spanned<<std::str::Split<'a, &'a str> as Iterator>::Item, S>; | |
fn next(&mut self) -> Option<Self::Item> { | |
let next = self.inner.next()?; | |
let (next_span, rem) = self.span.split(next.len()); | |
self.span = if rem.len().is_some_and(|x| x > 0) { | |
// avoid going over and creating an invalid span, even if it doesn't really matter | |
let (_delimiter, rem) = rem.split(self.delimiter_len); | |
rem | |
} else { | |
rem | |
}; | |
Some(Spanned { | |
inner: next, | |
span: next_span, | |
}) | |
} | |
} | |
pub fn pretty_fmt( | |
lineno: usize, | |
line: &str, | |
span: Range<usize>, | |
error: &(dyn std::error::Error + 'static), | |
f: &mut std::fmt::Formatter<'_>, | |
) -> std::fmt::Result { | |
writeln!(f, "{lineno:>3} | {}", line.trim())?; | |
write!(f, "{:>3} ", " ")?; | |
for _ in 0..span.start { | |
write!(f, " ")?; | |
} | |
match span.end.checked_sub(span.start) { | |
None | Some(0 | 1) => { | |
writeln!(f, "^") | |
} | |
Some(2) => { | |
writeln!(f, "^^") | |
} | |
Some(_more) => { | |
write!(f, "\\")?; | |
for _ in 0..(span.end - span.start - 2) { | |
write!(f, "-")?; | |
} | |
writeln!(f, "/") | |
} | |
}?; | |
write!(f, "Error: ")?; | |
let mut next: Option<&(dyn std::error::Error + 'static)> = Some(error); | |
while let Some(cur) = next.take() { | |
write!(f, "{cur}")?; | |
if let Some(nextnext) = cur.source() { | |
write!(f, ": ")?; | |
next = Some(nextnext); | |
} | |
} | |
writeln!(f) | |
} | |
#[cfg(test)] | |
mod tests { | |
use super::*; | |
#[test] | |
fn spanned_full() { | |
let input = Spanned::full("hello world"); | |
// 1 | |
// 01234567890 | |
assert_eq!(input.span, 0..11); | |
} | |
#[test] | |
fn spanned_split_once_char() { | |
let input = Spanned::full("hello world"); | |
// 1 | |
// 01234567890 | |
let (hello, world) = input.split_once_char(' ').unwrap(); | |
assert_eq!(hello.span, 0..5); | |
assert_eq!(Span::len(&hello.span), Some(5)); | |
assert_eq!(world.span, 6..11); | |
assert_eq!(Span::len(&world.span), Some(5)); | |
} | |
#[test] | |
fn spanned_split_once_str() { | |
let input = Spanned::full("hello world"); | |
// 1 | |
// 01234567890 | |
let (hello, orld) = input.split_once_str(" w").unwrap(); | |
assert_eq!(hello.span, 0..5); | |
assert_eq!(Span::len(&hello.span), Some(5)); | |
assert_eq!(orld.span, 7..11); | |
assert_eq!(Span::len(&orld.span), Some(4)); | |
} | |
#[test] | |
fn spanned_split_many() { | |
let orig = "1, 2, 3, "; | |
// 012345689 | |
let input = Spanned::full(orig); | |
let mut split = input.split_str(", "); | |
let next = split.next().unwrap(); | |
assert_eq!(next.inner, "1"); | |
assert_eq!(next.span, 0..1); | |
assert_eq!(Span::len(&next.span), Some(1)); | |
let next = split.next().unwrap(); | |
assert_eq!(next.inner, "2"); | |
assert_eq!(next.span, 3..4); | |
assert_eq!(Span::len(&next.span), Some(1)); | |
let next = split.next().unwrap(); | |
assert_eq!(next.inner, "3"); | |
assert_eq!(next.span, 6..7); | |
assert_eq!(Span::len(&next.span), Some(1)); | |
let next = split.next().unwrap(); | |
assert_eq!(next.inner, ""); | |
assert_eq!(next.span, 9..9); | |
assert_eq!(Span::len(&next.span), Some(0)); | |
let next = split.next(); | |
assert!(next.is_none()); | |
} | |
} | |
} | |
mod broilerplate { | |
use std::io::BufRead; | |
pub struct InputLines<I> { | |
inner: I, | |
buf: String, | |
lineno: usize, | |
} | |
impl<I: BufRead> From<I> for InputLines<I> { | |
fn from(inner: I) -> Self { | |
InputLines { | |
inner, | |
buf: String::new(), | |
lineno: 0, | |
} | |
} | |
} | |
impl<I: BufRead> InputLines<I> { | |
pub fn next(&mut self) -> Result<Option<(usize, &str)>, std::io::Error> { | |
self.lineno += 1; | |
self.buf.clear(); | |
let read = self.inner.read_line(&mut self.buf)?; | |
if read == 0 { | |
return Ok(None); | |
} | |
Ok(Some((self.lineno, self.buf.as_str()))) | |
} | |
pub fn into_last_line(self) -> String { | |
self.buf | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment