How to solve Advent of Code 2023 โ€“ Day 1 with Python

We’re back! It’s once again time for the most festive time of the year and therefore Advent of Code! ๐ŸŽ‰ But before we get right into the first day and my solution for this day’s puzzle:

What even IS Advent of Code??!!

Good question, my friend. Here is this yearโ€™s about page: https://adventofcode.com/2023/about and it says:

Advent of Code is an Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like. People use them as interview prep, company training, university coursework, practice problems, a speed contest, or to challenge each other.

If youโ€™re wondering why you should participate, read my Advent Of Code 2022 โ€“ 7 Reasons why you should participate article ๐Ÿ˜‰ I promise it’s still relevant in 2023, too.

But chances are, if youโ€™re here, you already want to get going and see how the challenge is solved, so letโ€™s get going.

My GitHub Repository for Advent Of Code

https://github.com/GalaxyInfernoCodes/Advent_Of_Code_2023

Like in the last year, I will again upload all of my solutions there in the form of Python notebooks (.ipynb files), but you can copy paste the code into normal .py files, too.

I am uploading the examples from the task as ‘example.txt’ files to test my solution. You will have to use your own ‘input.txt’ with your supplied input since everyone gets their own and everyone needs to provide a different solution based on their input.

Locally, I use in VSCode as my IDE, but you can work with whatever you know best.

Day 1 Puzzle

In this blog post Iโ€™ll describe the puzzle and solution steps, but if you want to jump ahead to said solution, you can do so here: https://github.com/GalaxyInfernoCodes/Advent_Of_Code_2023/blob/main/Day01_Python/AdventOfCode_Day01_Python.ipynb

Here is the challenge, if you want to read the full puzzle: https://adventofcode.com/2023/day/1

In summary, on day 1 we learn that there is an issure with the snow production and we need to help the elves to fix it and make sure we get a white christmas. To get to the first location that needs help, the elves load us into a trebuchet (don’t ask, they’re always a bit weird) which needs specific numbers as calibration. This is where the puzzle comes in.

Part 1

Day 1 gives you an input text portraying the calibration values mixed with letters:

1abc2
pqr3stu8vwx
a1b2c3d4e5f
treb7uchet

For the solution we want the first and last digit of each line.

To parse the input I always put the text into a .txt file and read from there. With that I can easily switch between the “example.txt”, which is described in the task including its solution, and the final “input.txt” each of us gets. First, I read from this file line by line:

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

This returns: ['1abc2', 'pqr3stu8vwx', 'a1b2c3d4e5f', 'treb7uchet']

I then iterate through each line (calibration_str) with the following function to extract the digits:

def get_digits(calibration_str: str) -> int:
    first_digit = None
    last_digit = None
    for character in calibration_str:
        if character.isdigit():
            if first_digit is None:
                first_digit = int(character)
            last_digit = int(character)
    return first_digit * 10 + last_digit

Some pointers:

  • .isdigit() is a great function to use here on a text character to find out if it’s a number or not
  • a string is always a list of characters, so you can simply iterate over it with a for-loop
  • combine the two single-digits either as a string ("".join(["2", "1"]) -> "21" and then into an integer (int("21") -> 21) or multiply the first digit by 10 like I did above (2 * 10 + 1 = 21)
    • the string-approach becomes more convenient the longer the number is

Here’s the full solution for part 1:

def solve_part1(input_file: str) -> int:
    with open(input_file, 'r') as f:
        lines = f.readlines()
        calibration_strings = [entry.strip() for entry in lines]
        return sum([get_digits(calibration_str) for calibration_str in calibration_strings])

Part 2

In part 2, it gets revealed that it’s also possible for numbers to appear as text (like “seven” instead of “7”) in the input. These should also count (“one” to “nine”).

Here’s the new example input:

two1nine
eightwothree
abcone2threexyz
xtwone3four
4nineeightseven2
zoneight234
7pqrstsixteen

What is only implicitly said, and I overlooked this at first: number-words can overlap. Sigh.

The string “oneight” should translate to “18”. This means you can’t replace “one” by “1”, because then the remaining string would be “1ight” and the “8” would not be recognized.

So what I do is only replacing the first letter of the word while iterating through the string from front to back… Example:

  • “two1nine” -> “2wo19ine”
  • “zoneight234” -> “z1neight234” -> “z1n8ight234”

This ensures that the “e” remains to find the “eight” in the next steps. Here’s the code to achieve this monstrosity:

def replace_digit_words(calibration_str: str) -> str:
    character_map = {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6, "seven": 7, "eight": 8, "nine": 9}
    extracted_string = list(calibration_str)
    # loop through the string:
    for str_index in range(len(calibration_str)):
        for digit_word, digit_value in character_map.items():
            if calibration_str[str_index:].startswith(digit_word):
                extracted_string[str_index] = str(digit_value)
    return "".join(extracted_string)

The full code for part 2 reuses the original method from part 1 on the adjusted string:

def solve_part2(input_file: str) -> int:
    with open(input_file, 'r') as f:
        lines = f.readlines()
        calibration_strings = [entry.strip() for entry in lines]
        adjusted_strings = [replace_digit_words(calibration_str) for calibration_str in calibration_strings]
        calibration_numbers = [get_digits(calibration_str) for calibration_str in adjusted_strings]
        return sum(calibration_numbers)

Conclusion

I’m writing this after the first 3 days and honestly it feels like the string-fighting is particularly gnarly this year. Looking forward to actually implementing more logic than fighting with letters in the following days.

The upside here is that you can reuse a lot of these parsing methods, so once you understood them, you will have less issues coming up.

Leave a Reply

Consent Management Platform by Real Cookie Banner