Advent Of Code 2022 - Day 01
Dec 01, 2022
It's December. Time for snow, slippery roads, hot chocolate and cozy fire places. Also time for Advent of Code. An advent calendar with small, daily programming puzzles, growing progressively more difficult.
Every year I participate in a programming language I did not use for Advent of Code before, in order to learn new ways of doing things and to challenge myself. This year, that language is F#.
Day 01: Calorie Counting
Summary: Given a list of groups of numbers, each number on a new line and each group separated by an empty line, give the largest sum.
Example input:
100 200 400 500 300
Find the full description here.
Day 1 is often straight forward. Nice and easy to get in the groove. This year that's no different. Summing a list of numbers isn't very difficult, nor is finding a maxium value. Given the groups, that would look something like this:
let groups = [[100; 200]; [400]; [500; 300]] groups |> Seq.map Seq.sum |> Seq.max
Getting the groups turned out to be trickier than I'd liked, which has something
to do with the way I tend to set up my working environment for Advent of
Code. Experience teaches me that, 9 out of 10 times, I want to parse lines
rather than the raw input. So I've abstracted that away. The signature of my
solve
functions look like this:
solve: input: (string list) -> 'a
Most languages I've used so far for Advent of Code, F# included, do not have a built-in function to split a list of things based on a predicate. String splitting does usually exist. The straightforward way to get the groups therefore would have been:
// assume input is a newline separated string // like described in the problem statement input.Split("\n\n") |> Seq.map (fun s -> s.Split("\n")) |> Seq.map (Seq.map Int32.Parse)
Instead I ended up stealing a splitBy
function and using that, resulting in
the final solution for part one:
let splitBy fn seq = let i = ref 0 seq |> Seq.groupBy (fun e -> if fn e then incr i !i) |> Seq.map snd let caloriesPerElf input = input |> splitBy ((=) "") |> Seq.map (Seq.filter ((<>) "")) |> Seq.map (Seq.map Int32.Parse) |> Seq.map Seq.sum let solve1 (input: string list) = input |> caloriesPerElf |> Seq.max
Part 2
Part two asks us to, instead of finding the maximum sum, to find the sum of the three maximum sums. So instead of taking the maximum, we order the list of sums (largest first), take the first three elements and take the sum of that.
let solve2 (input: string list) = input |> caloriesPerElf |> Seq.orderByDescending |> Seq.take 3 |> Seq.sum
Improvements
There are some things that can be improved here.
splitBy
Both incr
and !
turn out to be deprecated. Also, the function keeps the
element that's being used as a separator. Finally, it would be much nicer if
splitBy
would be part of the Seq
module.
We can do all that and make it nice and reusable.
module Seq let splitOn predicate source = let mutable i = 0 source |> Seq.groupBy (fun e -> if predicate e then i <- i + 1 i) |> Seq.map snd let reject predicate source = Seq.filter (predicate >> not) source let splitOnExclusive predicate source = let mutable i = 0 source |> Seq.groupBy (fun e -> if predicate e then i <- i + 1 -1 else i) |> reject (fun (idx, _) -> idx = -1) |> Seq.map snd
caloriesPerElf
then also looks a lot nicer.
let caloriesPerElf input = input |> Seq.splitOnExclusive ((=) "") |> Seq.map (Seq.map Int32.Parse) |> Seq.map Seq.sum
Input
Another improvement is to determine wether to pass a list of lines or the raw string input to the solver based on the signature of the solver function. But that is an exercise for another time.
Reflection
When looking at other people's solutions after submitting my own, I realised that I never even considered building the list of lists in a more imperative way. Even though that comes more natural to me and F# has full support for it. Whether that's a good thing or not, is up for debate.