Problem Solving with Vim: Advent of Code Day 1

A while back, I had seen someone do some insane things with Vim: things that really should be done in a programming language but is somehow done in Vim. I was inspired, but not enough to go out of my way to find problems to solve in Vim. Fast forward to yesterday, when a friend messaged me his solution to the day 1 Advent of Code problem in an esolang. That gave me the idea to try solving it in Vim, trying to avoid deferring to vimscript or executing outside programs as much as possible — I wanted to solve things almost purely by manipulating the text.

Today, I gave day 1 a shot. The first part was really simple, even in Vim. You were given a newline-separated file of alphanumeric strings and all you had to do was take each line, form the first and last numbers of the line into a two digit number and then add up all the numbers. In Vim, that just meant

  1. Delete all non-digit characters with :%s/\D//g
  2. Record a macro to copy the first and last digits into separate registers with 0v"ay$v"by and then paste it with "ap"bp
  3. Run the macro on each line
  4. Replace every newline character with + using :%s/\n/+/g
  5. Delete the trailing +, cut the entire line into the register with 0C, and then use expression mode to evaluate the register content with <C-R>=<C-R>"

Where things got a bit more difficult was in part 2. First, you had to convert spelled out digits like one into 1 before doing the same thing as part 1. I wanted to just run %s/one/1/g, %s/two/2/g, ..., but the example showed that words could overlap: you could get words like xtwone3four, which should get converted into 24, but obviously doesn’t with the naive substitutions I wanted to do. Additionally for a more illustrative example, something like 1twone should come out as 121, and then 11, which I didn’t realize until a while later.

The first problem I was thinking about was how can we even know in Vim that we should substitute the two in xtwonefour first before the one. I didn’t end up exactly figuring that out, but it was no matter because I realized that ultimately, all we need to do is resolve the first and last spelled out word and we could ignore everything in between. This is what we do to get xtwonefour into x2ne4:

  1. Yank the line with yy and paste 8 copies with 8P
  2. On copy N, substitute spelled out digit N with the first occurrence of the actual digit and ignore errors for the macro (e.g. on copy 1, do :s/one/1/e)
  3. Select all 9 lines and sort them
  4. Either all 9 lines are the same or the first line will be the one with the digit replaced the earliest, which is what we want. So delete the 8 copies below.

So now we’re at x2nefour. Resolving the last spelled out word is very similar to doing it for the first:

  1. Yank the line with yy and paste 8 copies with 8P
  2. On copy N, substitute spelled out digit N with the last occurrence of the actual digit and ignore errors for the macro (e.g. on copy 1, do :s/.*\zsone/1/e)
  3. Select all 9 lines and reverse them (unfortunately, I did use !rev here)
  4. Select all 9 lines and sort them
  5. Either all 9 lines are the same or the first line will be the one with the digit replaced the latest, which is what we want. So delete the 8 copies below.
  6. The line we have is still reversed, so reverse it again to get it back to original order.

Now we have x2ne4 and so we can just delete non-digits and do what we did for part 1.

This works for almost all lines, but there’s a special case. For lines with only a single set of two overlapping number words, this fails. Let’s take a look at 3twonep for example, which should convert into 31. With the first set of steps, we’d get 32nep and then… oh we lost the one to replace to 1. After scratching my head a bit on how to resolve this, I came up with this.

  1. Make two copies of the line (e.g. 3twonep\n3twonep)
  2. On the first copy, resolve the first word according to above (e.g. 32nep\n3twonep)
  3. On the second copy, resolve the last word according to above (e.g. 32nep\n3tw1p)
  4. Join the copies onto a single line (e.g. 32nep3tw1p)
  5. Delete non-digits and do what we did for part 1 (e.g. 32nep3tw1p -> 3231 -> 31)

