AoC 2023, Day 3, Gear Ratios
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 (r1, c1)
if r > 0 then yield (r1, c)
if r > 0 && c < text.[r1].Length  1 then yield (r1, c+1)
if c > 0 then yield (r, c1)
if c < text.[r].Length  1 then yield (r, c+1)
if r < text.Length  1 && c > 0 then yield (r+1, c1)
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.

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