Skip to content

Instantly share code, notes, and snippets.

@ncalm
Last active December 6, 2023 18:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ncalm/4094c28b64925193e8ffe4a4b5455886 to your computer and use it in GitHub Desktop.
Save ncalm/4094c28b64925193e8ffe4a4b5455886 to your computer and use it in GitHub Desktop.
Learning F# - Advent of Code 2023 - Day 3
--- Day 3: Gear Ratios ---
You and the Elf eventually reach a gondola lift station; he says the gondola lift will take you up to the water source, but this is as far as he can bring you. You go inside.
It doesn't take long to find the gondolas, but there seems to be a problem: they're not moving.
"Aaah!"
You turn around to see a slightly-greasy Elf with a wrench and a look of surprise. "Sorry, I wasn't expecting anyone! The gondola lift isn't working right now; it'll still be a while before I can fix it." You offer to help.
The engineer explains that an engine part seems to be missing from the engine, but nobody can figure out which one. If you can add up all the part numbers in the engine schematic, it should be easy to work out which part is missing.
The engine schematic (your puzzle input) consists of a visual representation of the engine. There are lots of numbers and symbols you don't really understand, but apparently any number adjacent to a symbol, even diagonally, is a "part number" and should be included in your sum. (Periods (.) do not count as a symbol.)
Here is an example engine schematic:
467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598..
In this schematic, two numbers are not part numbers because they are not adjacent to a symbol: 114 (top right) and 58 (middle right). Every other number is adjacent to a symbol and so is a part number; their sum is 4361.
Of course, the actual engine schematic is much larger. What is the sum of all of the part numbers in the engine schematic?
// define a record type with four fields: number, rowindex, startcolumn, endcolumn
type Number = { Number: int; StartColumn: int; EndColumn: int }
// Function to test if a character is neither a period nor a digit, using regex
let isNotDigitOrPeriod (c: char) = not (Regex.IsMatch(string c, "[0-9\.]"))
// Extract the numbers and their starting and ending positions from a line
let extractNumbers (threeLines: string[]) =
// if threeLines doesn't contain three elements, exit
if threeLines.Length <> 3 then failwith "threeLines must contain three elements"
let line = threeLines.[1] |> string
let previousLine = threeLines.[0] |> string
let nextLine = threeLines.[2] |> string
line
|> fun s -> Regex.Matches(s, "[0-9]+")
|> Seq.map (fun m -> { Number = m.Value |> int; StartColumn = m.Index; EndColumn = m.Index + m.Length - 1 })
|> Seq.filter (fun n ->
let left = max 0 (n.StartColumn - 1)
let right = min (line.Length - 1) (n.EndColumn + 1)
let above = previousLine.[left .. right] |> Seq.exists isNotDigitOrPeriod
let below = nextLine.[left .. right] |> Seq.exists isNotDigitOrPeriod
let this = line.[left .. right] |> Seq.exists isNotDigitOrPeriod
above || below || this)
// return a sequence of the Number from the record
|> Seq.map (fun n -> n.Number)
// Get the line before, the line itself, and the line after. If the line is zero, put the line after as the line before
let getThreeLines (rowIndex: int) =
let line = lines.[rowIndex]
let previousLine = if rowIndex > 0 then lines.[rowIndex - 1] else lines.[rowIndex + 1]
let nextLine = lines.[rowIndex + 1]
[| previousLine; line; nextLine |]
let processALine (i: int) (processFunction) =
i |> getThreeLines |> processFunction
// process all lines and return a single list of numbers
let numbers =
lines
|> Seq.take (lines.Length - 1)
|> Seq.mapi (fun i line -> i, processALine i extractNumbers)
|> Seq.map (fun (i, numbers) -> numbers)
|> Seq.concat
numbers |> Seq.sum |> printfn "%d"
type Gear = { Position: int }
// Extract the numbers and their starting and ending positions from a line
let getGearPositions (line: string) =
line
|> fun s -> Regex.Matches(s, "\*")
|> Seq.map (fun m -> m.Index)
let getNumPositions (threeLines: string[]) =
// if threeLines doesn't contain three elements, exit
if threeLines.Length <> 3 then failwith "threeLines must contain three elements"
let getPositions (s: string) =
s
|> fun s -> Regex.Matches(s, "[0-9]+")
|> Seq.map (fun m -> { Number = m.Value |> int; StartColumn = m.Index; EndColumn = m.Index + m.Length - 1 })
let linePositions = getPositions (threeLines.[1] |> string)
let previousLinePositions = getPositions (threeLines.[0] |> string)
let nextLinePositions = getPositions (threeLines.[2] |> string)
// Collate the numbers from the three lines into a single sequence of numbers
linePositions
|> Seq.append previousLinePositions
|> Seq.append nextLinePositions
let getGearRatios (i: int) =
let gearPositions = getGearPositions lines.[i]
let numPositions = getNumPositions (getThreeLines i)
gearPositions
|> Seq.map (fun gearPosition ->
let gearRatio =
numPositions
|> Seq.filter (fun numPosition ->
(numPosition.StartColumn - 1) <= gearPosition && (numPosition.EndColumn + 1) >= gearPosition)
// if the filtered sequence has two elements, multiply the numbers from the elements
|> (fun seq -> if Seq.length seq = 2 then seq else Seq.empty)
// if gearRatio is an empty sequence, return 0, otherwise return the product of the numbers in the sequence
// This is necessary because Seq.reduce throws an exception if the sequence is empty
gearRatio
|> (fun seq ->
if Seq.isEmpty seq
then 0
else
seq
|> Seq.map (fun numPosition -> numPosition.Number)
|> Seq.reduce (*) ))
|> Seq.toArray
// Get the gear ratios for every line in lines
lines
|> Seq.take (lines.Length - 1)
|> Seq.mapi (fun i line -> getGearRatios i)
|> Seq.collect (fun x -> x)
|> Seq.sum
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment