diff --git a/2023/aoc2023.cabal b/2023/aoc2023.cabal new file mode 100644 index 0000000..1748b5a --- /dev/null +++ b/2023/aoc2023.cabal @@ -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 diff --git a/2023/app/Main.hs b/2023/app/Main.hs new file mode 100644 index 0000000..369f1cf --- /dev/null +++ b/2023/app/Main.hs @@ -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 [OPTIONS]" options + exitFailure + (_, _, errs) -> do + putStrLn $ concat errs + putStrLn $ usageInfo "Usage: aoc2023 [OPTIONS]" options + exitFailure diff --git a/2023/inputs/day01/part1.txt b/2023/inputs/day01/part1.txt new file mode 100644 index 0000000..7bbc69a --- /dev/null +++ b/2023/inputs/day01/part1.txt @@ -0,0 +1,4 @@ +1abc2 +pqr3stu8vwx +a1b2c3d4e5f +treb7uchet diff --git a/2023/inputs/day01/part2.txt b/2023/inputs/day01/part2.txt new file mode 100644 index 0000000..41aa89c --- /dev/null +++ b/2023/inputs/day01/part2.txt @@ -0,0 +1,7 @@ +two1nine +eightwothree +abcone2threexyz +xtwone3four +4nineeightseven2 +zoneight234 +7pqrstsixteen diff --git a/2023/inputs/day02/example.txt b/2023/inputs/day02/example.txt new file mode 100644 index 0000000..295c36d --- /dev/null +++ b/2023/inputs/day02/example.txt @@ -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 diff --git a/2023/src/Day01.hs b/2023/src/Day01.hs new file mode 100644 index 0000000..04fbcfd --- /dev/null +++ b/2023/src/Day01.hs @@ -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') + ] diff --git a/2023/src/Day02.hs b/2023/src/Day02.hs new file mode 100644 index 0000000..3514dbf --- /dev/null +++ b/2023/src/Day02.hs @@ -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' diff --git a/2023/test/Common/Tests.hs b/2023/test/Common/Tests.hs new file mode 100644 index 0000000..1793bbe --- /dev/null +++ b/2023/test/Common/Tests.hs @@ -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 diff --git a/2023/test/Day01Spec.hs b/2023/test/Day01Spec.hs new file mode 100644 index 0000000..c4bd831 --- /dev/null +++ b/2023/test/Day01Spec.hs @@ -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 diff --git a/2023/test/Day02Spec.hs b/2023/test/Day02Spec.hs new file mode 100644 index 0000000..1277c93 --- /dev/null +++ b/2023/test/Day02Spec.hs @@ -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 diff --git a/2023/test/Spec.hs b/2023/test/Spec.hs new file mode 100644 index 0000000..ebe4bc1 --- /dev/null +++ b/2023/test/Spec.hs @@ -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