Advent of Code 2024 – Day 3
Find all occurrences of mul(#,#)
in a set of strings, multiply the specified numbers, then sum the results.
The input contains several mul
-like entries, that need to be ignored. This problem immediately looks like a problem for a Regular Expression.
Naturally, being a regular expression, I did have an error in my first attempt, where I put the first +
outside the first capturing group, not inside it.
# I predict Part2 will have operations other than mul
struct MulOp
a::Int
b::Int
end
function parse_input(src = "input.txt")
# The input is one-or-more lines of text
# Within that text, there will be occurrances of
# mul(##,##)
# There will also be many things that are close to but don't quite match the pattern.
# This is clearly a case for regular expressions
pattern = r"mul\(([\d]+),([\d]+)\)"
results = Vector{MulOp}(undef, 0)
# Since a newline would break the pattern, we can process the file as lines, not a single string
for line in eachline(src)
for match in eachmatch(pattern, line)
a, b = match
push!(results, MulOp(
parse(Int, a),
parse(Int, b)))
end
end
return results
end
function main_part1()
ops = parse_input()
println("Results: ", sum(
map( op -> op.a * op.b, ops)
))
end
main_part1()
Julia's eachmatch
function really made this process simple. At first I thought I'd have to do the loop manually, but while reading the documentation on RegexpMatch
I scrolled up and saw eachmatch
. I should have known. The code also takes advantage of destructuring capture groups into a
and b
for convenience.
I used a struct
to store the parsed numbers because
– I suspect Part 2 is going to have non-mul
operations, and being able to switch on the returned type will be useful.
– Julia doesn't provide a fixed length array as part of Base
(although StaticArrays
is so stable it might as well be Base
).
– Tuple
felt like overkill.
– Julia is a Just-In-time compiled language, so all the options result in the same memory layout anyway.
Part 2
Find do()
and don't()
entries in the string. Disable mul
operations after a don't
, and enable again after a do
. Initially mul
operations are enabled.
This can be implemented by extending the Regular Expression to also match these new commands, and only performing the push!
onto results
when operations are enabled.
I thought it would be possible to have multiple capturing groups use the same name, as only one would ever be matching, so initially I extended the Regular Expression to use named capture groups. However, this isn't allowed, BUT there is a Branch reset group, that does the same thing for numbered groups. I've never used this feature of Regular Expressions before!
I made use of a Regular Expression Explorer while diagnosing the expression. It is really useful being able to see the Regular Expression as a hierarchy of nested operators. Without it, I would not have realized that there needs to be a single Branch reset group containing the |
alternation groups.
function parse_input_part2(src = "input.txt")
pattern = r"(?|(?:(mul)\((\d+),(\d+)\))|(?:(do)\(\))|(?:(don't)\(\)))"
results = Vector{MulOp}(undef, 0)
enabled = true
for line in eachline(src)
for match in eachmatch(pattern, line)
# dump(match)
op, a, b = match
if op == "mul"
if enabled
push!(results, MulOp(
parse(Int, a),
parse(Int, b)))
end
elseif op == "do"
enabled = true
elseif op == "don't"
enabled = false
end
end
end
return results
end
function main_part2()
ops = parse_input_part2()
println("Part 2 Results: ", sum(
map( op -> op.a * op.b, ops)
))
end
main_part2()
Thinking about viewing the Regular Expression as a hierarchy made me wonder if someone had made a Julia package to make PCRE expressions read more like Julia expressions, and I found ReadableRegex.jl. I don't think it would have helped with this problem (it doesn't look like it implements the equivalent of (?| ... )
), but it would at least allow writing the expression with multiple lines and indentation.