Advent of Code Day 2: 1% Inspiration, 99% Parse-piration

Posted on December 2, 2020

Today’s problem is… password validation? As you can guess from the title, the vast majority of the work was just parsing the puzzle input. The puzzle input looks like the following:

1-3 a: abcde
1-3 b: cdefg
2-9 c: ccccccccc

which is meant to be interpreted as passwords together with whatever policy requirements each password needed to meet. We want to parse out each line into the following data type:

data PasswordEntry = PasswordEntry
  { _policy :: ((Int, Int), Char),
    _password :: String
  }

It’d be pretty reasonable to reach for a regex here, although in Haskell it might be more idiomatic to use a parser combinator library like Megaparsec (and that’s what I’d probably do in production). For a toy case like this, though, if you’re willing to swallow writing a partial function, the nested pattern-matching approach is actually quite nice:

parsePasswordEntry :: Text -> PasswordEntry
parsePasswordEntry line =
  let [rawRange, [char, ':'], password] = words $ Text.unpack line
      (rawMin, '-' : rawMax) = break (== '-') rawRange
   in PasswordEntry ((read rawMin, read rawMax), char) password

This is a nice reminder that Haskell makes for a pretty pleasant slap-something-together language even when you don’t care about production-readiness, thanks to the expressiveness of pattern matching. Haskell gets (rightfully) bashed for having its default String type be just a type synonym for [Char], but we benefit from it here by getting to pattern match on the resulting lists like this.

The actual password validation bits are quite simple. For part 1, we interpret the policy as giving us a character whose number of occurrences in the password needs to fall within a particular range:

isValid1 :: PasswordEntry -> Bool
isValid1 (PasswordEntry ((lo, hi), char) password) = 
  lo <= count && count <= hi
  where
    count = length $ filter (== char) password

answer1 :: [PasswordEntry] -> Int
answer1 inp = length $ filter isValid1 inp

Don’t have much to add here. Partially-applied operators like in (== char) are nice for readability. For part 2, we interpret the policy’s numbers as (one-indexed) indices, where the provided character must appear at exactly one of the two indices in the password:

isValid2 :: PasswordEntry -> Bool
isValid2 (PasswordEntry ((lo, hi), char) password) = 
  checkInd lo /= checkInd hi
  where
    checkInd ind = password ^? ix (ind - 1) == Just char

answer2 :: [PasswordEntry] -> Int
answer2 inp = length $ filter isValid2 inp
  • (/=) on booleans is xor :)
  • The ^? ix bit is there because I habitually import Lens.Micro.Platform for convenience, but you could do just as well with any other safe index (or even the unsafe !!).

As always, my full solution is on GitHub: https://github.com/ewilden/aoc-2020/blob/main/src/Day02.hs