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 issue 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()](https://www.geeksforgeeks.org/python-string-isdigit-method/)
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.