Solving Advent of Code With Vim – Day 1, Part 2

Can Visual Studio Code do THIS?

I’ve never done Advent of Code before, and I wasn’t planning to do it this year1, but for some reason I decided to try my hand at solving the 2023 event’s Day 1’s challenges in Vim, using only its editing commands. (So ex commands are allowed, but Vimscript’s more programmerly constructs — functions, loops, conditionals, etc. — aren’t.)

It was fun!

So much fun, in fact, that I ended up carrying on and completing a bunch more of the challenges.2

As a bit of a break from the litany of macro posts I’ve been making, I thought I’d describe how I solved Part Two in detail.

The Challenge

The task to be solved isn’t visible on the Advent of Code site unless you’ve already completed Part 1, and it’s wrapped in a cutesy backstory which might not be to everyone’s taste, so I’ll paraphrase here so you know what it is we need to achieve.

You’re given a text file which has a bunch of lines that look something like this:

x2y3z
fivexyz7abc
a34bsixc
xxx9
xonex

Each of the lines has at least one “single digit number” on it. The scare quotes are because some of the digits are written out as English words: six instead of 6.

The task is to find the first and last “digit” on each line and smush them together to make a two-digit number. If a line only has one digit on it, then it should instead be repeated to make a two-digit number.

Then you add up all the two digit numbers to find your final answer.

So for the example above, the two digit numbers for the respective lines are 23, 57, 36, 99, and 11, and the answer is the sum of these: 226.

My Solution

First we run this ridiculous :substitute command:

:%s/\v^.{-}([1-9]|one|two|three|four|five|six|seven|eight|nine).*([1-9]|one|two|three|four|five|six|seven|eight|nine).{-}$/\1\2

Okay that is a very long command. Let’s break it up a little.

Astute readers will have noticed that the following very long alternation of numbers is repeated twice:

[1-9]|one|two|three|four|five|six|seven|eight|nine

This matches anything that “looks like” a “digit”.

Now let’s look at the rest of the expression:

:%s/\v^.{-}(digit).*(digit).{-}$/\1\2

 %                                     In the whole of the file
    \v                                 using very magic
  s/                                   replace:
      ^                                  the start of a line, followed by
       .{-}                              anything
            digit                        the first digit on the line
           (     )                       (saving it in group 1)
                  .*                     followed by anything followed by
                     digit               the last digit on the line
                    (     )              (saving it in group 2)
                           .{-}          followed by anything
                               $         followed by the end of the line
                                /      with:
                                 \1      the first digit we found, and
                                   \2    the last digit we found

Each of the lines matched by the pattern now contains just the first and last “digit” on each line.

23
five7
3six
xxx9
xonex

We replace the ones that are actually words with real digits3:

:%s/one/1/g
:%s/two/2/g
:%s/three/3/g
:%s/four/4/g
... you get the idea ...
:%s/nine/9/g

Getting there!

23
57
36
xxx9
xonex

But some of the lines only had one digit on them, so the first :substitute we ran didn’t do anything!

We can run another substitute to convert these to double digit numbers:

%s/\v^\D*(\d)\D*$/\1\1

Actually no that’s boring let’s do it a more fun way:

First we’ll move the lines to the top:

:g/\v^\D*(\d)\D*$/m0

   \v                 Using very magic
 g/              /    find every line that matches
     ^                the start of the line followed by
      \D*             any number of non-digits
          \d          followed by a digit
         (  )         (saving it in a group)
             \D*      followed by any number of non-digits
                $     followed by the line
                  m0  and move them to the top of the file

Now we’ll remove everything on these lines apart from the digits by running a new :substitute:

:%s//\1

Because we leave the pattern part of this command empty, it will re-use the pattern from our previous command, so this replaces the entire matching line with just the digit.

1
9
23
57
36

Aw SHUCKS we forgot to duplicate the digits. We could undo and fix our substitute but I’m kind of fed up of the command-line now. Lets use visual block mode and a yank/put instead:

<C-V>ggyp

Every line now has a two digit number:

11
99
23
57
36

We just need to add them all up.

You could do this pretty easily with a :substitute replace expression submatch() or a :global but PSYCH! Even though I said I was writing a non-macro post here, I’m totally going to use a recursive macro. You can take the boy out of the macros but you can’t… uh… take the macros out of the boy?

qqqqqyiwddciw<C-R>=<C-R>0+<C-R>-<CR><Esc>jk@qq@q
qqqqq
Clear out the"qregister and start recording a macro into it.
yiw
Yank the word under the cursor i.e. the number, storing it in the yank register"0.
dd
Delete the line.
ciw
Change the word under the cursor. i.e. the next number. Note that this will save this next number in the small delete register"-.
<C-R>=
Enter the command-line so we can enter an expression into the expression register. We’re going to use this to perform the addition, so we need to insert the two values we want added.
<C-R>0
The first value is in the yank register"0, so we can insert it with CTRL-R.
+
This is kind of obscure, but in addition to its normal function of [count] lines downward, on the first non-blank character linewise, this funny cross symbol can also be used to add two numbers together.
<C-R>-
The second value we want added is in the small-delete register "-, so ditto.
<CR>
Now our command line has the contents: 11+99. Press enter to complete the expression. Vim will now perform the addition and insert the result into our buffer.
<Esc>
We're still in insert mode. Escape!
jk
We’re recording a recursive macro, so we need to induce an error that will stop playback when we’ve added all the numbers. A simple way to do that here is to try to move down a line, which will fail if we’re at the bottom of the buffer. If we weren’t at the bottom of the buffer, we’ll need to move back up again before continuing.
@q
That's it! We've added two numbers. We now recurse with@q. This will do nothing when we're recording because we emptied the"qregister before we started, but it will run the macro again when we're playing back.
q@q
We can now end the recording, and play it back with@q. The macro will run down the file adding numbers till it hits the bottom of the file.

Done!

If you enjoyed this, why not check out the rest of my Advent of Code solutions. For even further TOMFOOLERY, check out reddit user Smylers. They’ve apparently been doing this ridiculousness for YEARS. I haven’t checked out their solutions past day 8 in case I do ever decide to pick it back up again, but their solutions to the first week or so are surprisingly different to mine in many cases. Vim is so VERSATILE! Looks like they also managed to figure out solutions for a couple of the challenges that I FAILED at4, so chapeau to them.


  1. Technically last year, at this point. ↩︎

  2. I was derailed by a nasty bout of the SNIFFLES halfway through day 7, and, despite a lacklustre effort to get back on the horse after I recovered, I never ended up getting past Day 8. ↩︎

  3. Perhaps you are wondering why we didn’t do this step before searching for the first and last digit on every line. We have to do it this way round because of a quirk in the input data. In some cases, word-digits can overlap: eightwo. If you convert these to digits with substitutions then you either end up with 8wo or eigh2, and which of these is correct depends on the pair’s location in the line. Doing this step after we’ve already determined which digits we want to use sidesteps this issue. ↩︎

  4. Although my foolish pride apparently won’t allow me not to mention that the restrictions they imposed on themselves aren’t quite as onerous as the ones I for some reason set for myself. ↩︎