Advent of Code 2024

Updated Thursday, Dec 12, 2024

Every December since 2015, Advent of Code publishes a Christmas Advent calendar loaded to the brim with challenging programming quibbles and trials. Advent of code can be solved in any programming environment, from Microsoft Excel to Rust, if you can write it, you can use it. In fact, my 2020 Advent of Code “challenge” (or “theme”) was a different language every day. Nope, I didn’t finish all 25 problems, though it was a heck of a lot of fun to practice Forth, various assembly languages, code golfing languages like CJam, various Lisps, Standard ML, even a real weird language called ZKL with a .howza() method in its base API.

AoC particitation is not limited to the days in December; all challenges are always available once published. Some people like myself do enjoy solving the problems as if it were a race, but I assure you, all you need to do is just log in with GitHub, Google, or another logic method then get coding!

Edit: Learn about the making of Advent of Code

After publishing this post I stumbled across this talk by Eric Wastl, the fellow who created AoC on the creation of the project.

My 2024 Goals

I don’t expect to complete AoC 2024 before the end of December, though I will achieve my own practice goals. My goal this year is to practice bog-standard Python with the allowance for click to simplify things.

Here is the basic outline of my Python solutions:

import click


@click.command()
@click.argument('puzzle-input', type=click.File())
def main(puzzle_input):
    for line in puzzle_input:
        ...  # Read the input into an appropriate data structure.

    part1 = part2 = 0  # Initialize the variables to contain the solutions.

    ...  # Crunch numbers for a solution!

    # Print the two solutions out with one per line.
    click.echo(part1)
    click.echo(part2)


if __name__ == '__main__':
    main()

Then I run it using python3 day01.py input.txt. Using click it’s about a line of code to add a --debug flag:

@click.command()
@click.argument('puzzle-input', type=click.File())
@click.option('--debug', is_flag=True)
def main(puzzle_input, debug):
    if debug:
        # Fire up the debugger.
        breakpoint()
    ...

By the way, turns out you don’t need two statements to start the built-in Python debugger. import pdb; pdb.set_trace() simplifies to breakpoint(). instead.

A secondary goal for AoC 2024 is to learn more Perl 5.

A tertiary goal is to practice Go and C.

SPOILERS: AoC 20204, day 1 remarks

This discussion is typed up after completion of the first few days. There be spoilers beyond this sentence, take heed! My goal here is to detail the solution for day 1 and discuss some of the language features used.

import click
import re


@click.command()
@click.argument('input', type=click.File())
def main(input):
    L1, L2 = [], []
    for line in input:
        a, b = [int(i) for i in re.split(r'\s+', line) if i]
        L1.append(a)
        L2.append(b)
    print(sum(abs(a - b) for a, b in zip(sorted(L1), sorted(L2))))
    print(sum(a * L2.count(a) for a in L1))


if __name__ == '__main__':
    main()

First, import click for the command line argument parsing and re for regular expression searching. Next declare the program entrypoint main(). The @click... lines are decorator calls. These calls “wrap up” main with its own to change its behavior, in our case, produce a convenient command line interface without minimal effort.

In the body of the main() function, click has already opened the file and bound its file-object to input. Using a for loop, we iterate over each line. Next we construct a list comprehension: [int(i) for i in re.split(r'\s+', line) if i] that (1) splits the line on groups of whitespace, (2) converts each non-whitespace group of characters into a number or discards it in the off chance that leading or trailing white space existed.

This line also assigns to variables a and b. This assignment acts like an assertion because the list comprehension’s result must have two objects in it or a ValueError is raised. Here’s an example:

L = list(range(3))

a, b, c = L
print(f'a got {a}')
try:
    p, q = L  # Fails
except ValueError as e:
    print(f'Got an error: {e.__class__.__name__}: {e}')
a got 0
Got an error: ValueError: too many values to unpack (expected 2)

p, q = L throws an exception: ValueError: too many values to unpack (expected 2).

Back to the day 1 Python. In the body, read the input file object line by line. The first and second number read on the line are assigned to a and b. L1.append(a) appends a to the first list, and L2.append(b) appends to the second.

Next let’s break down two concise generator comprehensions (notice no square or curly brackets):

print(sum(abs(a - b) for a, b in zip(sorted(L1), sorted(L2))))
print(sum(a * L2.count(a) for a in L1))

