Advent of Code 2022 - Day 1: Calorie Counting
It's that time of year again! We have 25 days to collect 50 stars and save Christmas. That's right: it's Advent of Code. This year I am solving the puzzles using Haskell, and I plan to write about each day's solution here.
Day 1: Calorie Counting is a bit of a warm-up, as usual for the first day's challenge. This year, we have input like this:
1000 2000 3000 4000 5000 6000 7000 8000 9000 10000
Each grouping represents the snacks carried by a single elf, and each line is the calorie count for an individual snack. In Part 1, we need to sum up each group and find the greatest. In Part 2, we need to find the three (3) greatest.
Here's my first attempt at solving the problem.
import Data.List import Data.List.Split import Input import Test.Hspec input :: IO String input = readDay 2022 1 spec :: IO () spec = hspec $ do describe "Day 01" $ do describe "Part 01" $ do it "runs on custom input" $ do myInput <- input part1 myInput `shouldBe` 0 -- redacted describe "Part 02" $ do it "runs on custom input" $ do myInput <- input part2 myInput `shouldBe` 0 -- redacted part1 :: String -> Int part1 = maximum . calorieCounter part2 :: String -> Int part2 = sum . take 3 . reverse . sort . calorieCounter calorieCounter :: String -> [Int] calorieCounter = fmap (sum . fmap read . lines) . splitOn "\n\n"
I use Hspec to run my code as a test suite. Once I find the correct answer, I add it as an expectation. This allows me to refactor my code more easily, either in solving Part 2 or in cleanup afterwards.
Input.readDay is a simple helper method that reads my input file from a local directory.
There were a few things I wanted to improve with this solution:
- Part 1 and Part 2 can both be viewed as "Take the N greatest elves and sum them". But the code for Part 1 and Part 2 were pretty different. I wanted to move the sorting into the
calorieCounter, so that it could be used in both parts.
reverseforces the full list to be evaluated, so
reverse . sortsorts the whole list even when we only need a few top items. The Data.Ord module provides a
Downtype which can sort in descending order. Combined with
sortOn, this allows us to lazily sort only as much of the list as we need to take.
- Is there any way to simplify the
splitOncan operate on any list, not just Strings (aka,
[Char]). We should be able to use
linesfirst, and then split the result into groups, rather than split into groups first, and then
splitOn "\n\n", we would use
splitOn [""]. Since each item in the list is a line, our delimiter is now one item long. We can replace
splitOn [""] with
The final code looks like this:
import Data.List import Data.List.Split part1 :: String -> Int part1 = head . calorieCounter part2 :: String -> Int part2 = sum . take 3 . calorieCounter calorieCounter :: String -> [Int] calorieCounter = sortOn Down . fmap (sum . fmap read) . splitWhen null . lines
Polish I didn't do:
I'm still using
part1 instead of
sum . take 1. It's not quite equivalent! While
head is more concise, it could bottom on an empty input.
sum . take 1will always return an Int, defaulting to 0 for an empty list. With this option, we could add the number of items to take as an argument to
part1 = calorieCounter 1and
part2 = calorieCounter 3
A third option is
listToMaybe, which would return
Just 69836 for my input and
None for an empty input.
Advent of Code 2022 Series
This post is part of a series describing my Haskell solutions to Advent of Code 2022.