2024 Advent of Code, day 7 - Perl walkthrough
I have been keeping up with Advent of Code so far this year, and thought after a week it might be nice to have a more detailed look at one of the entries.
For my own enjoyment, I have been trying to do each day as a one-liner in Perl, so below I will take one of the days (day 7, go and read the puzzle first) and provide a full commented version, along with my shorter version. Perl is not used as much nowadays, but is still just as capable as other similar languages (and still used to run FixMyStreet), and has tricks suited to small quick uses such as these.
To not scare people off, I am going to start with the commented version. I will try and assume no knowledge of Perl and explain things as I go, apologies if I have missed something and feel free to ask for more clarification!
#!/usr/bin/perl
#
# If saved as a file called day7, can be run as `perl day7 1 inputfile`
# for part 1, or `perl day7 2 inputfile` for part 2
# Declare our minimum version - this turns on strict programming and `say`
use v5.14;
# Load a third party library to do the base conversion -
# we only need the one function from it, so request that
# (the `qw` operator automatically creates a list of provided words)
use ntheory qw(todigits);
# Get which part we want from the command line arguments; `shift` gets the
# first value off an array, and without an argument at the top level uses
# @ARGV, the array of command line arguments (in a function, it uses @_,
# the function parameters).
my $part = shift;
# The code below actually wants 2^N for part 1, 3^N for part 2, so increment
$part++;
# Define a variable to hold our total result. `my` declares a local variable
# for the current block (or here, the file)
my $total = 0;
# <> reads lines from a filehandle, and a null filehandle reads from either
# STDIN or the files listed on the command line. It puts each line in a
# variable named $_ if you don’t assign to something else.
while (<>) {
# The lines are of the form '3267: 81 40 27' - use split to separate the
# special first number from the rest. Without an argument, split operates
# on the $_ variable. (Lots of functions do this.)
my ($num, $rest) = split /: /;
# Then split all the other numbers into an array called @nums
# (Arrays start with @, scalars with $, and hashes (dicts) with %.)
my @nums = split / /, $rest;
# Okay, now we have N numbers, with N-1 operators to have between them
# (either +, *, or in part 2 ||). “All the possible operators” is equivalent
# to looping through every possible binary/base-3 number of the right
# length. $#nums is the maximum index of the @nums array (you can also
# use @nums as a scalar, and it is the size of the array).
#
# This is an old-style for loop as we need the number; Perl also has a
# much more often used way of looping over an array directly, such as
# `for (@array)` or `foreach my $key (@keys)`.
for (my $i=0; $i < $part**$#nums; $i++) {
# Get the base 2 or 3 version of $i, padded to @nums characters long
# This function returns the number as an array (good for our use here)
my @ops = todigits($i, $part, @nums);
# Start out calculation with the first number. Referring to a scalar
# variable always starts with a $ - in this case, the first entry from
# the @nums array.
my $sum = $nums[0];
# Now loop through the remaining numbers. At each step we will see
# what the base 2/3 number has and pick an operator accordingly
# (0 is addition, 1 is multiplication and 2 is concatenation).
for (my $j=1; $j<@nums; $j++) {
if (0 == $ops[$j]) {
$sum += $nums[$j];
} elsif (1 == $ops[$j]) {
$sum *= $nums[$j];
} else {
$sum = "$sum$nums[$j]";
}
}
# Okay, we've finished calcuating - have we reached the right value?
if ($num == $sum) {
# If we have, we need to add that to our total, and we can then
# jump out of the for loop to go straight to the next row
$total += $num;
last;
}
}
}
# Outout our final total
say $total;
Hopefully me spacing it out and adding all these comments didn’t break it.
Now what about the one liner? That looks like this:
perl -Mntheory=todigits -nE'BEGIN{$x=1+shift}($n,$r)=split/: /;@z=split/ /,$r;
for($i=0;$i<$x**$#z;$i++){@_=todigits($i,$x,@z);for($s=$z[0],$j=1;$j<@z;$j++){
$q=$z[$j];$s=1<$_[$j]?$s.$q:$_[$j]?$s*$q:$s+$q}$t+=$n,last if$n==$s}END{say$t}' 1 input
You can hopefully see the same structure and contents, but with e.g. all the variables reduced to single letters. A few things to note:
-n
wraps all the code in awhile (<>)
loop for you (apart from any BEGIN/END blocks, which run at, well, have a guess). If you're processing a file at the command line, this is very handy.-M
is a command line way to load a package.-E
specifies the code to run, and automatically turns on 'new' features likesay
.- I changed the main
if
loop into a line using thetest ? True : False
ternary operator (and getting the right order here saves a few characters). - For one line statements, Perl lets you put conditions at the end so instead of
if (test) { do_thing(); }
you can saydo_thing() if test;
. This can make some types of function and code use quite a bit more readable. - Semicolons aren’t needed right before a closing brace.