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.
First Solution
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.
Polishing
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. reverse
forces the full list to be evaluated, soreverse . sort
sorts the whole list even when we only need a few top items. The Data.Ord module provides aDown
type which can sort in descending order. Combined withsortOn
, this allows us to lazily sort only as much of the list as we need to take.- Is there any way to simplify the
fmap
?splitOn
can operate on any list, not just Strings (aka,[Char]
). We should be able to uselines
first, and then split the result into groups, rather than split into groups first, and thenfmap lines
.
Instead of 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 splitWhen null
.
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 head
in 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 1
will 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 calorieCounter
: part1 = calorieCounter 1
and 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.
Cheers!