Skip to content

Instantly share code, notes, and snippets.

@koivunej
Last active December 2, 2023 15:23
Show Gist options
  • Save koivunej/1bb76c4ac8523858f78944b801d9082a to your computer and use it in GitHub Desktop.
Save koivunej/1bb76c4ac8523858f78944b801d9082a to your computer and use it in GitHub Desktop.
aoc 2023 day 02, upping the ante with spanned parsing errors
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