Advent of Code - Day 3 - Increasing and decreasing (Basic)

(task | solution) As I reached the third day of my adventure, I had already learned something new that significantly improves the process: the devcontainer.json file can handle creating and running the Docker container for me. This saves the trouble of manually building and running it—essentially saving two commands on the command line, which adds up to about three seconds. Still, it’s an improvement. However, I am not going to use it.

I had initially planned to solve today’s riddle in BASIC—the first programming language I learned 30 years ago. However, it turns out that deploying a BASIC compiler into a container isn

Today, we need to parse a text to find all occurrences of mul(n, m), calculate the product of n and m, and finally compute the sum of all products. Let’s try to find a neat one-liner for that.

Solution of the first part

Here it is:

with open("input.txt", "r") as file: print(sum([reduce(mul, ast.literal_eval(match.replace("mul", ""))) for match in re.findall("mul\(\d{1,3},\d{1,3}\)", file.read())]))

What am I doing? Well, let’s read it from the inside out:

First, we get all occurrences of mul(n, m), which is quite easy using regex:

re.findall("mul\(\d{1,3},\d{1,3}\)

Then, we go through all matching groups using Python’s list comprehension feature:

[match for match in re.findall("mul\(\d{1,3},\d{1,3}\)", file.read())]

But we’re not just using the plain result, which is a string like mul(4, 2). First, we remove mul to get the parentheses, then we evaluate the string into an actual tuple, like (4, 2):

ast.literal_eval(match.replace("mul", ""))

Now that we have a list of integers, we can apply multiplication to each item:

reduce(mul, (4, 2))

Each of the products will be fed into the final list. The sum() method simply adds up all the products in the list. The surrounding with ... as file: simply opens the file to read the content.

Solution of the second part

Well, the second part is a little trickier with three additional rules:

  • Only sum up products of mul() if there’s the preceding keyword do().
  • Ignore all mul() instructions after the keyword dont().
  • All mul() instructions at the beginning, where there’s neither a dont() nor a do(), need to be considered, too.

This sounds easy at first glance, so I tried to figure out a way to simply adjust my regular expression. With no success, but a ton of frustration. However, I figured out a different way that is way more fun and demonstrates the obscure power of list comprehension:

sum([sum([sum([reduce(mul, ast.literal_eval(match.replace("mul", ""))) for match in re.findall("mul\(\d{1,3},\d{1,3}\)", dos)]) for _j, dos in enumerate(donts.split("do()")[1:])]) for _i, donts in enumerate(file_contents.split("don't()")) if len(donts.split("do()")) > 1])

I guess you have questions? Fair enough. There you go, this is the expanded version:

sum_of_all = 0
for _i, donts in enumerate(('do()' + file_contents).split("don't()")):
    if len(donts.split("do()")) > 1:
        for _j, dos in enumerate(donts.split("do()")[1:]):
            for match in re.findall("mul\(\d{1,3},\d{1,3}\)", dos):
                print(f'string: {match} - list: {ast.literal_eval(match.replace("mul", ""))} -  product: {reduce(mul, ast.literal_eval(match.replace("mul", "")))}')
                sum_of_all += reduce(mul, ast.literal_eval(match.replace("mul", "")))

print(f'sum: {sum_of_all}')

The idea is that I simply split the whole text by the string dont() in the first for loop. This returns a list of items, where I know that every mul() must be ignored until I encounter a do() in the string.

This leads to the next operation: Instead of parsing the string, I split each of the strings by the keyword do(). If this string contains do(), I get a list of 2 items. If not, I can ignore the whole string in my outer loop.

Otherwise, I take all items except the first one, and now I can apply the logic from above: using the regular expression to get the list of integers, then multiplying and summing up.

There’s a small pitfall: The third rule says that all instructions at the beginning of the string, until the first do() or dont(), need to be considered. The simplest way to handle this is by just adding do() to the initial input string.

In case you’re wondering how to go from the seemingly simple connection of nested loops to this nasty nested list comprehension—you achieve it step by step:

First, we iterate through the raw file content:

[donts for _i, donts in enumerate(('do()' + file_contents).split("don't()"))]

Then, we replace dont() with the next loop:

[[dos for _j, dos in enumerate(donts.split("do()")[1:])] for _i, donts in enumerate(('do()' + file_contents).split("don't()"))]

Don’t forget to add a condition to skip results with a length of 1:

[[dos for _j, dos in enumerate(donts.split("do()")[1:])] for _i, donts in enumerate(('do()' + file_contents).split("don't()")) if len(donts.split("do()")) > 1]

Then, we can replace do() with the following loop:

[[[match for match in re.findall("mul\(\d{1,3},\d{1,3}\)", dos)] for _j, dos in enumerate(donts.split("do()")[1:])] for _i, donts in enumerate(('do()' + file_contents).split("don't()")) if len(donts.split("do()")) > 1]

Now we are at the point where we handle the matching groups, so we replace match with everything we need to get a product out of the stringified list of integers:

[[[reduce(mul, ast.literal_eval(match.replace("mul", ""))) for match in re.findall("mul\(\d{1,3},\d{1,3}\)", dos)] for _j, dos in enumerate(donts.split("do()")[1:])] for _i, donts in enumerate(('do()' + file_contents).split("don't()")) if len(donts.split("do()")) > 1]

And eventually, we get the sum of each nested list:

sum([sum([sum([reduce(mul, ast.literal_eval(match.replace("mul", ""))) for match in re.findall("mul\(\d{1,3},\d{1,3}\)", dos)]) for _j, dos in enumerate(donts.split("do()")[1:])]) for _i, donts in enumerate(('do()' + file_contents).split("don't()")) if len(donts.split("do()")) > 1])

Looks great, doesn’t it?

Whats up, Python?

Rating: 14/12 – nice and friendly, always again

Uncontroversial—one of Python’s best features is list comprehensions, which also work for dictionaries, sets, and tuples, by the way. List comprehensions have so many features; besides the way you can nest them, you can add conditions in two ways. Either when you add an item to the output list…

[x if x % 2 == 0 else -x for x in range(10)]

…or before:

[x for x in range(10) if x % 2 == 0]

You can loop through multiple lists (et al):

[(x, y) for x in range(3) for y in range(2)]

You can not only nest them, but also serialize multiple loops to flatten a nested input:

nested = [[1, 2], [3, 4], [5, 6]]
[x for sublist in nested for x in sublist]

And there are probably a couple more that I don’t even know.

See you next day…