After being bitten by day two, I was definitely wary of day three. We get a nasty string of what appears to be functions. Upon reading through the instructions, it seems relatively clear: we search for the pattern mul
, which should be immediately followed by two numbers separated by a comma, surrounded by brackets. The task is simple: multiply the two numbers together and sum all the results. Ignore any malformed functions, such as those with missing brackets, misplaced spaces, or missing numbers.
Looking at our input, there is quite a lot of noise. It seems like part two could be nasty, but let's start, as I always do, with a test.
#[cfg(test)]
mod tests {
use super::*;
use crate::DefaultOutputSignal;
#[test]
fn test_solve_part_one() {
let input = String::from(
r#"xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))"#,
);
let mullet = Mullet {
output_signal: Box::new(DefaultOutputSignal),
};
let result = mullet.solve_part_one(input);
assert_eq!(result, Ok("161".to_string()));
}
}
No doubt, many of you are thinking, "That's a regex problem!",and you could be right. But I have two concerns. Firstly, I have been bitten by regex in Advent of Code before. This feels like a trap. Secondly, I am compiling my solution into my blog, which is written in Rust with Leptos and compiled to WebAssembly. While a regex solution would work, it would be expensive in terms of binary size. The first suggestion of what to avoid in Leptos is regex, as noted here. Currently, my blog's WebAssembly binary is roughly 800 KB. Adding regex would push that to 1.3 MB. I do not want that, so no regex for me.
So, the approach: instead of trying to process the entire string at once, let's focus on just the start of the string. As we process it, we find the next substring, drop the start, and continue processing until we have no more string left. It's a brute-force approach, but it's simple, it works, and I suspect it will make part two easier. Let's begin:
mod part_one {
use crate::utils::split_multiline_string;
use crate::y2024::day3::Mullet;
impl Mullet {
pub fn part_one_process(&self, input: String) -> Result<String, ()> {
let input_as_lines = split_multiline_string(input);
let mut sum = 0;
input_as_lines.iter().for_each(|line| {
let mut substr = line.clone();
while substr.len() > 0 {
if substr.starts_with("mul(") {
let start = substr.find("(").unwrap() + 1;
let comma = substr.find(",").unwrap();
let end = substr.find(")").unwrap();
let left_str = &substr[start..comma];
let left = left_str.parse::<i32>();
if comma < end {
let right_str = &substr[comma + 1..end];
let right = right_str.parse::<i32>();
if left.is_ok() && right.is_ok() {
sum += left.unwrap() * right.unwrap();
}
}
}
substr = substr[1..].to_string();
}
});
Ok(sum.to_string())
}
}
}
Essentially, all I am doing here is iterating over the input, checking if it starts with our expected mul
. If it does not, then we continue to the next character through a substring. If it does, we find the key elements we care about (two numbers, a comma, and two brackets) and try to parse the numbers. If they do not parse, we know it is bad input, and we can move on. Finally, we sum the results. It is super simple, super effective, and it works. So, let us move on to part two.
Part two is more or less the same, but now we care about three patterns: mul
, do()
, and don't()
. The do()
and don't()
patterns tell us whether we should process mul
. If we see don't()
, we should not process any mul
until we see do()
. Essentially, it acts as a flag. The great thing is that our current solution is 95% of the way there. All we need is a flag and two more checks. As before, let's start with a test. Note that the test input has also changed, so do not let that catch you off guard.
#[cfg(test)]
mod tests {
...
#[test]
fn test_solve_part_two() {
let input = String::from(
r#"xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))"#,
);
let mullet = Mullet {
output_signal: Box::new(DefaultOutputSignal),
};
let result = mullet.solve_part_two(input);
assert_eq!(result, Ok("48".to_string()));
}
}
Now that we have our test, let's adjust our implementation to support these new requirements.
mod part_two {
use crate::utils::split_multiline_string;
use crate::y2024::day3::Mullet;
impl Mullet {
pub fn part_two_process(&self, input: String) -> Result<String, ()> {
let input_as_lines = split_multiline_string(input);
let mut sum = 0;
let mut last_mutex = true;
input_as_lines.iter().for_each(|line| {
let mut substr = line.clone();
while substr.len() > 0 {
if substr.starts_with("mul(") {
let start = substr.find("(").unwrap() + 1;
let comma = substr.find(",").unwrap();
let end = substr.find(")").unwrap();
let left_str = &substr[start..comma];
let left = left_str.parse::<i32>();
if comma < end {
let right_str = &substr[comma + 1..end];
let right = right_str.parse::<i32>();
if left.is_ok() && right.is_ok() && last_mutex {
sum += left.unwrap() * right.unwrap();
}
}
} else if substr.starts_with("do()") {
last_mutex = true;
} else if substr.starts_with("don't()") {
last_mutex = false;
}
substr = substr[1..].to_string();
}
});
Ok(sum.to_string())
}
}
}
All I have done here is add a flag called last_mutex
, check that flag before adding our values to the sum, and then two conditions to check if we should set the flag. Easy!
After yesterday's rough day, it's nice to have a straightforward puzzle to solve. Advent of Code lulling us into a false sense of security, ready to strike with a vengeance.