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 (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.
-
You could probably argue this violates separation of concerns (or SRP), which is a valuable rule in both functional and object oriented programming ↩︎