Spoiler alert
This is a full solution for Advent of Code 2025, day 2 puzzles with explanations.
If you want to try to solve it yourself first, head over to https://adventofcode.com/2025/day/2 and give it a go and then come back and compare notes.
You can find the full code at https://github.com/Hamatti/adventofcode-2025/blob/main/src/day_2.py
I keep talking about the benefits of writing notes and blog posts for yourself, to document what you’ve learned and to strengthen your technical understanding of topics.
Today is a masterclass of that.
We’ve entered Santa’s gift shop but a young elf has made a mess with product id database and there’s a ton of invalid ids there.
As soon as I saw the instructions:
Since the young Elf was just doing silly patterns, you can find the invalid IDs by looking for any ID which is made only of some sequence of digits repeated twice. So,
55(5twice),6464(64twice), and123123(123twice) would all be invalid IDs.
I thought to myself: “I’ve done something similar in the past”.
Lo and behold, back in 2015, day 5 we had to find out if a string was naughty or nice and one of the rules for being nice was that it contained a portion that repeated itself.
That basically solved today’s puzzle for me.
But let’s go through how.
Read input
This time, we have a single line input with integer ranges (like 11-22) separated with commas.
def mapper(line: str) -> List[Tuple[int, int]]:
"""Reads a line of integer rangers, divided by commas"""
ranges = []
for r in line.split(','):
start, end = [int(p) for p in r.split('-')]
ranges.append((start, end))
return rangesI split the line at commas to get ranges and then split those ranges at dashes and convert both to integers with a list comprehension.
Part 1
Your job is to find all of the invalid IDs that appear in the given ranges. [ - - ] What do you get if you add up all of the invalid IDs?
Regular expressions are a powerful tool to check for patterns in strings. They can get unwieldy quite quickly though, so it’s important to be vigilant with them. (I have a quick primer if you’re new to them.)
INVALID_ID_PATTERN = r'^(\d+)\1$'Let’s break down the pattern to understand what we’re looking for here:
^means “start of the string”(\d+)matches any number that is at least one digit long\1matches the captured value in previous step$means “end of the string”
So essentially, we look for any number and then see if it happens twice. We then wrap this around a function:
def is_invalid(product_id: int) -> re.Match[str] | None:
"""Checks if product_id is made of two repeated strings"""
return re.search(INVALID_ID_PATTERN, str(product_id))and move on to the main function for this part:
def part_1() -> int:
data = read_input(2, mapper)[0]
invalid_id_sum = 0
for start, end in data:
for product_id in range(start, end+1):
if is_invalid(product_id):
invalid_id_sum += product_id
return invalid_id_sumTo solve the puzzle, we get our data (my system is built for the input to always be multi-line, hence in cases like this with only single line of data, I need to get it with [0] index access).
For each range, we loop over all the numbers within the range (remember to add +1 to the end because Python ranges don’t include the last number) and check if our product id is invalid. If it is, we add it to our sum.
First ⭐️ of the day in the bag!
Part 2
The twist of part 2 fits our approach really nicely
Now, an ID is invalid if it is made only of some sequence of digits repeated at least twice.
The only thing we need to do is add this requirement to our regex pattern:
INVALID_ID_PATTERN_2 = r'^(\d+)(\1)+$'This time, we wrap the \1 into a capture group with parenthesis and add a + sign after it. The + means “more than once”. So here, we need to find the original and it must be repeated at least once.
Everything else is the same, I wrapped this into a new function is_invalid_p2 that just uses the different pattern but everything else is the same.
Addendum A: non-regex part 2
At our archipylago Advent of Code jam, I got an opportunity to redo part 2 to help a student figure out how to approach it. I uploaded that as day_2_alt.py in my GitHub repo and here’s how it works.
Let’s take a look at an example:
12345678 is a valid number as there’s no repeating parts. We split it into half:
1234 and check how long it is in relation to the original (original’s length (8) divided by this substring’s length (2)). Then we multiply this substring by that number and compare it to the original:
12341234 != 12345678
We then take a number off from the end and repeat:
123123 != 12345678
and once again
12121212 != 12345678
and so on until it either we end up with a single-digit number that doesn’t match or we find a match.
For something that matches, let’s look at 565656:
We start with 565 which is a half of the original so we double it 565565 != 565656. We chop off a digit and end up with 56 which is a third of the original and now we get 565656 == 565656.
def part_2() -> int:
ranges = read_input(2, mapper)[0]
invalid_ids = 0
for start, end in ranges:
for num in range(int(start), int(end)+1):
original = str(num)
for i in range(len(original)//2, 0, -1):
substring = original[:i]
multiplier = len(original) // len(substring)
if substring * multiplier == original:
invalid_ids += num
break
return invalid_idsOnce we find a hit, we break out of the loop to avoid finding the same number multiple times.
Get in touch
If you want to comment on these solutions, I share them in Mastodon where you can look for the post for the right date or you can start a discussion over email with juhamattisantala@gmail.com.
Follow via RSS
I have created a custom RSS feed for these solutions so you can follow along and not miss any of them!