Advent of Code Day 2, 2025

Posted on April 2, 2026 by Jarvis Cochrane · Tagged ,

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 fileContents

The 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)