use crate::puzzle::Part;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Answers {
pub part1: String,
pub part2: Option<String>,
}
impl Answers {
pub fn iter(&self) -> impl Iterator<Item = (Part, &str)> {
std::iter::once((Part::One, self.part1.as_str()))
.chain(self.part2.as_deref().map(|answer| (Part::Two, answer)))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outcome {
Answers(Answers),
Raw,
}
#[must_use]
pub fn classify(stdout: &str) -> Outcome {
let body = stdout.strip_suffix('\n').unwrap_or(stdout);
if body.is_empty() {
return Outcome::Raw;
}
let mut lines = body.split('\n').map(str::trim);
let (Some(part1), part2, None) = (lines.next(), lines.next(), lines.next()) else {
return Outcome::Raw;
};
if part1.is_empty() || part2.is_some_and(str::is_empty) {
return Outcome::Raw;
}
Outcome::Answers(Answers {
part1: part1.to_owned(),
part2: part2.map(ToOwned::to_owned),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn answers(stdout: &str) -> Option<Answers> {
match classify(stdout) {
Outcome::Answers(answers) => Some(answers),
Outcome::Raw => None,
}
}
fn parts(stdout: &str) -> Option<(String, Option<String>)> {
answers(stdout).map(|a| (a.part1, a.part2))
}
#[test]
fn one_line_is_part_one() {
assert_eq!(parts("42\n"), Some(("42".to_owned(), None)));
}
#[test]
fn two_lines_are_both_parts() {
assert_eq!(
parts("42\n99\n"),
Some(("42".to_owned(), Some("99".to_owned())))
);
}
#[test]
fn a_missing_trailing_newline_still_yields_both_parts() {
assert_eq!(
parts("42\n99"),
Some(("42".to_owned(), Some("99".to_owned())))
);
assert_eq!(parts("42"), Some(("42".to_owned(), None)));
}
#[test]
fn multibyte_output_splits_on_character_boundaries() {
assert_eq!(
parts("ä→é\n42\n"),
Some(("ä→é".to_owned(), Some("42".to_owned())))
);
}
#[test]
fn surrounding_whitespace_is_trimmed() {
assert_eq!(
parts(" 42 \n\t99\t\n"),
Some(("42".to_owned(), Some("99".to_owned())))
);
}
#[test]
fn carriage_returns_are_trimmed() {
assert_eq!(
parts("42\r\n99\r\n"),
Some(("42".to_owned(), Some("99".to_owned())))
);
}
#[test]
fn three_or_more_lines_are_raw_output() {
assert_eq!(classify("1\n2\n3\n"), Outcome::Raw);
assert_eq!(classify("a\nb\nc\nd\n"), Outcome::Raw);
}
#[test]
fn empty_and_blank_output_is_raw() {
assert_eq!(classify(""), Outcome::Raw);
assert_eq!(classify("\n"), Outcome::Raw);
assert_eq!(classify(" \n"), Outcome::Raw);
assert_eq!(classify("42\n\n"), Outcome::Raw);
assert_eq!(classify("\n42\n"), Outcome::Raw);
}
#[test]
fn iterates_parts_in_order() {
let answers = Answers {
part1: "a".to_owned(),
part2: Some("b".to_owned()),
};
let collected: Vec<_> = answers.iter().collect();
assert_eq!(collected, [(Part::One, "a"), (Part::Two, "b")]);
let single = Answers {
part1: "a".to_owned(),
part2: None,
};
assert_eq!(single.iter().count(), 1);
}
}