It's already been more than two weeks since I started my little Advent of Code + Haskell adventure. So it's time for a little update. Here's what I got done.
Regular expressions are often my first choice when I have to parse something. Like in this case, to get the values easily out of strings like
6-8 b: bbbnvbbb. How hard can that be? Harder than I thought. After spending quite some in the docs and repl I was close to giving up. But then I found an article that really helped. So, I finally got a working
parse function that returns the four values of a given line as a list of strings:
parse :: String -> [String] parse input = drop 1 (getAllTextSubmatches $ input =~ "(.+)-(.+) (.): (.+)" :: [String])
From there on things went smoothly.
Part 1: Just some range checking
policy1 min max char str = cnt >= min && cnt <= max where cnt = length (filter (==char) str)
Part 2: Just some good old XOR
charAt p str = if length str > p then str !! p else ' ' policy2 p1 p2 char str = (charAt (p1 - 1) str == char) /= (charAt (p2 - 1) str == char)
But wait. How do I pass the list of strings to the policy functions? I introduced a little helper that does all the destructuring, conversion and execution.
isValid policy [min, max, char, str] = policy (read min :: Int) (read max :: Int) (head char) str
Finally, plugging it all together:
validCount xs = length (filter (==True) xs) solve input = do let pwData = map parse (lines input) putStrLn ("Part 1: " ++ (show (validCount (map (isValid policy1) pwData)))) putStrLn ("Part 2: " ++ (show (validCount (map (isValid policy2) pwData))))
This one was really easy and there isn't much I have to say. The only thing I had to look up was how to do a modulo. The rest was pretty straight forward.
tree x map = if (map !! (x `mod` (length map))) == '#' then 1 else 0 traverseMap :: [String] -> Int -> Int -> Int -> Int traverseMap  px right down = 0 traverseMap (x:xs) px right down = tree px x + traverseMap (drop (down - 1) xs) (px + right) right down solve input = do let traverse = traverseMap (lines input) 0 putStrLn ("Part 1: " ++ (show (traverse 3 1))) putStrLn ("Part 2: " ++ (show ((traverse 1 1) * (traverse 3 1) * (traverse 5 1) * (traverse 7 1) * (traverse 1 2))))
In the solution for this puzzle I wrote in PHP I used multiple regexes. After my previous struggle with regexes in Haskell, I tried to avoid them this time.
I wanted to have a structure that could be used to solve both parts of the puzzle. A map seemed like a good fit for both, the passport data (field names and values) and the schema (field names and validator functions).
passportSchema = Map.fromList [ ("byr", isBetween 1920 2002), ("iyr", isBetween 2010 2020), ("eyr", isBetween 2020 2030), ("hgt", isValidHeight), ("hcl", isHexColor), ("ecl", isEyeColor), ("pid", isPid) ]
isEyeColor verify that a value is part of a given set using
isPid checks the string length and validates characters using a combination of
isHexColor is basically the same, except that it checks the characters using
isHexDigit and that the string starts with
isBetween min max v = (read v :: Int) `elem` [min..max] isEyeColor v = v `elem` ["amb", "blu", "brn", "gry", "grn", "hzl", "oth"] isPid v = length v == 9 && all isDigit v isHexColor v = hash == '#' && length values == 6 && all isHexDigit values where (hash:values) = v
isValidHeight was a little bit trickier.
The function splits the numeric value from the unit by using
dropWhile in combination with
isDigit. Although this solution isn't as strict as a proper regex, it did the job for this puzzle.
isValidHeight v = let unit = dropWhile isDigit v value = takeWhile isDigit v in (unit == "cm" && between 150 193 value) || (unit == "in" && between 59 76 value)
To convert a single passport from the puzzle input to the map structure I use
splitOneOf to split the fields by either space or newline. All fields will then be splitted into key/value pairs using
splitOn and mapped to a tuple which is needed for
toPassport input = Map.fromList (map (\x -> (head x, last x)) (map (splitOn ":") (splitOneOf " \n" input)))
Part 1: A passport is valid if it has at least all fields defined in the schema.
hasAllRequiredFields schema passport = length ((Map.keys schema) \\ (Map.keys passport)) == 0
Part 2: A passport is valid if all values can be validated. If a field is missing in a passport, the
Nothing clause in the
case expression will make sure that the validation result is
isFieldValid passport field isValid = case Map.lookup field passport of Nothing -> False Just x -> isValid x allFieldsValid schema passport = Map.foldrWithKey (\field validator acc -> acc && (isFieldValid passport field validator)) True schema
Applying it to all passports and counting the results:
countValid l = length (filter (==True) l) solve :: String -> IO () solve input = do let passports = map toPassport (splitOn "\n\n" input) putStrLn ("Part 1: " ++ (show (countValid (map (hasAllRequiredFields passportSchema) passports)))) putStrLn ("Part 2: " ++ (show (countValid (map (allFieldsValid passportSchema) passports))))
This one's puzzle input is just a bunch of binary strings in disguise. No fancy stuff needed. Just folding it down bit by bit.
charToBit x | x `elem` "FL" = 0 | x `elem` "BR" = 1 seatId input = foldl (\acc x -> acc * 2 + (charToBit x)) 0 input
Part 1: The
maximum function is all that's needed to find the highest seat number
Part 2: To find the empty seat, I filter down the list of seat ids to the one that has its next id missing, but the one after next exists. Adding one gives us the empty seat id.
emptySeat seatIds = (head (filter (\x -> ((x + 1) `notElem` seatIds) && ((x + 2) `elem` seatIds)) seatIds)) + 1 solve input = do let seatIds = map seatId (lines input) putStrLn ("Part 1: " ++ (show (maximum seatIds))) putStrLn ("Part 2: " ++ (show (emptySeat seatIds)))
nub already returns the unique elements of a list. Only thing left to do: Removing everything that's not an alpha character using a filter so newlines doesn't count.
countAny group = length (nub (filter isAlpha group))
Part 2: I used
foldl1 to build the intersection of all answers within a group.
foldl1 is just like
foldl except that it starts with the first element in the accumulator already. Perfect for the job!
countAll group = length (foldl1 (\acc x -> acc `intersect` x) (lines group))
Nothing unexpected to see here
solve input = do let groups = splitOn "\n\n" input putStrLn ("Part 1: " ++ (show (sum (map countAny groups)))) putStrLn ("Part 2: " ++ (show (sum (map countAll groups))))
Well, I really haven't gotten that far. It's not that I'm lazy - at least not that lazy. I'm still solving the puzzles every day in PHP but my plan to catch up with Haskell afterwards hasn't worked out that well. The advent isn't yet over, though. Let's see what I can squeeze into the last few days.
Is there something I did overly complicated or goofy? Let me know. I'm still a Haskell novice and looking to improve.