feat: finished day 2

This commit is contained in:
Ceferino Patino 2025-06-22 09:12:53 -05:00
commit 6622977d25
Signed by: c4patino
SSH key fingerprint: SHA256:Wu+cU1t+7zVT9wzew/4meNmeulo66NzMqc22MdDBgXI
11 changed files with 418 additions and 0 deletions

126
2023/aoc2023.cabal Normal file
View file

@ -0,0 +1,126 @@
cabal-version: 3.0
-- The cabal-version field refers to the version of the .cabal specification,
-- and can be different from the cabal-install (the tool) version and the
-- Cabal (the library) version you are using. As such, the Cabal (the library)
-- version used must be equal or greater than the version stated in this field.
-- Starting from the specification version 2.2, the cabal-version field must be
-- the first thing in the cabal file.
-- Initial package description 'aoc2023' generated by
-- 'cabal init'. For further documentation, see:
-- http://haskell.org/cabal/users-guide/
--
-- The name of the package.
name: aoc2023
-- The package version.
-- See the Haskell package versioning policy (PVP) for standards
-- guiding when and how versions should be incremented.
-- https://pvp.haskell.org
-- PVP summary: +-+------- breaking API changes
-- | | +----- non-breaking API additions
-- | | | +--- code changes with no API change
version: 0.1.0.0
-- A short (one-line) description of the package.
-- synopsis:
-- A longer description of the package.
-- description:
-- The license under which the package is released.
-- license:
-- The file containing the license text.
-- license-file:
-- The package author(s).
author: C4 Patino
-- An email address to which users can send suggestions, bug reports, and patches.
maintainer: c4patino@gmail.com
-- A copyright notice.
-- copyright:
build-type: Simple
-- Extra doc files to be distributed with the package, such as a CHANGELOG or a README.
-- extra-doc-files:
-- Extra source files to be distributed with the package, such as examples, or a tutorial module.
-- extra-source-files:
common warnings
ghc-options: -Wall
library
-- Import common warning flags.
import: warnings
-- Modules exported by the library.
exposed-modules: Day01, Day02
-- Modules included in this library but not exported.
-- other-modules:
-- LANGUAGE extensions used by modules in this package.
-- other-extensions:
-- Other library packages from which modules are imported.
build-depends: base ^>=4.19.2.0
-- Directories containing source files.
hs-source-dirs: src
-- Base language which the package is written in.
default-language: Haskell2010
executable aoc2023
-- Import common warning flags.
import: warnings
-- .hs or .lhs file containing the Main module.
main-is: Main.hs
-- Modules included in this executable, other than Main.
-- other-modules:
-- LANGUAGE extensions used by modules in this package.
-- other-extensions:
-- Other library packages from which modules are imported.
build-depends:
base ^>=4.19.2.0,
aoc2023
-- Directories containing source files.
hs-source-dirs: app
-- Base language which the package is written in.
default-language: Haskell2010
test-suite aoc2023-test
-- Import common warning flags.
import: warnings
-- Base language which the package is written in.
default-language: Haskell2010
-- Modules included in this executable, other than Main.
other-modules: Common.Tests, Day01Spec, Day02Spec
-- LANGUAGE extensions used by modules in this package.
-- other-extensions:
-- The interface type and version of the test suite.
type: exitcode-stdio-1.0
-- Directories containing source files.
hs-source-dirs: test
-- The entrypoint to the test suite.
main-is: Spec.hs
-- Test dependencies.
build-depends: base ^>=4.19.2.0, hspec, hspec-contrib, QuickCheck, HUnit, aoc2023

60
2023/app/Main.hs Normal file
View file

