Posted in:

Day 4’s challenge involved calculating “checksums” and decrypting data. Each line of the input contained an encrypted room name, a sector id and a checksum. Regular expressions were the obvious way to parse the input into a custom “Room” record type.

A few days practice means that I can at least use regular expressions fairly easily in F#, although it seems a shame that it takes several steps to parse out the matches from each group when other languages can often do this in a single line:

type Room = { name:string; sectorId:int; checksum: string}

let parseRoom room =
    Regex.Match(room,@"([a-z\-]+)(\d+)\[([a-z]+)\]").Groups
     |> Seq.cast<Group>
     |> Seq.map (fun g -> g.Value)
     |> Seq.toList
     |> function | [_;a;b;c] -> {name=a;sectorId=int b; checksum=c} | x -> failwith "Parse Error"

The next step was calculating the “checksum” for a room, which basically meant counting the frequency of letters in the room name, and picking out the five most common. The rules for sorting the letters were by frequency first and then alphabetical order. The countBy function is perfect for counting the characters in a string, and sorting by a tuple of frequency first (descending) and then character was idea. One thing that tripped me up is that F# has a String.concat which has a different signature from System.String.Concat

let calcChecksum (roomName:string) =
    roomName.Replace("-","") 
    |> Seq.countBy id 
    |> Seq.sortBy (fun (a,b)->(-b,a))
    |> Seq.map (fst >> string)
    |> Seq.take 5
    |> System.String.Concat 

With input parsing and checksum calculation functions in place, the rest of the problem is trivial. Filter out the rooms with invalid checksums and sum the valid rooms by their “sector Id”

let isRealRoom room = calcChecksum room.name = room.checksum
let input = System.IO.File.ReadAllLines (__SOURCE_DIRECTORY__ + "\\input.txt")
input |> Array.map parseRoom |> Array.filter isRealRoom |> Seq.sumBy (fun r -> r.sectorId) |> printfn "Part a: %d"

Part b of the problem required us to implement the Caesar cipher to decrypt room names. I made one function to decrypt a single character which is trivial apart from having to fight the F# compiler to let me do arithmetic on chars, and a function to decrypt a room name for which I used a pattern matching function in order to deal with the special case of dashes in the encrypted name

let shiftBy (n:int) (c:char) = char (int 'a' + (int c - int 'a' + n) % 26)
let decryptName n = Seq.map (function | '-' -> ' ' | a -> shiftBy n a) >> System.String.Concat

You may have noticed that in decryptName I use the function composition operator (>>). After a while using F#, you start to realise that the following two functions are equivalent:

let doubleThenSquare n = n |> double |> square
let doubleThenSquare2 = double >> square

With our decrypting function in place, it was just a matter of decrypting all the room names and looking for the one we were supposed to find (north pole storage) so we could get its “sector id”.

let decryptRoom r = decryptName r.sectorId r.name
let isStorageRoom r = (decryptRoom r).StartsWith("northpole object storage")
let storageRoom = input |> Array.map parseRoom |> Array.filter isRealRoom |> Array.find isStorageRoom
printfn "Part b: %d" storageRoom.sectorId

You can check out my full solution on GitHub. As usual my solution was nowhere near as concise as other people managed in different languages, but I’m fairly pleased with today’s solution, as I think it shows some of the benefits of good functional programming. We’re writing lots of small functions to solve individual pieces of the problem, which can be easily combined to solve the overall problem. Writing code like this means our solution is easier to understand, and also has built-in flexibility to help us answer other similar questions about our input data if we need to.

As always, let me know how you solved the problem or any ways you think my solution can be improved.