Posted in:

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 =
    |> 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>
                        |> (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") |> 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.