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.

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 (5 twice), 6464 (64 twice), and 123123 (123 twice) 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 ranges

I 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
  • \1 matches 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_sum

To 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.