We’re given lines of text where each line contains digits, either as literal numeric characters (“1”, “2”, etc.) or as spelled-out words (“one”, “two”, etc.). We need to extract the first and last digits from each line to form a two-digit number. If there’s only one digit in a line, it’s used as both the first and last digit.
two1nine
eightwothree
abcone2threexyz
xtwone3four
4nineeightseven2
zoneight234
7pqrstsixtee
29
83
13
24
42
14
76
Note the special case: Words can overlap. For example, in threeightwo
, we identify “three”, “eight”, and “two”, resulting in 32.
To solve this problem, we need to:
For part two, we encounter an additional challenge: recognizing spelled-out digits like “one”, “two”, etc.
Let’s start by defining a trait that provides a consistent interface for our parsers:
trait Parse {
fn parser<'a>(&self, inp: &'a str) -> impl Iterator<Item = u32> + 'a;
}
This abstraction allows us to implement different parsing strategies while maintaining a consistent interface - a classic application of the Strategy pattern.
For Part 1, we only need to consider numeric digits:
struct ParserDigits;
impl Parse for ParserDigits {
fn parser<'a>(&self, inp: &'a str) -> impl Iterator<Item = u32> + 'a {
inp.chars()
.filter(|c| c.is_ascii_digit())
.map(|c| (c as u8 - b'0') as u32)
}
}
This implementation:
Now we can create a function that takes a parsing strategy and computes the sum:
fn sum_up(inp: &str, p: impl Parse) -> u32 {
inp.lines()
.filter_map(|line| {
let mut iter = p.parser(line);
iter.next().map(|f| 10*f + iter.last().unwrap_or(f))
})
.sum::<u32>()
}
This function:
For Part 2, we need to recognize both numeric digits and spelled-out digits:
struct ParserNumerics;
impl Parse for ParserNumerics {
fn parser<'a>(&self, input: &'a str) -> impl Iterator<Item = u32> + 'a {
static DIGITS: [(&str, u32); 9] = [
("one", 1), ("two", 2), ("three", 3), ("four", 4), ("five", 5),
("six", 6), ("seven", 7), ("eight", 8), ("nine", 9)
];
let mut buf = String::with_capacity(60);
input.chars()
.filter_map(move |c| {
match c {
'0'..='9' => Some((c as u8 - b'0') as u32),
'a'..='z' => {
buf.push(c);
DIGITS.iter()
.filter_map(|(d, numeric)|
if !buf.ends_with(d) { None } else { Some(*numeric) }
)
.next()
},
_ => None
}
})
}
}
This implementation:
The key insight here is how we handle overlapping words by checking if any digit word ends with the current buffer, rather than checking if the buffer equals a digit word. This allows us to detect things like “oneight” as both “one” and “eight”.
Finally, we can run both parts:
fn main() {
let inp = std::fs::read_to_string("src/bin/day1/input.txt")
.unwrap_or_else(|e| panic!("{e}"));
let t = Instant::now();
println!("Part 1 -> Sum = {:?} - {:?}",
sum_up(&inp, ParserDigits), t.elapsed());
let t = Instant::now();
println!("Part 2 -> Sum = {:?} - {:?}",
sum_up(&inp, ParserNumerics), t.elapsed());
}
This measures the execution time of each part, showing not only the correctness but also the efficiency of our solutions.
Parse
trait, we created a flexible interface for different parsing strategies.This problem demonstrates how appropriate abstractions and data structures can make complex parsing tasks straightforward and maintainable.