Advent of Code 2025 Day 2

Part 1

Read in a list of product ID ranges, where the ranges are separated by commas and the start and end IDs are separated by hyphens. Work out which IDs are invalid and add them up.

Solution

As usual, first of all we need to design some data structures. We need a data structure for an ID which stores the number and whether it is valid:

type ProductId struct {
	id    int
	valid bool
}

We also need a product ID range:

type ProductIdRange struct {
	ids []ProductId
}

Before we start reading any input, we should write (and test!) a function for working out whether a product ID is valid.

I initially failed to adhere to my tips on solving the puzzles and read the specification as: an invalid ID is one which is made up solely of a repeated sequence of digits. How can we work this out? The simplest way is to split the ID into chunks, starting at one character, and see if the chunks are equal - if so the ID is not valid.

How do we do this in Go? Recent versions have slices.Chunk, which will split a slice into sub slices of a given length (but returns an iterator, so we have to use slices.Collect to get the associated slice). The rough outline is:

  1. Split the string into characters (strictly speaking UTF-8 sequences but we know the input is ASCII): strings.Split with the empty string as the separator.
  2. Split the slice of characters into chunks starting at size of 1 and increase the size on each iteration.
  3. Count how many slices of characters after the first are not equal to the first.
  4. If no slices of characters are different, there must be a repeated sequence and the ID is not valid.

In Go:

func isValid(id string) bool {
	valid := true

	// ID of length 1 is always invalid because all
	// digits are the same
	if len(id) == 1 {
		return false
	}

	characters := strings.Split(id, "")

	for chunkSize := 1; chunkSize <= (len(characters)/2) && valid; chunkSize++ {
		chunks := slices.Collect(slices.Chunk(characters, chunkSize))

		// We only need to know how many chunks are different to the first
		// Anything more than 0 means the ID is valid
		differentToFirst := 0

		for c := 1; c < len(chunks); c++ {
			if !slices.Equal(chunks[c], chunks[0]) {
				differentToFirst++
			}
		}

		if differentToFirst == 0 {
			valid = false
		}
	}

	return valid
}

I realised my error when writing tests for the isValid function and seeing that 111 was invalid (it is a repeated sequence of the same digit) whereas it should have been valid. This did prove once again how useful tests are though, even for relatively simple puzzles - and perhaps also demonstrates that I shouldn’t try to solve puzzles on a Friday afternoon after I’ve spent the whole week writing IT policy documents and refactoring legacy PHP code.

Fortunately most of the thought process was re-usable, as the simplified validity check would still use the same functions from the standard library.

  1. Assume the ID is valid.
  2. If the length of the ID is even, check for validity (odd length IDs will always be valid as they cannot have two equal chunks).
  3. Split the ID into two chunks of size: length of ID / 2.
  4. Compare the two chunks - if they are equal then the ID is not valid.

The isValid function is now:

func isValid(id string) bool {
	valid := true

	// Only check even length IDs as odd length IDs cannot be
	// split into two repeated sequences
	if len(id)%2 == 0 {
		characters := strings.Split(id, "")
		chunks := slices.Collect(slices.Chunk(characters, len(id)/2))
		valid = !slices.Equal(chunks[0], chunks[1])
	}

	return valid
}

Parsing the input and converting it into a slice of ProductIdRange is fairly straightforward - we split the whole string on commas and then split each substring on hyphens:

func getProductIdRanges(input string) []ProductIdRange {
	productIdRanges := []ProductIdRange{}

	rangeStrings := strings.Split(input, ",")

	for r := range rangeStrings {
		idStrings := strings.Split(rangeStrings[r], "-")

		productIdRange := ProductIdRange{}
		startId, _ := strconv.Atoi(idStrings[0])
		endId, _ := strconv.Atoi(idStrings[1])

		for currentId := startId; currentId <= endId; currentId++ {
			productIdRange.ids = append(productIdRange.ids, ProductId{
				id:    currentId,
				valid: isValid(strconv.Itoa(currentId)),
			})
		}

		productIdRanges = append(productIdRanges, productIdRange)
	}

	return productIdRanges
}

Finally, we need to add up all the invalid IDs. We can use the samber/lo library with a nested SumBy, where we return the ID if invalid and zero if valid (so it has no effect on the sum):

func main() {
	inputBytes, _ := os.ReadFile("../2025-02-input.txt")
	inputString := string(inputBytes)

	productIdRanges := getProductIdRanges(inputString)
	invalidIdSum := lo.SumBy(productIdRanges, func(productIdRange ProductIdRange) int {
		return lo.SumBy(productIdRange.ids, func(productId ProductId) int {
			if !productId.valid {
				return productId.id
			} else {
				return 0
			}
		})
	})

	fmt.Println(invalidIdSum)
}

Part 2

Overall thoughts

Other than my mistake in not reading the specification correctly, this was an easy puzzle to solve. I discovered slices.Chunk which is likely to be useful in other solutions.