@ -0,0 +1,60 @@
module Main where
import qualified Day01
import qualified Day02
import System.Console.GetOpt
import System.Environment (getArgs)
import System.Exit (exitFailure)
data Options = Options {optFile :: Maybe FilePath, optHelp :: Bool} deriving (Show)
defaultOptions :: Options
defaultOptions = Options {optFile = Nothing, optHelp = False}
options :: [OptDescr (Options -> Options)]
options =
[ Option
['f']
["file"]
(ReqArg (\f opts -> opts {optFile = Just f}) "FILE")
"input file to use",
Option
['h']
["help"]
(NoArg (\opts -> opts {optHelp = True}))
"show help"
]
runDay :: Int -> FilePath -> IO (String, String)
runDay 1 filename = do
(a, b) <- Day01.run filename
return (show a, show b)
runDay 2 filename = do
(a, b) <- Day02.run filename
return (show a, show b)
runDay _ _ = return ("Not implemented", "Not implemented")
defaultFilename :: Int -> FilePath
defaultFilename day = "inputs/day" ++ (if day < 10 then "0" else "") ++ show day ++ "/input.txt"
main :: IO ()
main = do
args <- getArgs
case getOpt Permute options args of
(opts, [dayStr], []) -> do
let day = read dayStr
finalOpts = foldl (flip id) defaultOptions opts
filename = case optFile finalOpts of
Just f -> f
Nothing -> defaultFilename day
(part1Answer, part2Answer) <- runDay day filename
putStrLn $ "Part 1: " ++ part1Answer
putStrLn $ "Part 2: " ++ part2Answer
(_, [], []) -> do
putStrLn "Error: Day number is required"
putStrLn $ usageInfo "Usage: aoc2023 <day> [OPTIONS]" options
exitFailure
(_, _, errs) -> do
putStrLn $ concat errs
putStrLn $ usageInfo "Usage: aoc2023 <day> [OPTIONS]" options
exitFailure

View file

@ -0,0 +1,4 @@
1abc2
pqr3stu8vwx
a1b2c3d4e5f
treb7uchet

View file

@ -0,0 +1,7 @@
two1nine
eightwothree
abcone2threexyz
xtwone3four
4nineeightseven2
zoneight234
7pqrstsixteen

View file

@ -0,0 +1,5 @@
Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green

42
2023/src/Day01.hs Normal file
View file

@ -0,0 +1,42 @@
module Day01 (part1, part2, run) where
import Data.Char (isDigit)
import Data.List (isPrefixOf)
import Data.Maybe (maybeToList)
part1 :: [String] -> Int
part1 = sum . map (extractValue . filter isDigit)
part2 :: [String] -> Int
part2 = sum . map (extractValue . collectDigits)
run :: FilePath -> IO (Int, Int)
run filename =
readFile filename >>= \contents ->
let parsed = lines contents
in return (part1 parsed, part2 parsed)
collectDigits :: String -> String
collectDigits [] = []
collectDigits s@(c : cs)
| isDigit c = c : collectDigits cs
| otherwise = maybeToList (spelledDigitToChar s) ++ collectDigits cs
extractValue :: String -> Int
extractValue [] = 0
extractValue s@(d : _) = read [d, last s]
spelledDigitToChar :: String -> Maybe Char
spelledDigitToChar s = lookup True matches
where
matches =
[ ("one" `isPrefixOf` s, '1'),
("two" `isPrefixOf` s, '2'),
("three" `isPrefixOf` s, '3'),
("four" `isPrefixOf` s, '4'),
("five" `isPrefixOf` s, '5'),
("six" `isPrefixOf` s, '6'),
("seven" `isPrefixOf` s, '7'),
("eight" `isPrefixOf` s, '8'),
("nine" `isPrefixOf` s, '9')
]

76
2023/src/Day02.hs Normal file
View file

