Dazbo's Advent of Code solutions, written in Python
ClassesLoggingEnumerateZipPattern Matching
We find ourselves stuck in a magnetically sealed garbage smasher! (There’s something strangely familiar about this.) While we wait for a family of helpful cephalopods to rescue us, we decide to help the youngest one with their math homework.
The homework consists of “Cephalopod math”, which uses vertical blocks of numbers. We need to parse these strange blocks and compute the answers.
The input looks like this:
123 328 51 64
45 64 387 23
6 98 215 314
* + * +
For Part 1, we interpret this as four separate vertically-aligned problems. The operator at the bottom (* or +) applies to all numbers in that column.
123 * 45 * 6328 + 64 + 98What is the grand total found by adding together all of the answers to the individual problems?
The core challenge here is parsing. The text is given line-by-line, but the logical data structure is column-based (or block-based).
Puzzle ClassI decided to model each math problem as an object of a Puzzle class.
Why a class?
puzzle.add_number(num) and puzzle.calculate(), which reads like English.Puzzle objects, we don’t need to worry about lists of lists or parallel arrays.Here is the class definition:
class Puzzle:
def __init__(self, operator: str):
self.numbers = []
self.operator = operator
def add_number(self, number: int):
self.numbers.append(number)
def calculate(self):
match self.operator:
case "+":
return sum(self.numbers)
case "*":
return math.prod(self.numbers)
case _:
raise ValueError(f"Invalid operator: {self.operator}")
Puzzlematch / case (Structural Pattern Matching)In the calculate method, I’m using the match statement. This was introduced in Python 3.10 and is a much cleaner, more powerful version of the traditional if/elif/else chain.
Instead of:
if self.operator == "+":
# ...
elif self.operator == "*":
# ...
We get a concise, readable structure that clearly shows we are dispatching logic based on the value of self.operator.
math.prodFor the multiplication case, I used math.prod. This was introduced in Python 3.8. It does exactly what you expect: multiplies all items in an iterable. It’s much cleaner than the old way of importing reduce from functools and doing reduce(lambda x, y: x*y, numbers).
The operator line - the last line in the input - is the key. It tells us where each problem column starts.
operator_indices = {}
for idx, operator in enumerate(data[-1]): # data[-1] is the last line with operators
if operator in "*/+-":
operator_indices[idx] = operator
puzzles.append(Puzzle(operator))
Here, enumerate gives us both the index (column position) and the character. If we find an operator, we create a new Puzzle object.
Then we iterate through the number lines:
for line in data[:-1]:
indices_list = list(operator_indices.keys())
start = end = None
for puzzle_num, puzzle in enumerate(puzzles):
# ... calculate start and end indices based on operator positions ...
puzzle.add_number(int(line[start:end]))
Using enumerate(puzzles) lets us easily look up the start index for the current puzzle and the start index for the next puzzle (which acts as our end index) from our indices_list.
Finally, because we used a Puzzle class, the final calculation is a beautiful one-liner:
return sum(puzzle.calculate() for puzzle in puzzles)
This comprehension iterates over all our puzzle objects, asks each one to calculate itself, and sums the results. Clean!
What is the grand total found by adding together all of the answers to the individual problems?
Turns out, Cephalopod math is written right-to-left in columns! The visual block:
123
45
6
Should actually be read as vertical columns forming numbers:
356, 24, 1
So we need to turn rows into columns. The standard, reliable Python idiom for this uses zip():
transposed_block = list(zip(*block))
To explain zip(*block):
block is a list of strings (rows).*block unpacks this list into separate arguments.zip() takes these rows and aggregates elements from each of them. It takes the 1st element of every row, then the 2nd element of every row, etc.Once transposed, we use .join() to stick the characters back together into a number string:
num = int("".join(num_chars))
"".join(iterable) is the efficient, Pythonic way to concatenate a list of strings. It’s much faster than doing s += char in a loop.
After re-parsing the numbers into our Puzzle objects using the new logic, we just run our calculation again:
return sum(puzzle.calculate() for puzzle in puzzles)
Final output looks something like this:
Part 1 soln=5418430663371
Part 2 soln=13022319475282
And it all runs in 0.002 seconds. Nice!