Both of the next lines contain the form print(sum(...)). That is, summates (and prints the total of) all the elements in the iterable sequence (a generator expression in both casees. Here’s a simpler example that sums all the square of each natural number less than 10:

print(sum(i**2 for i in range(10)))
285

This example also contains a generator expression. Notice no square brackets and a for keyword in the middle of a line. Think list comprehension, but only runs if the code actually iterates through the generator. The iterator pattern can be thought of a simulated form of lazy evaluation, a coding paradigm popular in functional programming where code is only ran if the result is used in some way e.g. saved to disk or displayed to the user.

Now that we understand generator expressions and print and sum let’s talk about each generator expression within print(sum(...)). The first print(sum(...)) contains following generator expression: abs(a - b) for a, b in zip(sorted(L1), sorted(L2)). The sorted(...) function iterates over the iterable (in this case each sorted() iterates over one of L1 or L2) and builds a new iterable of the lists’s sorted contents. Next zip() is used to combine the two sorted lists into pairs of numbers, one from each list, thereby lining up the pair of inputs according to part1. The least number in L1 is now paired with least in L2, and the the most with the most in the other). For each of these zip()‘ed pairs, the code sums up the difference between a and b then takes its absolute value (no negatives!). This sum is the answer for part 1.

And within the second print(sum(...)) is a * L2.count(a) for a in L1). For every a in L1, count how many times a occurs in L2 then multiple it against a itself. Sum all these values up for the answer of part 2.

Whew, I think that’s everything.

Other language learnings

I detailed the Python solution in this post, but also have take-aways for some of the other languages that I solved this day in.

C’s qsort() is for sorting arrays

I was not to keen on writing a sorting algorithm to use in C. After a couple Googles, I stumbled across qsort().

In my C solution, I chose to use arrays of unsigned int for storage. Here is how I used qsort() in my solution:

static int cmpuintp(const void *a, const void *b) {
        return *((unsigned int *)a) - *((unsigned int *)b);
}

int main(int argc, char** argv) {
        unsigned int *a1 = malloc(max_size);
        unsigned int *a2 = malloc(max_size);
        size_t lines = 0;

        /* Populate a1, a2... */
        /* ... */

        /* Sort a1, a2... */
        qsort(a1, lines, sizeof(unsigned int), cmpuintp);
        qsort(a1, lines, sizeof(unsigned int), cmpuintp);

        /* Do something with sorted a1, a2... */
        /* ... */

        return 0;
}

qsort() takes four arguments:

  1. Memory start address (a1 and a2 in the above example).
  2. The number of elements (lines in the above example).
  3. The size of each element (sizeof(unsigned int) in the above example).
  4. A function pointer that compare two elements and returns a number with a different sign based on its sorted order. In this case I wrote cmpuintp that subtracts the two unsigned integers and returns the difference.

Perl’s grep and list vs scalar context

Perl’s grep returns the matches in a list value of matching elements but since it returns a list, it can be used in a scalar context to gets its length. Compare the two outputs:

$, = " ";
print "(1..10):        ", (1..10), "\n";
print "LIST   context: ", grep { $_ % 2 == 0 } (1..10), "\n";
print "SCALAR context: ", scalar grep { $_ % 2 == 0 } (1..10), "\n";
(1..10):         1 2 3 4 5 6 7 8 9 10
LIST   context:  2 4 6 8 10
SCALAR context:  6

This was useful. Check out this snippet from may Perl solution:

for my $el (@a1) {
    $p2 += $el * grep { $_ == $el } @a2;
}

$p2 contains the answer for part 2 after this code runs. The return value of grep is automatically converted into a scalar context because the * operator works on scalars only.

Will you play AoC?

Take up whatever language you’re learning or practicing. Give it a go. It’s fun and easy to integrate into coding practice. Code well.

Sidenote: Is this website broken?

Yep, woops! I broke my theme used on winny.tech and blog.winny.tech! I’m working on it.

Previously I introduced the reader to ShellCheck. In this post I detail how I use Flycheck in Emacs and offer an Emacs function to automatically suppress Shellcheck errors at the current line.

I’m an avid Emacs user and it follows that I’ve set up editor customization to exude the most from ShellCheck. If you, dear reader, are not an Emacs user, I cannot help you! Please, for the love of shell scripts, ensure ShellCheck works within your preferred text editor, lest you wish to ship edgecased buggy scripts!

How to use Shellcheck in Emacs

First, ensure you have shellcheck installed. Check Repology for a list of distros and OSes that package Shellcheck. On Debian/Ubuntu try apt install shellcheck. Verify that shellcheck works by invoking shellcheck --version.

$ shellcheck --version || echo 'No shellcheck found :('
ShellCheck - shell script analysis tool
version: 0.10.0
license: GNU General Public License, version 3
website: https://www.shellcheck.net

