Today’s puzzle is called Gear Ratios.

Part 1

I am going to start by declaring a couple of types to help me. A digit can be a SymboolDigit or a NonSymbolDigit, where a SymbolDigit will be any digit with a symbol around it.

open System

type Digit = SymbolDigit of char | NonSymbolDigit of char
type Number = PartNumber of string | OtherNumber of string

I also want a few helper functions. Check if a character is a symbol (not a digit or dot).

let isSymbol chr = chr <> '.' && not (Char.IsDigit chr)

Also, need to check if the edges are symbols, so lets have a way of getting all edges for a particular point.

let getEdges (text: string[]) r c =
    seq { if r > 0 && c > 0 then yield (r-1, c-1)
          if r > 0 then yield (r-1, c)
          if r > 0 && c < text.[r-1].Length - 1 then yield (r-1, c+1)
          if c > 0 then yield (r, c-1)
          if c < text.[r].Length - 1 then yield (r, c+1)
          if r < text.Length - 1 && c > 0 then yield (r+1, c-1)
          if r < text.Length - 1 then yield (r+1, c)
          if r < text.Length - 1 && c < text.[r+1].Length - 1 then yield (r+1, c+1) }

Also, if there is a symbol on the edge make it a SymbolDigit, or else make it a NonSymbolDigit.

let asDigit text r c chr =
    let hasSymbol =
        getEdges text r c
        |> Seq.map (fun (r, c) -> text[r][c])
        |> Seq.exists isSymbol
    if hasSymbol then SymbolDigit chr
    else NonSymbolDigit chr

I think that is the main helper functions done, So I want the main function to extract the digit groups. This uses an inner recursive function.

let extract (text: string[]) =
    let rec extractRow r c (acc: List<Digit list>) (num: Digit list): List<Digit list> =
        if r >= text.Length then acc
        else if (c >= text[r].Length && num = []) then extractRow (r+1) 0 acc []
        else if (c >= text[r].Length) then extractRow (r+1) 0 (acc@[num]) []
        else
            let chr = text[r][c]
            if Char.IsDigit(chr) then
                let dig = asDigit text r c chr
                extractRow r (c+1) acc (num@[dig])
            else if num <> [] then
                extractRow r (c+1) (acc@[num]) []
            else extractRow r (c+1) acc []
    extractRow 0 0 [] []

This will result in a list of Digit lists, so lets have a function that will convert a digit list into a PartNumber or OtherNumber.

let asNumber (num: Digit list) =
    let rec partial (acc: Number) (remain: Digit list): Number =
        match remain, acc with
        | [], _ -> acc
        | SymbolDigit head::tail, PartNumber x -> partial (PartNumber $"{x}{head}") tail
        | SymbolDigit head::tail, OtherNumber x -> partial (PartNumber $"{x}{head}") tail
        | NonSymbolDigit head::tail, PartNumber x -> partial (PartNumber $"{x}{head}") tail
        | NonSymbolDigit head::tail, OtherNumber x -> partial (OtherNumber $"{x}{head}") tail
    partial (OtherNumber "") num

And finally, lets convert the PartNumber into integers and ignore the OtherNumber.

let partNumberAsInt (number: Number) =
    match number with
    | PartNumber x -> Some (int x)
    | OtherNumber x -> None

Now, with the sample text, this can all be put together.

let sampleText =
    "467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598.."

sampleText.Split('\n')
|> extract
|> List.map asNumber
|> List.choose partNumberAsInt
|> List.sum
4361

4361 is the correct answer with the sample input. My final answer is also correct. I get another gold star, and I can move on to part 2.

Part 2

Part two will require some changes, mostly a copy paste of Part 1 answer with the required changes in each function.

I am going to create a few more types, to describe what I am working with

type Gear = Gear of r: int * c: int
type GearDigit =
    | GearDigit of chr: char * gear: Gear
    | NoGearDigit of char
type GearNumber =
    | GearNumber of number: string * gear: Gear
    | NoGearNumber of number: string

And then a couple of helper methods

let isGear r c chr = if chr = '*' then Some (Gear (r, c)) else None

let asGearDigit text r c chr =
    let gear =
        getEdges text r c
        |> Seq.map (fun (r, c) -> text[r][c], r, c)
        |> Seq.choose (fun (x, r, c) -> isGear r c x)
        |> Seq.tryHead
    match gear with
    | Some g -> GearDigit (chr, g)
    | None -> NoGearDigit chr

The extract method is very similar

let extractGears (text: string[]) =
    let rec extractRow r c (acc: List<GearDigit list>) (num: GearDigit list): List<GearDigit list> =
        if r >= text.Length then acc
        else if (c >= text[r].Length && num = []) then extractRow (r+1) 0 acc []
        else if (c >= text[r].Length) then extractRow (r+1) 0 (acc@[num]) []
        else
            let chr = text[r][c]
            if Char.IsDigit(chr) then
                let dig = asGearDigit text r c chr
                extractRow r (c+1) acc (num@[dig])
            else if num <> [] then
                extractRow r (c+1) (acc@[num]) []
            else extractRow r (c+1) acc []
    extractRow 0 0 [] []

Similar to Part One, I now have a list of lists, so I will collect them into numbers, either with or without a gear.

let asGearNumber (num: GearDigit list) =
    let rec partial (acc: GearNumber) (remain: GearDigit list): GearNumber =
        match remain, acc with
        | [], _ -> acc
        | GearDigit (head, gear)::tail, GearNumber (x, _) -> partial (GearNumber ($"{x}{head}", gear)) tail
        | GearDigit (head, gear)::tail, NoGearNumber x -> partial (GearNumber ($"{x}{head}", gear)) tail
        | NoGearDigit head::tail, GearNumber (x, gear) -> partial (GearNumber ($"{x}{head}", gear)) tail
        | NoGearDigit head::tail, NoGearNumber x -> partial (NoGearNumber $"{x}{head}") tail
    partial (NoGearNumber "") num

The real difference starts now in matching, if two numbers have the same gear they are paired. I am going to use this function to convert to integers at the same time 1.

let numberPairs (numbers: GearNumber list) =
    let rec matchPair (num: (string * Gear) option) (remain: (string * Gear) list): (int * int) option =
        match num, remain with
        | _, [] -> None
        | Some (n, g), (no, go) :: _ when go = g -> Some (int n, int no)
        | Some (n, g), _ :: tail -> matchPair (Some (n, g)) tail
        | None, (no, go) :: tail -> matchPair (Some (no, go)) tail

    let rec findPairs acc remain =
        match remain with
        | head :: tail ->
            let pair = matchPair None (head::tail)
            match pair with
            | Some p -> findPairs (acc@[p]) tail
            | None -> findPairs acc tail
        | [] -> acc
    numbers
    |> List.choose (function | GearNumber (x, gear) -> Some (x, gear) | NoGearNumber _ -> None)
    |> findPairs []

This can be piped together and executed

sampleText.Split('\n')
|> extractGears
|> List.map asGearNumber
|> numberPairs
|> List.map (fun (a,b) -> a*b)
|> List.sum
467835

The result of 467835 is what I am expecting with the sample data. My final answer is also correct, and I do get another star.


  1. You could probably argue this violates separation of concerns (or SRP), which is a valuable rule in both functional and object oriented programming ↩︎