Today’s challenge was relatively kind, although this tweet from Eric Wastl makes me fear that things could be about to get tricky.
Our input is a list of strings, but what we want is a sequence of the first character from each line, then the second and so on. Sadly F# doesn’t have a direct equivalent to Python’s zip function which is perfect for this problem. I could have repurposed the rotate function I created for day 3, which rotates an array of arrays to solve this, but actually it was simpler just to iterate the input sequence multiple times picking out a letter at a different index each time.
We’re looking for the most (or least for part b) frequent character in each column of the input, and so
Seq.countBy is ideal for this which returns a tuple of each character and its frequency. We can then use
Seq.minBy for part b)
decodePos function takes the sequence of input strings, the selector (
Seq.minBy) and the position index we are decoding:
let decodePos (messages:seq<string>) selector n = messages |> Seq.map (fun msg -> msg.[n]) |> Seq.countBy id |> selector snd |> fst
And now we just need to run this for each position, which we do by using the length of the first string in the list and applying
decodePos to each index. Note that in F# 4, constructors are first-class functions so I can pipe the array of characters directly into the
let decodeMessages (messages:string) selector = [|0..messages..Length-1|] |> Array.map (decodePos messages selector) |> System.String
Now we can use these functions to solve both parts of the problem:
let input = System.IO.File.ReadAllLines (__SOURCE_DIRECTORY__ + "\\input.txt") decodeMessages input Seq.maxBy |> printfn "Part a: %s" decodeMessages input Seq.minBy |> printfn "Part b: %s"
That was the code for my original solution, and you can view it on GitHub. But I wanted to try it with zip as well, so I made a poor man’s Python
zip that works for string arrays:
let zip (a:string) = [| for x in 0..a..Length-1 -> [| for y in a -> y.[x] |] |]
and this means we can implement
decodeMessages like this:
let decodeMessages selector (messages:string) = messages |> zip |> Array.map (Seq.countBy id >> selector snd >> fst) |> System.String
Although arguably, we should make this code a bit more readable by expressing the decoding of a single character from an array of characters into its own function. Something like this:
let decodeMessages selector = let decodeChar = Seq.countBy id >> selector snd >> fst zip >> (Array.map decodeChar) >> System.String
My alternative solution can be found here.
As always with these puzzles, there is no one “right” way to solve it. I like solutions that are succinct, readable, perform well, and easily adapted to changing requirements. But there’s usually a compromise between those constraints.