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:
- Parse each line of input and identify all occurrences of digits.
- Extract the first and last digit to form a two-digit number.
- Sum all these two-digit numbers to get the final result.
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:
- Iterates through each character
- Filters out non-digit characters
- Converts ASCII digits to numerical values
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:
- Processes each line of input
- Uses our parser to extract digits
- Takes the first digit and multiplies it by 10 (to make it the tens place)
- Adds the last digit (or the first digit again if there's only one)
- Sums all the resulting values
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:
- Creates a lookup table for spelled-out digits
- Maintains a buffer that accumulates characters
- For each character:
- If it's a numeric digit, converts it directly
- If it's a letter, adds it to the buffer and checks if any spelled-out digit ends at this point
- Returns the digit if found, otherwise None
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.
- Abstraction through Traits: By defining the
Parse
trait, we created a flexible interface for different parsing strategies. - Functional Programming: Using iterators and closures makes our code concise and expressive.
- Buffer Management: For Part 2, we maintain a buffer to detect spelled-out digits, handling overlapping cases elegantly.
- Performance Consideration: The code includes timing measurements, showing a focus on efficiency.
This problem demonstrates how appropriate abstractions and data structures can make complex parsing tasks straightforward and maintainable.