A bit late to start the event this year, as life got in the way a bit. But better late than never.
Day 1
A simple warm-up task. Read a text file, convert to numbers, and find a few numbers that match a criterion. I did entertain the idea of using a Set
of numbers, but decided a List
as good enough. I thought aboutdoing something clever with using the first number (or numbers) to calculate the last one, then checking of that calculated number existed. But there are only a few numbers, so a brute force search of the Cartesian product of the numbers is good enough.
List comprehensions to the rescue! This is the entirety of the code (but note the use of the visible type application when converting the strings to numbers). I order the numbers in the comprehension so I only find each solution once. I also assume that all the given numbers are distinct.
main :: IO ()
main =
do numStrs <- readFile "data/advent01.txt"
let nums = map (read @Int) $ lines numStrs
print $ head $ part1 nums
print $ head $ part2 nums
part1 nums = [ x * y
| x <- nums
, y <- nums
, x < y
, x + y == 2020
]
part2 nums = [ x * y * z
| x <- nums
, y <- nums
, z <- nums
, x < y
, y < z
, x + y + z == 2020
]
Day 2
A bit more data structuring here. I decided to represent the policies as a list of records, defined like this:
data Policy = Policy
{ lower :: Int
, upper :: Int
, character :: Char
, password :: String
} deriving (Show, Eq, Ord)
I used Megaparsec again to parse the input file. It's a bit more cumbersome than using a regular expression, but much of it is boilerplate. I find the final parser is much more readable, mainly because all the parts have names, unlike the various groups in a regex.
-- Parse the input file
type Parser = Parsec Void Text
sc :: Parser ()
sc = L.space space1 CA.empty CA.empty
lexeme = L.lexeme sc
integerP = lexeme L.decimal
symb = L.symbol sc
hyphenP = symb "-"
colonP = symb ":"
characterP = alphaNumChar <* sc
passwordP = some alphaNumChar <* sc
policiesP = many policyP
policyP = Policy <$> integerP <* hyphenP <*> integerP <*> characterP <* colonP <*> passwordP
The enforcement of rules is relatively direct as well. The're both taking the length
of the set of policies after applying a filter
. For part 1, count the number of occurrences of the given character, then check it's in range. For part 2, check the character at the two positions and check that exactly one of them matches. (For Booleans, not-equal-to, /=
, is the same as XOR.)
part1 = length . filter inRange
where nCharsPresent p = length $ filter (== (character p)) (password p)
inRange p = (nCharsPresent p >= (lower p)) && (nCharsPresent p <= (upper p))
part2 = length . filter matches
matches p = ((pw!!l) == c) /= ((pw!!u) == c)
where pw = password p
c = character p
l = (lower p) - 1
u = (upper p) - 1
Code
You can find the code somewhere here or on Gitlab.