@ -0,0 +1,76 @@
module Day02 (part1, part2, run) where
import Text.Read (readMaybe)
data Game = Game {id :: Int, draw :: Draw} deriving (Show)
data Draw = Draw {red :: Int, green :: Int, blue :: Int} deriving (Show)
part1 :: [Game] -> Int
part1 = sum . map gid . filter possible
where
gid (Game i _) = i
part2 :: [Game] -> Int
part2 = sum . map power
where
power (Game _ (Draw r g b)) = r * g * b
run :: FilePath -> IO (Int, Int)
run filename = do
input <- readFile filename
let games = parseGames input
return (part1 games, part2 games)
instance Semigroup Draw where
(Draw a b c) <> (Draw x y z) = Draw (max a x) (max b y) (max c z)
instance Monoid Draw where
mempty = Draw 0 0 0
possible :: Game -> Bool
possible = valid . draw
where
valid (Draw r g b) = r <= 12 && g <= 13 && b <= 14
parseGames :: String -> [Game]
parseGames input = map parseGame $ lines input
parseGame :: String -> Game
parseGame s =
let (header, rest) = break (== ':') s
gameId = read (drop 5 header) :: Int
drawsStr = drop 2 rest
draws = parseDraws drawsStr
in Game gameId draws
parseDraws :: String -> Draw
parseDraws s =
let drawParts = splitOn ';' s
in foldl (<>) mempty (map parseDraw drawParts)
parseDraw :: String -> Draw
parseDraw s =
let counts = splitOn ',' s
in foldl (<>) mempty (map parseCount counts)
parseCount :: String -> Draw
parseCount s =
case words s of
[nStr, color] ->
case (readMaybe nStr :: Maybe Int) of
Just i -> case color of
"red" -> Draw i 0 0
"green" -> Draw 0 i 0
"blue" -> Draw 0 0 i
_ -> mempty
Nothing -> mempty
_ -> mempty
splitOn :: Char -> String -> [String]
splitOn _ [] = []
splitOn delim str =
let (first, rest) = break (== delim) str
in first : case rest of
[] -> []
(_ : rest') -> splitOn delim rest'

44
2023/test/Common/Tests.hs Normal file
View file

@ -0,0 +1,44 @@
module Common.Tests where
import Test.Hspec
-- | Represents a single test case with an input file and its expected output.
data TestCase a = TestCase
{ -- | Path to the input file for the test case.
filename :: FilePath,
-- | Expected output value for the test case, of any type 'a'.
expectedOutput :: a
}
-- | Run a suite of test cases by applying a runner function to input files,
-- then projecting a part of the result for comparison against expected outputs.
--
-- Parameters:
--
-- * @label@: parent label for the specification.
-- * @projector@: function to extract a single value from a tuple (e.g., part 1 or part 2).
-- * @runner@: function under test, which takes a filename and returns an IO action producing a tuple of outputs.
-- * @testCases@: list of test cases with input files and expected results.
--
-- Returns an Hspec 'Spec' running all test cases with descriptive labels and assertions.
mkPartSpec ::
(Eq a, Show a) =>
-- | parent label for the specification
String ->
-- | projector from a tuple to the value to test
((a, a) -> a) ->
-- | function under test (runner)
(FilePath -> IO (a, a)) ->
-- | list of test cases with input files and expected outputs
[TestCase a] ->
-- | resulting Hspec Spec with all tests
Spec
mkPartSpec label projector runner testCases =
describe label $
mapM_
( \(TestCase file expected) ->
it ("run " ++ file ++ " returns " ++ show expected) $ do
result <- runner file
projector result `shouldBe` expected
)
testCases

22
2023/test/Day01Spec.hs Normal file
View file

@ -0,0 +1,22 @@
module Day01Spec (spec) where
import Common.Tests (TestCase (..), mkPartSpec)
import Day01 (run)
import Test.Hspec
part1TestCases :: [TestCase Int]
part1TestCases =
[ TestCase "inputs/day01/part1.txt" 142,
TestCase "inputs/day01/input.txt" 55108
]
part2TestCases :: [TestCase Int]
part2TestCases =
[ TestCase "inputs/day01/part2.txt" 281,
TestCase "inputs/day01/input.txt" 56324
]
spec :: Spec
spec = do
mkPartSpec "Day 1/1 tests" fst run part1TestCases
mkPartSpec "Day 1/2 tests" snd run part2TestCases

22
2023/test/Day02Spec.hs Normal file
View file

@ -0,0 +1,22 @@
module Day02Spec (spec) where
import Common.Tests (TestCase (..), mkPartSpec)
import Day02 (run)
import Test.Hspec
part1TestCases :: [TestCase Int]
part1TestCases =
[ TestCase "inputs/day02/example.txt" 8,
TestCase "inputs/day02/input.txt" 2617
]
part2TestCases :: [TestCase Int]
part2TestCases =
[ TestCase "inputs/day02/example.txt" 2286,
TestCase "inputs/day02/input.txt" 59795
]
spec :: Spec
spec = do
mkPartSpec "Day 2/1 tests" fst run part1TestCases
mkPartSpec "Day 2/2 tests" snd run part2TestCases

10
2023/test/Spec.hs Normal file
View file

@ -0,0 +1,10 @@
module Main (main) where
import qualified Day01Spec
import qualified Day02Spec
import Test.Hspec
main :: IO ()
main = hspec $ do
Day01Spec.spec
Day02Spec.spec