Inspired by the many articles about learning in public, I decided to write a blog post about my learnings for the first day of Advent of Code 2021.
Every year, I’m looking forward to participate in Advent of Code. Those daily puzzles are super delightful and it’s so rewarding to get a working solution. Though with the rising difficulty it’s very hard to nail every challenge all the way to the end but hey at least I’ve worked on Day 1!
This year, like last year, I decided to use Julia. I like to describe it as “As simple as Python, as fast as C”, but there are also some features that I really like (a fast and awesome REPL, pipes (|>
), vectorized functions…).
Parsing input is the first step in solving an Advent of Code puzzle.
My biggest tip about Advent of Code is to test your code against the public (I personally save it in a file test_input
next to my real, private input
file).
By solving last year’s Advent of Code in Julia too, I remembered the method I was using, which is essentially: - read file as a string - split by newline - for each line parse string as an Integer
= open(f->read(f, String), "test_input") |> f->split(f, "\n") |> a->map(s->parse(Int, s), a) input
But I was not satisfied with it and kept feeling like there must be a better way…
After some research that’s how I found out about Delimited Files, part of Julia’s standard library! More specifically, the readdlm
method that allows to read a file as a matrix while also handling parsing to integers or floats at the same time.
> using DelimitedFiles
julia
> input = readdlm("test_input", Int)
julia10×1 Matrix{Int64}:
199
200
208
210
200
207
240
269
260
263
What we want is a vector though (what in most other languages would be called an array or list, essentially a 1-Dimension Matrix in Julia’s perspective), but the vec
function easily handles that for us:
> input = readdlm("test_input", Int) |> vec
julia10-element Vector{Int64}:
199
200
208
210
200
207
240
269
260
263
Alright, now to the good stuff. I won’t go over Day 1’s puzzle here, I would recommed you go through it yourself first! We want to know whether element n
is lesser than element n+1
, or actually whether element n
is greater than element n-1
. My first guess is to map
over the input, using enumerate
to get the index of the n
th element, and using that to compare it to the n-1
th element. A quick ternary operator is necessary to handle the case of the first element.
> map(x -> x[1] - 1 == 0 ? false : x[2] > input[x[1] - 1], enumerate(input))
julia10-element Vector{Bool}:
0
1
1
1
0
1
1
1
0
1
Now that we have a list of booleans (or ones and zeros), a quick reduce allows us to sum up the values:
> reduce(+, map(x -> x[1] - 1 == 0 ? false : x[2] > input[x[1] - 1], enumerate(input)))
julia7
By sharing my first solution with my colleagues and reviewing others, I quickly got two feedback: - reduce
is not necessary and a simple sum
should work - zip
can be very useful! (I always forget about zip
…)
> map(((x, y),) -> x < y, zip(input, input[2:end]))
julia9-element Vector{Bool}:
1
1
1
0
1
1
1
0
1
Some explanation of the code above. This is what zip does:
> zip(input, input[2:end]) |> collect
julia9-element Vector{Tuple{Int64, Int64}}:
199, 200)
(200, 208)
(208, 210)
(210, 200)
(200, 207)
(207, 240)
(240, 269)
(269, 260)
(260, 263) (
Remember that Julia’s indexes start at 1, not 0 (similiar to other mathematics-oriented languages like R or MATLAB), meaning that we combine the initial vector with the same vector starting from the 2nd element. The result is a vector of tuples.
The ((x, y),) -> x < y
notation allows to destructurate the tuple inside the list of parameters, instead of having to do something like x -> x[1] < x[2]
(which you might consider cleaner, but I don’t, even though it’s technically terser).
Piping a sum
at the end:
> map(((x, y),) -> x < y, zip(input, input[2:end])) |> sum
julia7
I was pretty satisfied with this solution, but as I looked around the amazing /r/adventofcode’s solution megathread, I saw someone using a simply diff
function in R.
And to my pleasant surprise, Julia has a diff
function too!
> diff(input)
julia9-element Vector{Int64}:
1
8
2
-10
7
33
29
-9
3
It automatically calculates the difference between “next” elements of arrays/vectors.
Map
ing over this result to check if the result is positive:
> map(x -> x > 0, diff(input))
julia9-element Vector{Bool}:
1
1
1
0
1
1
1
0
1
And once again, piping a sum
at the end:
> map(x -> x > 0, diff(input)) |> sum
julia7
To recap, this gives us a one-liner:
> readdlm("test_input", Int) |> vec |> v->map(x -> x > 0, diff(v)) |> sum
julia7
Not much evolution here, but we’re going to benefit from everything we learnt in part 1.
We need to create sliding windows of size 3. I didn’t feel like implement it myself as I don’t enjoy reinventing the wheel, so looked around for an available method.
The Partition
method from Transducers.jl looked like it would work.
> using Transducers
julia
> windows = input |> Transducers.Partition(3; step = 1) |> Map(copy) |> collect
julia8-element Vector{Vector{Int64}}:
199, 200, 208]
[200, 208, 210]
[208, 210, 200]
[210, 200, 207]
[200, 207, 240]
[207, 240, 269]
[240, 269, 260]
[269, 260, 263] [
Nice
Using the dot syntax to vectorize function, we can get the sum of each window:
> sum.(windows)
julia8-element Vector{Int64}:
607
618
618
617
647
716
769
792
And we can still use the same workflow as part 1 to calculate differences and how many times there was an increase in value.
Or if you really want a one-liner…
> readdlm("test_input", Int) |> vec |> Transducers.Partition(3; step = 1) |> Map(copy) |> collect |> w->map(x -> sum(x), w) |> v->map(x -> x > 0, diff(v)) |> sum
julia5
Here’s the full script that is available on GitHub:
using DelimitedFiles
using Transducers
= readdlm("input", Int) |> vec
input compare(vec) = map(x -> x > 0, diff(vec))
= input |> Transducers.Partition(3; step = 1) |> Map(copy) |> collect
windows
println("Part 1: ", input |> compare |> sum)
println("Part 2: ", sum.(windows) |> compare |> sum)
Pretty satisfied with this! I don’t think there’s much left to improve.
Main learnings for me: - readdlm
is super useful - zip
can be cleaner than fiddling with enumerate
and indexes - vectorized functions rock!
I don’t think it will be sustainable for me to keep blogging about next Advent of Code puzzles, but hope this was an interesting read and that you learnt something.