Add another approach, make setup more elegant

This commit is contained in:
2023-12-02 00:08:25 +01:00
parent 93af7971e4
commit 96c71f3f93

View File

@@ -1,29 +1,4 @@
#[derive(Debug)] struct NumberPair(Option<u32>, Option<u32>);
struct FirstLast {
first: Option<u32>,
last: Option<u32>,
}
impl FirstLast {
fn new() -> Self {
Self {
first: None,
last: None,
}
}
fn record(&mut self, n: u32) {
if self.first.is_none() {
self.first = Some(n)
} else {
self.last = Some(n)
}
}
fn value(self) -> u32 {
self.first.unwrap() * 10 + self.last.unwrap_or(self.first.unwrap())
}
}
struct SpelledOutNumber(u32); struct SpelledOutNumber(u32);
@@ -54,84 +29,143 @@ impl SpelledOutNumber {
} }
} }
fn try_parse_at(input: &[char]) -> Option<u32> {
if let Some(digit) = input[0].to_digit(10) {
Some(digit)
} else {
SpelledOutNumber::parse(&input[0..input.len()]).map(|s| s.0)
}
}
fn main() { fn main() {
let input = std::fs::read_to_string("./input").unwrap(); let input = std::fs::read_to_string("./input").unwrap();
let approaches: Vec<u32> = vec![ let approaches: Vec<Box<dyn Fn(&[char]) -> NumberPair>> = vec![
// go through the string one by one, check the value at that position and record it into // go through the string one by one, check the value at that position and record it into
// a `FirstLast` recorder that holds state // a `FirstLast` recorder that holds state
input Box::new(|line| {
.lines() #[derive(Debug)]
.map(|line| { struct Recorder {
let line: Vec<char> = line.chars().collect(); first: Option<u32>,
let mut recorder = FirstLast::new(); last: Option<u32>,
for i in 0..line.len() { }
let c = &line[i];
if let Some(digit) = c.to_digit(10) { impl Recorder {
recorder.record(digit) fn new() -> Self {
} else { Self {
if let Some(digit) = SpelledOutNumber::parse(&line[i..]) { first: None,
recorder.record(digit.0) last: None,
}
} }
} }
recorder
}) fn record(&mut self, n: u32) {
.map(FirstLast::value) if self.first.is_none() {
.sum(), self.first = Some(n)
} else {
self.last = Some(n)
}
}
fn finish(self) -> NumberPair {
NumberPair(self.first, self.last)
}
}
let mut recorder = Recorder::new();
for i in 0..line.len() {
let c = &line[i];
if let Some(digit) = c.to_digit(10) {
recorder.record(digit)
} else {
if let Some(digit) = SpelledOutNumber::parse(&line[i..]) {
recorder.record(digit.0)
}
}
}
recorder.finish()
}),
// //
// go through the string one by one, transform it into an array of digits and go from there // Go through the string one by one, transform it into an array of digits and go from there
// i prefer this approach, as there is no stateful iteration // I prefer this approach, as there is no stateful iteration and it's very easy to understand
input Box::new(|line| {
.lines() let result = (0..line.len())
.map(|line| line.chars().collect::<Vec<char>>()) .map(move |pos| try_parse_at(&line[pos..line.len()]))
.map(|line| { // remove none values
(0..line.len()) .filter_map(|e| e)
.map(move |pos| { .collect::<Vec<u32>>();
if let Some(digit) = line[pos].to_digit(10) {
Some(digit)
} else {
SpelledOutNumber::parse(&line[pos..line.len()]).map(|s| s.0)
}
})
.filter_map(|e| e)
.collect::<Vec<u32>>()
})
// peculiar: if there is only one digit, use it for both the tenths digit and and ones digit // peculiar: if there is only one digit, use it for both the tenths digit and and ones digit
.map(|result| (result[0], *result.last().unwrap_or(&result[0]))) NumberPair(result.get(0).copied(), result.last().copied())
.map(|(d1, d2)| d1 * 10 + d2) }),
.sum(), //
// this one does two scans, one from each end. it's elegant because it does not require special
// handling for lines containing only one digit, i.e. it will *always* return (Some, Some)
Box::new(|line| {
NumberPair(
{
let mut tenth = None;
for pos in 0..line.len() {
if let Some(digit) = try_parse_at(&line[pos..line.len()]) {
tenth = Some(digit);
break;
}
}
tenth
},
{
let mut ones = None;
for pos in (0..line.len()).rev() {
if let Some(digit) = try_parse_at(&line[pos..line.len()]) {
ones = Some(digit);
break;
}
}
ones
},
)
}),
]; ];
enum Acc { let result: u32 = input
Init, .lines()
Some(u32), .map(|line| line.chars().collect::<Vec<char>>())
} .map(|line| {
(
impl Acc { approaches
fn unwrap(self) -> u32 { .iter()
match self { .map(|approach| approach(&line))
Self::Init => panic!("Accumulator did not contain a number"), .map(|pair| {
Self::Some(val) => val, (
} // we assume that there will *always* at least be one numbers in there
} pair.0.unwrap(),
} // if there is no second number, we "reuse" the first. so "7" => 77
pair.1.or(pair.0).unwrap(),
// check that all approaches result in the same result )
let result = approaches })
.iter() .collect::<Vec<(u32, u32)>>(),
// try_reduce would be nicer here line,
.try_fold(Acc::Init, |acc, value| { )
if let Acc::Some(acc) = acc {
if acc != *value {
return Err("approaches give different results");
}
};
Ok(Acc::Some(*value))
}) })
.unwrap() // check that there were no different results .map(|(results, line)| {
.unwrap() // check that there were results at all (cannot fail) // check that all approaches result in the same result
; let result = results[0];
let mut all_agree = true;
for other_result in &results[1..] {
if *other_result != result {
all_agree = false;
}
}
if !all_agree {
eprintln!("approaches yield different results!");
eprintln!("input: {}", line.iter().collect::<String>());
eprintln!("results: {results:?}");
}
results[0]
})
.map(|(tenth, ones)| tenth * 10 + ones)
.sum();
println!("{result}"); println!("{result}");
} }