Next — this is the only mandatory Emacs step — install flycheck. The simplest way is to Type M-x package-install RET flycheck RET followed by M-x global-flycheck-mode RET, though I personally employ use-package to declare my package usage up front. Here is what I have checked into my Emacs configuration git repository:

(use-package flycheck
  :ensure t
  :init
  (global-flycheck-mode 1))

After global-flycheck-mode is enabled, you’ll see errors underlined in prog-mode derived buffers. Try C-c ! l to list all errors in another buffer. Or C-c ! n or C-c ! p to go to next or previous error. See M-x describe-command RET flycheck-mode RET and its complimentary Flycheck documentation website for further learnings.

If you want to learn more about other keys in the C-c ! prefix from within Emacs, check out which-key for Emacs or read the flycheck.el sources (or try M-x describe-variable RET flycheck-mode-map RET from within Emacs).

But, ShellCheck likes to complain

Like all powerful tools, ShellCheck has its tradeoffs. It can save loads of time avoiding bugs in scripts later when already placed in production. On the other hand, ShellCheck can slow down development because one has to fix every single trifle to quell ShellCheck. A savvy shell scripter will find themself encountering Shellcheck errors that are intentional. Consider this Bash function that decrypts a pass managed login credential (source):

decrypt() {
    local file
    file="${PASSWORD_STORE_DIR}/${1}.gpg"
    if [[ ! -f $file ]]; then
        echo "No such entry \"${1}\"" >&2
        return 1
    fi
    # shellcheck disable=SC2086
    gpg $PASSWORD_STORE_GPG_OPTS --quiet --decrypt "$file"
}

Without the # shellcheck disable=... ignore directive, which itself is a comment (a line that begins with # [POSIX]​), ShellCheck unyieldingly complains about the unquoted use of $PASSWORD_STORE_GPG_OPTS.

Time and time again, I’ve forgotten the exact syntax of this ShellCheck directive. Moreover, resultant of my forgetfulness, I’ve devised a solution to automatically generate these comment lines in Emacs! Simply move the point to the line with a ShellCheck error (try C-c ! n or C-c ! p to find the next or previous error) then type C-c ! k to generate the necessary comment to shut up ShellCheck.

I’ve embedded the Elisp here for easy copy-paste and to discuss the anatomy of the code:

