Posted in:

Today’s Advent of Code challenge was particularly well suited to regular expressions (and reminiscent of last year’s naughty and nice strings problem). We have a list of “ip addresses” and need to test them according to some rules.

The first task was to split them out into “supernet” and “hypernet” sequences, so the string “abc[def]ghi” has two “supernet” sequences of abc and ghi and one hypernet sequence of def.

I did manage to come up with a regular expression that can find strings enclosed in square brackets, and by doing that I was able to use Regex.Split to get them into an alternating sequence of supernet, hypernet, supernet etc. So I then needed to split out every odd and even into separate sequences. I think I made a bit heavy weather of this, so do let me know if there’s a more elegant way. But here’s what I came up with:

let filterByIndex predicate sequence = 
    sequence |> Seq.indexed |> Seq.filter (fst >> predicate) |> Seq.map snd 

let parseIpAddress ipAddress =
    let parts = Regex.Split(ipAddress,@"\[(.*?)\]")
    let supernetSequences = parts |> filterByIndex (fun n -> n % 2= 0) |> Seq.toArray 
    let hypernetSequences = parts |> filterByIndex (fun n -> n % 2= 1) |> Seq.toArray
    supernetSequences, hypernetSequences

Now we needed to test each ip address. Part of this was looking for an “abba” string – two letters next to each other, with two different letters on either side. I attempted to solve this with regular expressions. “(\w)(\w)\2\1” is close but doesn’t stipulate that the inner characters are different to the outer ones. In the end I realised that it would make more sense to use Seq.windowed, which turns a sequence like “abcdef” into “abc”, “bcd”, “def” and use a pattern matching function to test for an “abba”:

let containsAbba s = s |> Seq.windowed 4 |> Seq.exists (function | [|a;b;c;d|] -> a=d&&b=c&&a<>b | _ -> false)

Now we can solve the first part of the problem by checking if there is a supernet containing an “abba” but no hypernets do:

let supportsTls ipAddress = 
    let super,hyper = parseIpAddress ipAddress
    let containsAbba s = s |> Seq.windowed 4 |> Seq.exists (function | [|a;b;c;d|] -> a=d&&b=c&&a<>b | _ -> false)
    (super |> Array.exists containsAbba) && not (hyper |> Array.exists containsAbba)

input |> Seq.filter supportsTls |> Seq.length |> printfn "Part a: %d"

Part b had a similar problem, we looked for instances of “aba” (same character repeated separated by a different character) inside of the supernets which we again could use Seq.windowed for. And then each “aba” gets turned into a “bab” by switching the inner and outer characters. Then we just need to find if the corresponding “bab” for one of the “aba”s can be found in the hypernets.

So my code for part b ended up like this:

let supportsSsl ipAddress = 
    let super,hyper = parseIpAddress ipAddress
    let findAbas s = s |> Seq.windowed 3 |> Seq.filter (function | [|a;b;c|] -> a=c&&a<>b | _ -> false) |> Seq.map System.String
    let abas = super |> Seq.collect findAbas
    let makeBab (aba:string) = sprintf "%c%c%c" aba.[1] aba.[0] aba.[1]
    let babExists bab = hyper |> Seq.exists (fun s -> s.Contains(bab))
    super |> Seq.collect findAbas |> Seq.exists (makeBab >> babExists)

input |> Seq.filter supportsSsl |> Seq.length |> printfn "Part b: %d"

Overall I feel that my solution for today could certainly be cleaned up a bit, but it does at least work, and I’m particularly proud that so far this year, I’ve entered the correct answer for each puzzle first time. I suspect its a combination of reading the question more carefully, using the example test cases, and using a functional programming language. I’ve still got nowhere near the leaderboard, but I can at least hide behind the excuse that the problems go live at 5am here, so the leaderboard is already full by the time I start the problems.