Day 1
Day 1 is mostly related to extracting numbers from a stream of chars.
Part One
This is a simple filter, iterating over the chars and getting those that can be parsed to digits.
#![allow(unused)] fn main() { use anyhow::{anyhow, Result}; fn parse_line(line: &str) -> Result<u32> { let mut numbers = line.chars().filter_map(|c| c.to_digit(10)).peekable(); let first = *numbers .peek() .ok_or(anyhow!("Line {line} missing a number."))?; let last = numbers .last() .ok_or(anyhow!("Line {line} missing a number."))?; Ok(10 * first + last) } }
The only way to mess up the first part would be to do something like filtering out
all values between the first and the last, because then things like akjsdk3ksdj
would
not become "33", but only "3" instead. But most organic solutions will naturally
deal with this edge case.
Part Two
This part is more problematic than it appears. The first immediate thought is just
replacing the spelled out names with the numbers, e.g. onelvjxcthree
-> 1lvjxc3
,
and then proceeding with the first part.
However, this fails due to the main friction of part two: characters can be reused
for different numbers, e.g. sevenine
becomes 79
.
So, the initial idea that I had was to replace the spelled out number with the spelled
out number + the number itself. It wasn't apparently clear why it didn't work, but
the very same example above should make it obvious: sevenine
would become
seven7ine
in the first replacement, which would not generate the nine. Curiously,
if 9 came before seven in the iteration of the replacemens, there would be no problem!
Then next idea would be to maybe put the number in the middle of the word, but that has some problems:
- It relies fundamentally in happenstances of the English language which might have to result in refactorings in case we switched the language;
- It creates new Strings every time we replace, with needless memory allocation.
Instead of doing that, I just ran a window through the string, capturing if it starts
with a number, and pushed the numbers coming from those windows to one single Vec
.
If the window began with a char that is a number, I'd also add that to the Vec
.
#![allow(unused)] fn main() { fn transform_line(line: &str) -> Vec<u32> { let mut numbers = Vec::new(); // Adding gibberish at the end just to guarantee comfortable buffering. let fake_line = line.to_string() + "AAAAA"; for i in 0..line.len() { for (j, spelled_out_number) in SPELLED_OUT_NUMBERS.iter().enumerate() { if fake_line[i..(i + 5)].starts_with(spelled_out_number) { numbers.push(j as u32 + 1); break; } } if let Some(x) = fake_line[i..(i + 5)] .chars() .next() .expect("Should not be empty given that it has AAAAA at the end") .to_digit(10) { numbers.push(x); } } numbers } }
This solves the problem, but let's make a change before moving on: the whole AAAAA
addition together with windows of length five is completely unnecessary, of course.
Also, using windows of length five couples our solution to english as well;
indeed, "quatro", which is "four" in Portuguese, already would violate that.
#![allow(unused)] fn main() { fn transform_line(line: &str) -> Vec<u32> { let mut numbers = Vec::new(); for i in 0..line.len() { for (j, spelled_out_number) in SPELLED_OUT_NUMBERS.iter().enumerate() { if line[i..].starts_with(spelled_out_number) { numbers.push(j as u32 + 1); break; } } if let Some(x) = line[i..] .chars() .next() .expect("Should not be empty given that i < line.len()") .to_digit(10) { numbers.push(x); } } numbers } }
Note that even though we are slicing for the whole rest of the
&str
, this does not give any performance deficit: we are dealing with references andstarts_with
only iterates on what it needs. Getting the first char also only goes into the very beginning of the slice.