How to solve Advent of Code 2022 - Day 5 with Python

How to solve Advent of Code 2022 - Day 5 with Python

If you missed any previous days, click here for all my content about that: Advent of Code, if you want to know why you should participate and try to code the solution for yourself, click here: Advent Of Code 2022 – 7 Reasons why you should participate. If you're here to see the solution of Day 5, continue reading ;)

GitHub Repository

https://github.com/GalaxyInfernoCodes/Advent_Of_Code_2022

I will upload all of my solutions there - in the form of Python (or Scala alternatives) notebooks. You have to use your own input for the "input.txt" since everyone gets their own and everyone needs to provide a different solution based on their input.

Day 5 Puzzle

On day 5 of Advent of Code, we had to move crates around. A major challenge in this was parsing the input and I would be lying if I said I was happy with my parsing. Tried multiple things and ultimately settled on the following:

Parse Input

with open('example.txt', 'r') as f:
    lines = f.readlines()
    lines = [entry for entry in lines]

I first take the first line and divide its length by 4 to get the number of crates we're dealing with. We'll see later what we do with this information.

number_of_crates = len(lines[0])//4

Then I divide the lines into two groups:

  1. lines before the empty line ('\n') and the ' 1 2 3 \n'-line before that
  2. lines after the empty line

The first group gives us the start configuration of the crates (called crate_lines), and the second group gives us the moving_lines, used to change the crate stacks.

crate_lines = lines[:lines.index('\n')-1]
moving_lines = lines[lines.index('\n')+1:]

Within the crate stacks we get the relevant entries by selecting the right positions in the string, the first is at index 1, the next is always 4 places further along. With list slicing that looks like this:

items = list(line)[1:-1:4]

For the moving lines, I first strip away the new line character and then split on whitespace to separate the words. Then I each "word" to see if it's a number and select those. I know that there are always 3 numbers so I can directly assign them to their variables:

amount, source, target = [int(entry) for entry in line.strip().split(' ') if entry.isdigit()]

Part 1 & 2 in one

Because the parts only differ in whether you move multiple crates at once of not, I'll show you the code in one go. They way I set it up in Part 1, I only had to reverse one list to make it work for Part 2.

The crates stacks

I considered multiple options here, but in the end crate stacks are an entity with clear actions upon them, so let's just model them as a class:

class CrateStack:
    def __init__(self) -> None:
        self.content = []

    def add_item_on_top(self, item):
        self.content.append(item)

    def take_x_crates(self, x, move_multiple):
        return_crates = self.content[-x:]
        self.content = self.content[:-x]
        if move_multiple:
            return return_crates
        else:
            return reversed(return_crates)
    
    def add_crates(self, new_crates):
        self.content += new_crates

    def get_top_content(self):
        return self.content[-1] if len(self.content) > 0 else ""

In essence, this is just a list, but with all the operations we need to do on the list packed away neatly behind descriptive method names. We can

  • add one item on top (at the end) of the list during setup
  • take x crates from the top (either one by one by reversing the end of the list if move_multiple=False in Part 1, or all at once)
  • add the chunk of crates from another stack on top during moving
  • get info about the top crate as a string for the output for the elves

Nice.

The cargo bay as a whole

While we're at it, this is a class, too. Under the hood it's a list of CrateStacks. So a list of lists. But with nice descriptive bundled methods again ;) This can do the following with its methods:

  • add a list of items to the creates - one item per crate
  • move a stack of crates from one CrateStack to the other using the methods we just designed above
  • return the summary message the elves desire so much by collecting all the top-info from all its crates
  • print its contents for debugging purposes :) I left that in after debugging, so you can take a look inside if you want
class CargoBay:
    def __init__(self, number_of_crates):
        self.number_of_crates = number_of_crates
        self.crates = [CrateStack() for _ in range(number_of_crates)]

    def add_items_to_crates(self, items):
        for crate, item in zip(self.crates, items):
            if item != ' ':
                crate.add_item_on_top(item)

    def move_items(self, amount, source, target, move_multiple):
        moving_crates = self.crates[source].take_x_crates(amount, move_multiple)
        self.crates[target].add_crates(moving_crates)

    def get_top_stacks(self):
        return_message = ""
        for crate in self.crates:
            return_message += crate.get_top_content()
        return return_message

    def print_crates(self):
        for crate in self.crates:
            print(crate.content)
        print('-----')

Putting it all together

So here's the input parsing and the resulting instructions to the cargo bay.

  • use the number of crates we identify in the first line of input to set up the CargoBay object
  • reverse the crate_lines to build the stacks from bottom to top (because it makes my brain happier, I guess you could do it the other way around, too)
  • hand off the moving instructions (amount, source, target) to the cargo bay
def solve(file_name, part_2=False, print_crates=False):
    with open(file_name, 'r') as f:
        lines = f.readlines()
        lines = [entry for entry in lines]

    number_of_crates = len(lines[0])//4
    cargo_bay = CargoBay(number_of_crates)

    # everything before the empty line (minus the ' 1   2   3...' line are crate lines)
    crate_lines = lines[:lines.index('\n')-1]
    # iterate over the crates from the bottom to the top
    for line in reversed(crate_lines):
        items = list(line)[1:-1:4]
        cargo_bay.add_items_to_crates(items)
    
    # everything after the empty line are moving lines
    moving_lines = lines[lines.index('\n')+1:]
    for line in moving_lines:
        if print_crates:
            cargo_bay.print_crates()
        amount, source, target = [int(entry) for entry in line.strip().split(' ') if entry.isdigit()]
        cargo_bay.move_items(amount, source-1, target-1, part_2)
    
    print(cargo_bay.get_top_stacks())

Final call (adjust file and parameters as needed):

solve('example.txt', part_2=True, print_crates=True)

Conclusion

I liked that. Creating two classes for what is essentially a list of lists felt a bit over-engineered, but I think especially for beginners this is a great exercise.

If you ever need that code again having descriptive methods and objects will make this much more easy to read than lines over lines of fighting with indices and slices of lists.

Of course, I am probably never reading this again, but since this is now on the internet, I wanted to make it as helpful as possible :)