It’s been a few months since I last posted about it, but I’m still slowly trying to learn Haskell!
This is a correct solution for part 1 of the puzzle for Day 2 of the 2025 Advent of Code.
(Previous solutions: Day 1a and Day 1b)
I was able to puzzle most of this out by myself, even using
Hoogle to find
the splitOn function and setting up a suitable
Cabal project file.
My initial version of main used the head function to extract the first
argument from the command line arguments and the first line of data from the
file:
main = do
args <- getArgs
let filename = head args
fileContents <- readFile filename
let sourceData = head fileContentsThe complete absence of data validation doesn’t especially matter for
an Advent of Code solution, but the linter raises a warning for the
use of head with lists that might potentially be empty.
I was able to work out the parseArgs and readData functions to
avoid using head and to return error messages,
but couldn’t work out how to combine them together in main.
My understanding of what Monads actually do, and how this is
expressed in the syntax is, ah, still a work in progress.
I asked Claude for help, and it suggested the
nested case...of structure. I’ll be filing that lesson away for
the future!
{--
Haskell solution for Advent of Code 2025 day 2 (a).
Given:
* A comma-separated list of numeric ID ranges where:
* The list is suppied as one line of data.
* Each ranges specifies a first and last value separated
by a hyphen i.e. 95-115 for the range 95 to 115 inclusive.
* No ID will have a leading zero.
Find:
* The sum of all the ranges (include the first and last values )
which are some sequence of digits repeated twice. E.g:
55 - 5 repeated twice
6464 - 64 repeated twice
123123 - 123 repeated twice
Example input (wrapped):
11-22,95-115,998-1012,1188511880-1188511890,222220-222224,
1698522-1698528,446443-446449,38593856-38593862,565653-565659,
824824821-824824827,2121212118-2121212124
Expected result from example input:
1227775554
Dependencies:
Run with:
cabal run aoc-2025-02a <input file>
March 28, 2026 by Jarvis Cochrane
Copyright (c) 2026 Jarvis Cochrane
Free to use under CC-BY-NC-4.0 license
--}
import Data.List.Split (splitOn)
import System.Environment (getArgs)
-- Convert a comma-separated list of ranges ("11-22,95-115,998-1012")
-- into a list of 2-tuples (Integer, Integer)
-- Split on commas: ["11-22","95-115","998-1012"]
-- Split each String on hyphens: [["11","22"],["95","115"],["998","1012"]]
-- Parse each String as an Integer: [[11,22],[95,115],[998,1012]]
-- Convert to 2-Tuples: [(11,22),(95,115),(998,1012)]
parseRanges :: String -> [(Integer, Integer)]
parseRanges = map (toTuple . map read . splitOn "-") . splitOn ","
where
toTuple [x, y] = (x, y)
-- Enumerate all the values in all the ranges
enumerateRanges :: [(Integer, Integer)] -> [Integer]
enumerateRanges = concatMap (\(x, y) -> [x..y])
-- Filter out values which have the same numeric sequence repeated twice
-- The easy way to do this is to convert each numeric sequence to a string
-- (e.g. 5656 -> "5656"), divide the string into left and right halves
-- (e.g. "5656" -> ("56", "56"), and compare the two halves.
-- Values with an odd number of digits don't need further consideration
-- since they cannot contain a repeated sequence.
filterValues :: [Integer] -> [Integer]
filterValues = filter retain
where
retain i = even stringLength && paired
where
stringValue = show i
stringLength = length stringValue
stringHalves = splitAt (stringLength `div` 2) stringValue
paired = uncurry (==) stringHalves
-- Parse, enumerate, filter, and sum
solve :: String -> Integer
solve = sum . filterValues . enumerateRanges . parseRanges
-- Parse command line arguments
parseArgs :: [String] -> Either String String
parseArgs [x] = Right x
parseArgs _ = Left "Missing input filename"
-- Read data
readData :: [String] -> Either String String
readData (x:_) = Right x
readData _ = Left "Invalid data file"
main :: IO ()
main = do
args <- getArgs
-- Claude.ai showed me how to use case..of like this.
-- I was struggling to work out how to the IO and Either
-- monads together.
case parseArgs args of
Left err -> putStrLn err
Right filename -> do
fileContents <- readFile filename
case readData (lines fileContents) of
Left err -> putStrLn err
Right input -> print (solve input)