Today’s Advent of Code challenge was a relatively kind one, and I’m pretty sure if I tried hard enough I could get this down to a single line of code. But a great article from Pierre Irrmann reminded me that with F# it can be all too easy to go overboard on pipelines and composition and end up with clever code that is incomprehensible.
So I decided to take an “outside” in approach. I needed a
solve function that took a sequence defining the “disks” and would return an integer by finding the first time at which all the disks were aligned. The disk definitions were tuples of number of positions and starting position (if I’d been following all of Pierre’s advice these would have been types instead). And the
isSolution function I stubbed out to return true. Obviously this meant my test case would return 0 initially.
let solve disks = Seq.initInfinite id |> Seq.find (isSolution disks) solve [(5,4);(2,1)] |> printfn "Test: %d"
So next was to implement
isSolution. If start time is 5, then the first disk must be open at 6, the second disk must be open at 7 and so on. So I used
Seq.indexed to get a tuple of the disk definition and an incrementing number, and then tested whether for each disk and time offset the slot was open. Again
isOpenAtT had not been implemented yet so was hard-coded to return true.
let isSolution disks startTime = disks |> Seq.indexed |> Seq.forall (fun (n,disk) -> isOpenAtT disk (startTime+1+n))
Finally, my outside in approach led me to create
isOpenAtT, which is simple to calculate:
let isOpenAtT (positions,startPos) time = ((startPos + time) % positions) = 0
I then used regular expressions to parse my input data:
open System.Text.RegularExpressions let parseInput input = let parts = Regex.Matches(input,@"\d+") |> Seq.cast<Match> |> Seq.map (fun m-> int m.Value) |> Seq.toList match parts with | [_;pos;_;start] -> (pos,start) | _ -> failwith ("parse error" + input) let discs = System.IO.File.ReadAllLines (__SOURCE_DIRECTORY__ + "\\input.txt") |> Array.map parseInput
And now could pass that input data into the solve function to solve parts a and b:
solve discs |> printfn "Part a: %d" solve (Seq.append discs [(11,0)]) |> printfn "Part b: %d"
As I said, there’s no doubt the solution could be made much more succinct, but the outside-in approach of starting with the
solve function worked very well and is one I will have to try again in future. Full code is available on GitHub as usual.