This works! It works because we don’t care about what’s happening in the middle of the line. The first digit will always be either already a digit or resolved by us from the first set of steps, and we’re still guaranteeing that the last digit of the combined line will also either already be a digit or resolved by us from the second step of steps, and it doesn’t matter that we have all that junk in the middle.

I haven’t felt so happy after solving a puzzle in a long time. From realizing the trick with sorting to extract the first replacement of a digit to realizing the trick with reversing to extract the last replacement to realizing I could just parallelize and then combine the two tricks, I was really forced to think out of the box in a dramatically different way due to my unusual and limited toolset, and I managed to do it. Here’s my full solution with some annotations.

Go^[                 " add a trailing line to the file. 
                     " For some reason, the macro below fails if there isn't another line
                     " below the one it's operating on.
gg0
qqyy9P               " First set of steps
:s/one/1/e           " Replace first occurrences
j:s/two/2/e
j:s/three/3/e
j:s/four/4/e
j:s/five/5/e
j:s/six/6/e
j:s/seven/7/e
j:s/eight/8/e
j:s/nine/9/e
V8k:sort            " Sort
jV7jd               " Delete copies
yy8P                " Second set of steps
:s/.*\zsone/1/e     " Replace last occurrences
j:s/.*\zstwo/2/e
j:s/.*\zsthree/3/e
j:s/.*\zsfour/4/e
j:s/.*\zsfive/5/e
j:s/.*\zssix/6/e
j:s/.*\zsseven/7/e
j:s/.*\zseight/8/e
j:s/.*\zsnine/9/e
V8k!rev             " Reverse
V8j:sort            " Sort
jV7jdkV:!rev        " Delete copies, reverse remaining line back to original
kJr,jq              " Combine lines, end macro
VGk:norm @q         " Run macro on rest of lines
:%s/\D//g           " Delete digits
gg0
qw                  " Record macro into w
v"ay$v"by           " Copy first character into register a, last character into register b
0D"ap"bp            " Delete the entire line, paste from register a, paste from register b
j0q                 " Go to start of next line, end macro
:%norm @w           " Run on all lines
:g/^\s*$/de         " Remove all empty lines, ignore errors
:%s/\n/+/g          " Replace newlines with +
$x                  " Delete trailing newline
0C                  " Cut line into default register
<C-r>=<C-r>"        " Evaluate expression in default register 
<ESC>

If you want to run it, here it is.
R28bZ2cwcXF5eTlQOnMvb25lLzEvZQ1qOnMvdHdvLzIvZQ1qOnMvdGhyZWUvMy9lDWo6cy9mb3Vy
LzQvZQ1qOnMvZml2ZS81L2UNajpzL3NpeC82L2UNajpzL3NldmVuLzcvZQ1qOnMvZWlnaHQvOC9l
DWo6cy9uaW5lLzkvZQ1WOGs6c29ydA1qVjdqZHl5OFA6cy8uKlx6c29uZS8xL2UNajpzLy4qXHpz
dHdvLzIvZQ1qOnMvLipcenN0aHJlZS8zL2UNajpzLy4qXHpzZm91ci80L2UNajpzLy4qXHpzZml2
ZS81L2UNajpzLy4qXHpzc2l4LzYvZQ1qOnMvLipcenNzZXZlbi83L2UNajpzLy4qXHpzZWlnaHQv
OC9lDWo6cy8uKlx6c25pbmUvOS9lDVY4ayFyZXYNVjhqOnNvcnQNalY3amRrVjohcmV2DWtKcixq
cVZHazpub3JtIEBxDTolcy9cRC8vZw1nZzBxd3YiYXkkdiJieTBEImFwImJwajBxOiVub3JtIEB3
DTpnL15ccyokL2RlDTolcy9cbi8rL2cNJHgwQxI9EiINGwoK

It’s base64 encoded because I can’t paste things like ESC (^[). To run it, save that encoded file somewhere, run base64 -d file > solution and then vi -s solution input.txt.

Leave a Reply

Your email address will not be published. Required fields are marked *