From 222f70f4aca133ca42745170169a5f72aac04257 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Sun, 19 Jan 2025 22:39:05 -0600 Subject: [PATCH] refactor(2024.07-haskell): add data types and flesh out docs --- LICENSE | 2 +- VERSION | 2 +- src/AdventOfCode/Year2024/Day07.hs | 157 ++++++++++++++++++++++------- 3 files changed, 120 insertions(+), 41 deletions(-) diff --git a/LICENSE b/LICENSE index e75495a..1058a24 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright © 2016-2022 Eric Bailey +Copyright © 2016-2025 Eric Bailey Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal diff --git a/VERSION b/VERSION index f884004..42850a8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2024.7.2.3 \ No newline at end of file +2024.7.2.4 \ No newline at end of file diff --git a/src/AdventOfCode/Year2024/Day07.hs b/src/AdventOfCode/Year2024/Day07.hs index 23a15d8..6341c39 100644 --- a/src/AdventOfCode/Year2024/Day07.hs +++ b/src/AdventOfCode/Year2024/Day07.hs @@ -1,6 +1,35 @@ {-# LANGUAGE MonadComprehensions #-} -module AdventOfCode.Year2024.Day07 where +-- | +-- Module : AdventOfCode.Year2024.Day07 +-- Description : Advent of Code 2024 Day 7: Bridge Repair +-- Copyright : (c) Eric Bailey, 2025 +-- License : MIT +-- Maintainer : eric@ericb.me +-- Stability : experimental +-- Portability : POSIX +-- https://adventofcode.com/2024/day/7 +module AdventOfCode.Year2024.Day07 + ( -- * Introduction + -- $intro + + -- * Data types + CalibrationEquation, + Operator (..), + + -- * Solution + main, + partOne, + partTwo, + execInverseOperation, + calibrate, + isPossible, + + -- * Input and example + getInput, + getExample, + ) +where import AdventOfCode.Input (parseInput, parseString) import AdventOfCode.TH (defaultMain, inputFilePath) @@ -11,19 +40,74 @@ import Data.List.NonEmpty (NonEmpty (..)) import Data.Maybe (mapMaybe) import Text.Trifecta (Parser, char, decimal, newline, sepEndBy, sepEndByNonEmpty, string) +-- $intro +-- This module borrows +-- [ideas](https://github.com/mstksg/advent-of-code/wiki/Reflections-2024#day-7) +-- from [Justin Le](https://blog.jle.im). In particular, 'isPossible' is +-- essentially the same as Justin's +-- [@search@](https://github.com/mstksg/advent-of-code/blob/0128ceaf/2024/AOC2024/Day07.hs#L24-L27). +-- The main idea is to process the operands from right to left, which enables +-- quickly filtering out equations that are impossible. + +-- | A calibration equation consists of a test value and a nonempty list of +-- operands. +type CalibrationEquation = (Int, NonEmpty Int) + +-- | An invertible binary operator. +data Operator + = -- | Add + (:+:) + | -- | Multiply + (:*:) + | -- | Concatenate + (:||:) + deriving (Eq, Show, Enum, Bounded) + +-- | Given an operator and two operands, execute the inverse operation. +-- +-- >>> execInverseOperation (:+:) 40 (81 + 40) +-- Just 81 +-- +-- >>> execInverseOperation (:*:) 19 (10 * 19) +-- Just 10 +-- +-- >>> execInverseOperation (:||:) 345 12345 +-- Just 12 +execInverseOperation :: Operator -> Int -> Int -> Maybe Int +execInverseOperation operator x y = + case operator of + (:+:) -> [y - x | y >= x] + (:*:) -> [y `div` x | y `mod` x == 0] + (:||:) -> + let pow = numDigits x + (d, m) = y `divMod` (10 ^ pow) + in [d | m == x] + +-- | Solve the puzzle and print the results. main :: IO () main = $(defaultMain) -partOne :: [(Int, NonEmpty Int)] -> Int -partOne = calibrate [unAdd, unMultiply] +-- | Calibrate the equations using addition and multiplication. +-- +-- @partOne = 'calibrate' ['(:+:)', '(:*:)']@ +partOne :: [CalibrationEquation] -> Int +partOne = calibrate [(:+:), (:*:)] -partTwo :: [(Int, NonEmpty Int)] -> Int -partTwo = calibrate [unAdd, unMultiply, unConcatenate] +-- | Calibrate the equations using addition, multiplication, and concatenation. +-- +-- @partTwo = 'calibrate' ['(:+:)', '(:*:)', '(:||:)']@ +partTwo :: [CalibrationEquation] -> Int +partTwo = calibrate [(:+:), (:*:), (:||:)] -- | Given a list of operators and a list of calibration equations, compute the --- calibration result, i.e., the sum of the test values from just the equations --- that could possibly be true. -calibrate :: [Int -> Int -> Maybe Int] -> [(Int, NonEmpty Int)] -> Int +-- sum of the test values from just the equations that could possibly be true. +-- +-- >>> calibrate [(:+:), (:*:)] <$> getExample +-- 3749 +-- +-- >>> calibrate [(:+:), (:*:), (:||:)] <$> getExample +-- 11387 +calibrate :: [Operator] -> [CalibrationEquation] -> Int calibrate operators = foldl' go 0 where go acc eq = @@ -38,44 +122,37 @@ calibrate operators = foldl' go 0 -- Process the operands from right to left, using the inverses of the given -- operators, to short circuit on operations that make the equation impossible. -- --- >>> isPossible [unAdd, unMultiply] (190, 10 :| [19]) +-- @190: 10 19@ has only one position that accepts an operator: between @10@ and +-- @19@. Choosing @+@ would give @29@, but choosing @*@ would give the test +-- value (@10 * 19 = 190@). +-- +-- >>> isPossible [(:+:), (:*:)] (190, 10 :| [19]) -- True -isPossible :: [Int -> Int -> Maybe Int] -> (Int, NonEmpty Int) -> Bool -isPossible operators (testValue, operand :| operands) = - operand `elem` foldrM go testValue operands - where - go x y = mapMaybe (\f -> f x y) operators - --- | The inverse of addition. -- --- >>> unAdd 40 121 --- Just 81 -unAdd :: Int -> Int -> Maybe Int -unAdd x y = [y - x | y >= x] - --- | The inverse of multiplication. +-- @3267: 81 40 27@ has two positions for operators. Of the four possible +-- configurations of the operators, /two/ cause the right side to match the test +-- value: @81 + 40 * 27@ and @81 * 40 + 27@ both equal @3267@ (when evaluated +-- left-to-right)! -- --- >>> unMultiply 19 190 --- Just 10 -unMultiply :: Int -> Int -> Maybe Int -unMultiply x y = [y `div` x | y `mod` x == 0] - --- | The inverse of concatenation. +-- >>> isPossible [(:+:), (:*:)] (3267, 81 :| [40, 27]) +-- True -- --- >>> unConcatenate 345 12345 --- Just 12 -unConcatenate :: Int -> Int -> Maybe Int -unConcatenate x y = [d | m == x] +-- @292: 11 6 16 20@ can be solved in exactly one way: @11 + 6 * 16 + 20@. +-- +-- >>> isPossible [(:+:), (:*:)] (292, 11 :| [6, 16, 20]) +-- True +isPossible :: [Operator] -> CalibrationEquation -> Bool +isPossible operators (testValue, operand :| operands) = + operand `elem` foldrM go testValue operands where - pow = numDigits x - (d, m) = y `divMod` (10 ^ pow) + go x y = mapMaybe (\operator -> execInverseOperation operator x y) operators -getInput :: IO [(Int, NonEmpty Int)] +-- | Parse the input into a list of calibration equations. +getInput :: IO [CalibrationEquation] getInput = parseInput (calibrationEquation `sepEndBy` newline) $(inputFilePath) --- | Parse a calibration equation, i.e., a test value and a nonempty list of --- operands. -calibrationEquation :: Parser (Int, NonEmpty Int) +-- | Parse a calibration equation. +calibrationEquation :: Parser CalibrationEquation calibrationEquation = (,) <$> (decimalInt <* string ": ") @@ -83,9 +160,11 @@ calibrationEquation = where decimalInt = fromInteger <$> decimal -getExample :: IO [(Int, NonEmpty Int)] +-- | Parse the example into a list of calibration equations. +getExample :: IO [CalibrationEquation] getExample = parseString (calibrationEquation `sepEndBy` newline) example +-- | The example. example :: String example = "190: 10 19\n\