Skip to content
michael.caisse.io michael.caisse.io
haskell advent-of-code

AoC 2025 Day 2

Advent of Code 2025 Day 2 — lists of lists, repeating patterns, and skipping the vegetables.

M

Michael Caisse

2 min read
AoC 2025 Day 2

AoC 2025 Day 2

Part 1

Lists of Lists of Lists

Like a kid eating the yummy parts first, I’m going to start with the mathy portions while I push the peas-of-parsing-the-input around on the plate in my head.

This puzzle requires that you take a range of numbers and find values that have repeating patterns. For example, 111 or 1212 or 423423423. These values should then be added together for the final answer.

Here is my first thought:

  1. Convert to values into String representations
    • 123123 -> “123123”
  2. For each String, convert into a list of lists of evenly grouped parts
    • [["1", "2", "3", "1", "2", "3"], ["12", "31", "23"], ["123", "123"]]
  3. If a list contains all matching values then we have found a number with repeating patterns.

A String is just a list, [Char]. So I think that means we have lists of lists of lists of lists… or something like that. Sounds pretty Lispy.

Coding the thoughts

Let’s see if this makes sense in code.

The data comes in as a list of ranges and ranges are trivial in Haskell. 324-565 can be written as the lazy list [324..565].

The show function will do the Int -> String for us.

rangeAsStrings :: Int -> Int -> [String]
rangeAsStrings a b = map show [a..b]

We can divide our string in parts like this:

chunkString :: String -> Int -> [String]
chunkString [] _ = []
chunkString s cnt = take cnt s : (chunkString (drop cnt s) cnt)

The : operator is really neat!

ghci> chunkString "123456" 3
["123","456"]

And then ask if all of the elements in the array are equal with the all function. We need to pick an identity value for this operation. There are some good maths that explain why an empty list should be True for and but I want the empty list to be False. I hear fooling yourself is the easiest.

allSame :: Eq a => [a] -> Bool
allSame [] = False
allSame (x:xs) = all (==x) xs

The chunkString function will work with values that are not evenly divisible into the string length. The comparison, allSame, will fail as it should. It is nice to have things “Just Work” ™ without special cases, in fact this is what we strive for. It also rubs my sensibilities wrong to chunk a 13-digit number into 13 different lists for comparison when we can easily calculate which ones are candidates. Calculating the list of candidate lengths is pretty straight forward with a list comprehension.

groupLengths :: Int -> [Int]
groupLengths x = [v | v <- [1..(x `quot` 2)], x `mod` v == 0]

It looks like we have the building blocks to get a list of chunked strings given an initial input string. List comprehension should do the trick to create a String -> [[String]].

candidateChunks :: String -> [[String]]
candidateChunks s = [chunkString s cnt | cnt <- groupLengths (length s)]

I want to pause a minute and bask in the glories of Haskell. I thought this was going to take a bit more code. I wrote the type signature and then the implementation line just fell out… tangled the file, loaded into ghci … it just works. Haskell is so elegant.

And now, given a string lets see if it has repeating chunks and is therefore a value we are looking for.

stringHasRepeats :: String -> Bool
stringHasRepeats s = or (map allSame (candidateChunks s))

Woot! and now we have a predicate that can check if a string meets our requirements. Composing a predicate means we can now easily filter a list for the things that match the predicate.

Being a Good Reader

Good readers are hard to find. I went back to look for sample input from the question and realized that I read the “specification” wrong. The repeating pattern only repeats twice. So, “1212” or “123123” fit the pattern but “121212” does not. That means my implementation is wrong. I’ve been around the block enough with AoC to know that being more generic is usually better when you get to Part 2. Strictly speaking, the only thing that has to change is the groupLength implementation which only needs to return [length/2] or []. So I’m going to make that minor change for now.

groupLengths :: Int -> [Int]
groupLengths x = [v | v <- [(x `quot` 2)], x `mod` 2 == 0]

Assembling the Parts

Let’s put our new shiny predicate to work.

intHasRepeats :: Int -> Bool
intHasRepeats = stringHasRepeats . show

filterSillyPatterns :: [Int] -> [Int]
filterSillyPatterns = filter intHasRepeats

Assuming the input data is in a form of a list of ranges or a list of lists, [[Int]], then it should be a pretty simple process of adding up all the silly patterns. We can just compose a function to do this (have I mentioned how much I’m loving Haskell?):

addSillyPatterns :: [[Int]] -> Int
addSillyPatterns = sum . concat . map filterSillyPatterns

Bam! That is it.

ghci> addSillyPatterns input_ranges
1227775554

Peas

Sometimes, you have so few peas on your plate that you find ways to hide them among the scraps. The input data set is really small for this puzzle day and good software engineers are just-the-right-amount-of-lazy … so I’m going to “manually” convert the values. Is this cheating? Maybe. Do I care right now? No. (o;

The input file looks something like:

45-76,123-897,1000-7666

I’m just going to copy that into my editor and do the transform so it looks like:

input_ranges = [[45..76],[123..897],[1000..7666]]

I’ll eat my peas tomorrow.

Part 2

I’ve been around the block enough with AoC to know that being more generic is usually better when you get to Part 2.

Look at that! I get to quote myself from just a few minutes ago. Buhahahaha! So, my original implementation is the solution for Part 2.

Day 2 is the books and I didn’t need to eat any vegetables!

Back to Blog
Share:

Related Posts