(require 'cl)                           ; for cl-loop
(require 'sh-script)                    ; For sh-mode-map

(defun winny--extract-shellcheck-error (err)
  (and-let* (((eq (flycheck-error-checker err) 'sh-shellcheck)))
    (flycheck-error-id err)))
(defun winny/shellcheck-disable-at-line ()
  "Insert \"# shellcheck disable=SC...\" line to silence shellcheck errors."
  (interactive)
  (save-match-data
    (save-excursion
      (and-let* ((errs
                  (cl-loop for err in (flycheck-overlay-errors-in (pos-bol) (pos-eol))
                           if (winny--extract-shellcheck-error err)
                           collect (winny--extract-shellcheck-error err))))
        (beginning-of-line)
        (insert (format "# shellcheck disable=%s"
                        (mapconcat 'identity errs ",")))
        (indent-according-to-mode)
        (newline-and-indent)))))
(add-hook 'sh-mode-hook
          (defun winny--bind-shellcheck-disable ()
            (define-key sh-mode-map (kbd "C-c ! k") 'winny/shellcheck-disable-at-line)))

As a sort of preamble, this code ensures cl-loop and sh-mode-map variables are in scope using the two require forms.

Then, this code defines a helper function winny--extract-shellcheck-error to determine if a flycheck-error object is a shellcheck error and return the SC prefixed identifier string. Next comes the interactive command winny/shellcheck-disable-at-line which searches for unaddressed ShellCheck errors on the current line then inserts a comment directive above the current line in order to silence those ShellCheck errors.

Finally, the add-hook function call adds a function named winny--bind-shellcheck-disable to execute whenever sh-mode is used. It has one job: bind winny/shellcheck-disable-at-line to a key.

Notice the usage of a defun instead of a lambda for add-hook’s second argument. I believe it prudent to name your anonymous functions such that the M-x describe-variable (C-h v) output is less busy and the code itself is accessible by name for further monkeying about with M-x eval-expression (M-:) and friends.

Conclusion

Equipped with this post, I hope the reader has ShellCheck set up in Emacs. If Emacs isn’t your prefererred editor, worry not, there is surely a blogpost or official documentation describing how to set up ShellCheck for your favorite editor!

Stay tuned for a post on how to set up ShellCheck with pre-commit to ensure code quality with team-based projects.

In this post I hope to convince the reader on the merits of ShellCheck. Stay tuned for more posts about using ShellCheck.

On Shellscripting

Shell scripting is a of passion of mine. Preferably Bash (here’s a guide). (POSIX sh a.k.a. Bourne shell works too, albeit with more effort thanks to diminished versatility when compared to Bash.) The shell scripting language family has many warts as the languages were designed for both real-time interaction and automation programming. Additionally, inextricable backwards compatibility requirements are continually placed on the shell scripting language family — many Unix heads expect their shell to execute POSIX sh scripts without a hitch. Bourne shell, the sh we love and know, was developed in the early 70s and coexistent firsts in human computer interfaces.

One of my favorite shell warts is emphasis of reusing the string datatype for all sorts of operations. This wart is addressed, mildly, with Bash’s addition of arrays and more (check out declare). As a result, much of shell scripting is focused on efficient text processing and the leaky abstractions that (mostly) “typeless” programming provides. For example, in order to perform arithmetic in sh, you can use a command such as a=$(( a * 2 )) to double the value stored in variable a.

Equipped with context, it might make a little more sense as we touch on the sheer volume of pitfalls that beget robust sh or Bash utilization. Consider the first one most script writers encounter - the infamous word split. Refer to the following shell snippet:

path='/my/path with spaces'
find $path -type f -name *.[ch]

Find will chime back with an error like:

find: ‘/my/path’: No such file or directory
find: ‘with’: No such file or directory
find: ‘spaces’: No such file or directory

Enter ShellCheck

ShellCheck is a linter for sh family languages. (A linter is a program that scans code for common mistakes.) I highly recommend it. Let’s see what the value it provides for this example:

ShellCheck catches the previous error as Double quote to prevent globbing and word splitting. [SC2086]. A brief search of SC2086 on the internet yields ShellCheck: SC2086 – Double quote to prevent globbing and word splitting. The solution here is to wrap $path in double quotes, so the script is improved to the following:

path='/my/path with spaces'
find "$path" -type f -name *.[ch]

But we’re not finished yet! Let’s say the current directory contains a file named hello.c, then the -name *.[ch] expands to -name hello.c. Woops! ShellCheck knows about this issue too: Quote the parameter to -name so the shell won't interpret it. [SC2061]. Read more here: ShellCheck: SC2061 – Quote the parameter to `-name`. The fix is the same, quote the parameter:

path='/my/path with spaces'
find "$path" -type f -name '*.[ch]'

Additional erroneous shell script examples are curated in ShellCheck’s Gallery of bad code.

Ways to use ShellCheck

In short, apt install it! There are many other ways to run ShellCheck.

  • Check the official Installing section within the ShellCheck README.
  • Install shellcheck via your package manager (check Repology aggregated packages).
  • pip3 install shellcheck-py to install shellcheck via a Python Package (GitHub repo)
  • Run shellcheck in an unofficial Docker container (Debian based) or in the official minimal container.
  • Run in CI/CD, maybe using pre-commit. (I will be detailing this workflow in another blog post. The Installing section within the ShellCheck README details one solution which I’ll compare against other more user friendly solutions.)
  • Copy/paste into shellcheck.net’s form to check in browser. Note: the checker runs on a server so the text you pasted is exfiltrated from your browser tab.

And last but not least, be sure to run ShellCheck in your text editor or IDE. Good ShellCheck integration checks as you edit shell scripts, so you can catch errors before even saving the file.

Suppress ShellCheck

ShellCheck offers a few different mechanisms to disable specific errors via environment variable, for the entire file, and for the next line of code. I usually reach for ignoring errors on a per-line basis:

  1. Insert a line before the offending line of the format # shellcheck disable=SC2116,SC2086
  2. ShellCheck will ignore SC2116 and SC2086 next time.
# shellcheck disable=SC2116,SC2086
hash=$(echo ${hash})    # trim spaces

(Code sample lifted from the ShellCheck wiki’s Ignore page.)

That’s it

There is little reason to not use ShellCheck. Improve the quality of your work today. Try out ShellCheck.

Stay tuned for additional posts related to ShellCheck usage.

P.S. If you really can’t use ShellCheck

If you can’t use ShellCheck, you can still validate that scripts parse correctly prior to execution — check out sh -n and bash -n. (See set -n in the POSIX sh docs and in the Bash infopages.) The -n flag will parse each line and inform you about parse errors, but not execute the parsed commands.