Skip to content

Instantly share code, notes, and snippets.

@sedm0784
Last active June 23, 2024 17:41
Show Gist options
  • Save sedm0784/3648d27abb000f181f43e3893f4ea5c9 to your computer and use it in GitHub Desktop.
Save sedm0784/3648d27abb000f181f43e3893f4ea5c9 to your computer and use it in GitHub Desktop.
Advent of Vim

I'm seeing how far I can get through Advent of Code using only Vim's editing commands. No Vimscript, variables, or function calls allowed! (Use of the expression register is also STRICTLY limited. I employed it to do adding in some early solutions, but anything more complex is a no-no.)

Solutions are notated using the standard notation used in Vim mappings and documentation. Ctrl-A is notated as <C-A>, Escape as <Esc>, Return as <CR>, etc.

But if a line starts with a colon, then it's an entire ex command: press Enter at the end of the line.

Otherwise, linebreaks aren't meaningful: they're just inserted at places that felt "natural" to me.

Note that I'm well aware the same techniques could be streamlined considerably: my intention with these is — hard as this may be to believe — to aim for clarity over efficiency. I'm not golfing here!

If you're not sure how something works, the easiest way to figure it out is to just try typing it in! (I recommend doing so in terminal Vim as you can see any macros progressing more easily there, and ignoring your config with a command like vim --clean input.txt Hopefully sometime soon I'll put fully annotated versions of these on my site: https://normalmo.de/

There's also some commentary in my Mastodon thread: https://mastodon.social/@normalmode/111513511912487936

https://adventofcode.com/2023/day/1
It's basically the same as part 2 below, but just using \d instead of
the very long (1|2|3|...|eight|nine) pattern. (And obviously without the
steps to replace the word versions of the numbers with digits.)
https://adventofcode.com/2023/day/1#part2
I spent a little while half-figuring out a convoluted macro-based
solution for dealing with overlaps before I realised I didn't have to!
We know from Part 1 that there's always at least one digit on the line,
so our regular expression to find the first and last number on the line
will greedily consume any overlaps too.
:%s/\v^.{-}(1|2|3|4|5|6|7|8|9|one|two|three|four|five|six|seven|eight|nine).*(1|2|3|4|5|6|7|8|9|one|two|three|four|five|six|seven|eight|nine).{-}$/\1\2
:%s/one/1/g
:%s/two/2/g
:%s/three/3/g
:%s/four/4/g
...
:%s/nine/9/g
:%s/\v^\D*(\d)\D*$/\1\1
ggqqqqqyiwddciw<C-R>=<C-R>0+<C-R>-<CR><Esc>jk@qq@q
https://adventofcode.com/2023/day/2
:%g/\v( (1|2|3|4|5|6|7|8|9|10|11|12))@<! red/d
:%g/\v( (1|2|3|4|5|6|7|8|9|10|11|12|13))@<! green/d
:%g/\v( (1|2|3|4|5|6|7|8|9|10|11|12|13|14))@<! blue/d
:%norm!dwED
ggqqqqqyiwddciw<C-R>=<C-R>0+<C-R>-<CR><Esc>jk@qq@q
Note that the last line is an identical macro recording as in Day 1
Part 2. If you haven't overwritten your "q register since then, you
don't need to re-record it. You can just play your existing one back
again with @q
https://adventofcode.com/2023/day/2#part2
qqma
:s/[:,;] /\r/g
$mbV`aj
:sort n
`b?red<CR>
kV`a:g//d<CR>
`b?green<CR>
kV`a:g//d<CR>
`b?blue<CR>
kV`a:g//d<CR>
/Game<CR>
q99@q
:%s/\v (red|green|blue)
:g/Game/norm!D4J
:%norm!wi*
:%norm!WWi*
ggqqC<C-R>=<C-R>-<CR><Esc>+q99@q
:%norm!A+
x
VggJ
0C<C-R>=<C-R>-<CR><Esc>
https://adventofcode.com/2023/day/3
There's probably some way you can do this one with a regular expression.
I'm not so good at those, though, so it's a recursive macro for me!
qambviwyggo<Esc>pI+<Esc>
yyPA viwr.<Esc>
o1 kj<Esc>
gg$/\d<CR>wy$
:2,3d
:.g/\./d
`b@0
`aq
ggO0<Esc>
qqqqq/[^0-9.]<CR>ma
k@akl@al@ajl@aj@ajh@ah@akh@a
r.
ggV/\./-1<CR>
J0C<C-R>=<C-R>-<CR><Esc>
@qq@q
The solution works by adding a total count to the first line of the
buffer and then uses two macros:
The recursive macro in "q finds the next "symbol", sets mark 'a to
remember the cursor position, and then runs the macro in "a once for
each of the eight cells surrounding the symbol, replaces the symbol with
a dot to mark it as processed, and then updates a running total.
The macro in "a processes possible part numbers. First it remembers
the current cursor position by setting mark 'b. Then it yanks the
entire word under the cursor and pastes it on the second line of the
buffer, preceded by a plus symbol. Then it makes another copy of this
line above, appends some text, and creates another line with the text
"1 kj" below that.
So if we were over a part number, lines 2-4 of the buffer will
might look like this:
+467 viwr.
1 kj
467
But if we were over a dot, they might look instead like this:
+...*..... viwr
1 jk
+...*.....
Next the macro moves to the end of the first line of the buffer and then
searches for a number. If we were over a part number originally the
cursor is now near the start of line 2. If we *weren't*, then we will
have skipped line 2 entirely and the cursor will be on the 1 that was
written into line three.
The macro then moves forward a word and yanks a word, storing its
contents in the yank register "0.
Next it deletes lines 2 and 3 with an ex command, and then uses a global
command to delete the current line if it contains a dot (i.e. if there
WASN'T a part number).
Finally, it moves back into the input area and runs the macro in the
yank register. If we were over a part number this contains the code
"viwr." and replaces the part number with dots to mark it as processed.
If we *weren't*, it just moves the cursor up and down again, doing
nothing.
When we have finished running the macro in "a eight times, the top
lines of the file might look like this:
0
+467
+35
...567....*...357+......
Macro "q adds these to the tally by selecting them visually with a
search for the first line *before* one that contains a dot, joining them
onto a single line to form a sum, and then calculating the result of the
sum by inserting them into the expression register.
https://adventofcode.com/2023/day/3#part2
It's a variation of the solution to Part 1:
qambviwy2GA *<Esc>p
yyPA kj<CR>. viwr.<Esc>
gg/\.<CR>wy$
:2,3d
:.g/\./norm!$daw
`b@0
`aq
7u
ggO0<Esc>
qqqqqo1<Esc>/\*<CR>ma
k@akl@al@ajl@aj@ajh@ah@akh@a
r.
2G
:.v/\v^1( \*\d+){2}$/norm!C0
0C<C-R>=<C-R>-<CR><Esc>
I+<Esc>kJ
0C<C-R>=<C-R>-<CR><Esc>
@qq@q
https://adventofcode.com/2023/day/4
Note that at the end of line 10 you have to press Enter *twice*. Once
for the <CR> I've written at the end of the line, and once because of
the confusing notation I decided to use where lines with a single ex
command on them require you to type an implicit Enter at the end of
them.
:%norm!A<Space>
:%s/\v( \d+ ).*\|.*\zs\1/ *2 /g
9@:
:v/\*/d
:%norm!dt*
:%s/\v\*\d+ \zs[^*]*//g
:%norm!cW1
:%norm!C<C-V><C-R>=<C-V><C-R>-<C-V><CR>
:%norm!I+
:%j
C<C-R>=<C-R>-<CR>
https://adventofcode.com/2023/day/4#part2
My solution to Part 1 was BOOOORING: just a bunch of regular
expressions. YAWN.
This one, I hope, is anything but. Usually with this sort of thing, I
recommend typing it in to see how it works, but you might not find it
that INSTRUCTIVE for this set of keystrokes, because a lot of the
commands don't do anything when you type them! (They only have effect
when playing back the macro, when the buffer has different contents.)
However, if you ARE interested in this sort of thing, I recommend trying
typing it in first to see if you can figure it out before reading on:
it's more fun that way!
yypD-2W
qamayiwjA <Esc>p
okJDJDciw +1<C-V><Esc><CR>3BJDJDdiw<Esc>
`a*++y$@0`awq
:2d
:%norm!I1<Space>
ggP+
qbq
qqqqq
"dyiw
kC<C-R>=<C-R>-+<C-R>d<CR><Esc>
jo0<Esc>-3W10@a
+C<C-R>=<C-R>-<CR>@b`a<Esc>Ima<Esc>
:.g/^ma0/norm!D
0y$ddk@0
kJD+
@qq
10u
qb+ciw<C-R>=<C-R>-+<C-R>d<CR><Esc>q
uggO0<Esc>+@q
Not sure if I'll ever get around to writing up a fully annotated
explanation of how this one works, but a rough description that
reasonably experienced Vimmers should be able to follow is below.
Before I realised how many scratchcards we were going to end up with I
wrote a version that actually made new lines for each copied card. I'm
pretty sure this version works, but I'm way less certain that it will
complete before the HEAT DEATH OF THE UNIVERSE. (I set it running
yesterday and it's still going: 670k scratchcards and counting.)
So the basic concept for this MARGINALLY more efficient version is to
only keep a single line for each card, and add a counter at the start of
that line to keep track of how many copies of that card we have. It also
adds a running total at the top of how many scratchcards we currently
have stuffed into our pockets, drawers, every room in the house, the
garage, the other houses in the village, etc.
We do the calculations with three macro recordings. The macro in "q
does all the work. Those in "a and "b are little helpers. Like
little macro elves.
Macro a: Check for Matches
--------------------------
This macro is to be run with the cursor over one of the winning numbers.
If it is a matching number, then it writes the text "+1" into the line
below. This task is a LITTLE bit tricky when you're not allowed to use
any functions or Vimscript so it works like this:
First it sets the 'a mark to the current location, then it copies
the current number into the line below. Next it writes two more lines of
apparent GIBBERISH into the next two lines below that. Then it jumps
back to the 'a mark to return to the current number and searches for
the number with the * command. This will leave the cursor either on
the matching number, if there is one, or on the copy we just made in the
line below, if not.
Next it moves two lines down, placing the cursor on one of the two lines
we wrote before. Then it yanks that line - putting it in the yank register
"0 - and immediately executes it as a macro with @0.
The two lines we wrote contain the normal mode editing commands to
either remove the number we added (for no match) or replace it with "+1"
(for a match).
Finally, macro "a moves the cursor one word forwards to the next
winning number.
We will run this macro 10 times: once for each winning number.
Macro b: Make Copies for 1 Scratchcard
---------------------------------------
Let's say you have 12 copies of scratchcard A, and it has two matching
numbers. This macro moves down a line, and then adds 12 copies to the
scratchcard the cursor is currently over. When processing scratchcard A.
We'll run it twice: once for each winning number.
It's not super complicated: it expects the number of copies to be stored
in register "d and it adds that to the number the cursor is over
by using the expression register.
Recursive Macro q: Process All The Scratchcards
-----------------------------------------------
So now we have our "a and "b macros stuffed in our utility belt,
doing the rest of the calculations will surely be TRIVIAL. Right?
...Right?
Macro "q starts with the cursor on the copies-counter for the
current scratchcard, which is near the top of the buffer on line 2.
First, it yanks the count into register "d. Then it moves up a line (to
the total and adds the current scratchcard's count with the expression
register.
Next it calculates how many winning numbers there are on this
scratchcard. It does this by writing a 0 into the line below and then
running macro "a once for each winning number.
So now the line below contains a sum (0 +1 +1 +1 ...) that adds up to
the number of winning numbers. The macro calculates the sum by plugging
it into the expression register and then adds the text "@b`a" after the
sum and "ma" before it, so the line looks something like:
ma12@b`a
It's a macro that runs macro "b once for each winning number, and
then moves the cursor back where it started!
Before macro "q yanks and runs it, though, it needs to handle the
case where there weren't any winning numbers, because the command
"0@b" won't run a macro zero times: it will run it once. Using a
:global command to delete the line if it starts "ma0" solves this
problem.
With that out of the way, macro "q yanks the macro, runs it, and
then finishes off by deleting the current scratchcard line and recursing
by running itself.
Et voila!
https://adventofcode.com/2023/day/6
So it turns out Advent of Code *isn't* solely about text processing. I
skipped Day 5 because, while there's a fairly straightforward way to do
deal with the massive ranges by performing calculations using the
expression register, it's not very EXCITING.
I thought about skipping Day 6 too because, again, the obvious solution
is just to do MATHS in the expression register, but then I decided a
better idea would be to ban the use of the expression register too.
So I came up with the below. Instead of using MATHS (ugh), it writes out
long series of 1s to represent the number of millimetres travelled for
each button press, deletes ones that are shorter than the current
record with a :vglobal, and then does a bunch of additions with <C-A>
to count how many ways we can beat the record and do the multiplications
required for the solution.
No expression register required!
O`m<C-V><C-A>0y$gg@0<Esc>
0"lDddW
qqqqqma
yiwO<Esc>p
<C-X>yypA@l<CR>yypkxjdiw0p<Esc>0mm
kkAA1<Esc>
0D@-<Esc>
+Ddd-@-
`mddgg
`ajyiwgg
pAA1<Esc>0D@-<Esc>
I,/Time/-1v/<Esc>A/d<Esc>dd
:@1
-l<C-V>gg$d
Go0<Esc>
:g/^1$/norm!G<C-V><C-A>
:g//d
`at lw@qq@q
GojkA<C-V><C-V><C-V><C-A><C-V><Esc>0Ddd@-@s<Esc>"sdd
qpqqp?^\d<CR>
gJio<Esc>A<C-V><Esc><Esc>0Dddmp@-`p+
:norm!@s
0@pq@p
The same *algorithm* would suffice for Part 2, but I think it would be
fair to assume there may be some issues with execution time and resource
usage. I tried executing the normal mode command 1000000A1 to write a
million 1s into a buffer while I drafted the brief explanation above. It
had gotten up to about 500,000 when I hit <C-C> after coming back to
see how it was getting along.
The target distance in my input is over 291 *trillion*.
https://adventofcode.com/2023/day/7
The first thing to do is to make the cards more amenable to Vim's :sort
command, which we do by replacing each of the card types with alphabetic
characters. This will have the nifty side-effect of making the horrendous
regular expressions we need to use for finding the various hand types slightly
easier to write, as we can completely ignore the numerical bids at the end of
each line.
I didn't type these all in by hand! Instead I wrote one of them into a buffer,
copied and pasted it a bunch of times, tweaked each line for the different card
values, yanked the entire paragraph with yip , and then ran them all in one
go with the :@0 command.
:%s/\vK\ze.* /B/g
:%s/\vQ\ze.* /C/g
:%s/\vJ\ze.* /D/g
:%s/\vT\ze.* /E/g
:%s/\v9\ze.* /F/g
:%s/\v8\ze.* /G/g
:%s/\v7\ze.* /H/g
:%s/\v6\ze.* /I/g
:%s/\v5\ze.* /J/g
:%s/\v4\ze.* /K/g
:%s/\v3\ze.* /L/g
:%s/\v2\ze.* /M/g
Next we're going to move each of the hand types one by one to the bottom of the
buffer with :global/:move commands, separating each hand type with a blank line
and sorting each with a macro recording as we go:
Five of a kind:
Go<Esc>
:g/\v(\u)\1{4}/m$
qq
v{j
:sort
Go<Esc>q
Four of a kind:
:1,/^$/g/\v(\u)(\u*\1){3}/m$
@q
Full house (skip to the bottom for an explanation of the pretty hairy regular
expression we use here):
:1,/^$/g/\v\u*(\u)(\u*\1){2}&\u*(\u)\u*(\3&\1@!)/m$
@q
Three of a kind:
:1,/^$/g/\v(\u)(\u*\1){2}/m$
@q
Two pairs:
:1,/^$/g/\v(\u)\u*\1&\u*(\u)\u*(\2&\1@!)/m$
@q
One pair:
:1,/^$/g/\v\u*(\u)\u*\1/m$
@q
The rest. We don't need a :global command for this one, we can pass the range
directly into the :move command:
:1,/^$/m$
@q
Delete blank lines:
:v/./d
Now all the hands are sorted by their rank!
Now we just need to calculate the score, which we do by first setting up with a
:substitute command:
:%s/\D*\(\d*\)/0 \1<C-A>
Followed by a bunch of macros. There's nothing too exciting here: we're just
using normal mode CTRL-A to add 1 a lot of times in order to perform the
required multiplications and additions.
First we record and playback a macro to append a macro to the end of each line
including the hand's rank:
A 1@0<Esc>
qqyaWk$pB<C-A>q
998@q
Then we record and playback a macro to use the line's contents to multiply each
hand's bid by its rank. multiplication of each hand's bid and rank:
ggqqwyEWD0@-+q
999@q
Finally we add everything up:
ggO0<Esc>
qq+f C<C-V><C-A><Esc>0y$-JD@0q
999@q
Full House Regular Expression
In the regular expression below, each card is matched by \u, an upper-case
letter. The first half of the regex matches the three of a kind, and the second
half matches the remaining pair.
\v\u*(\u)(\u*\1){2}&\u*(\u)\u*(\3&\1@!)
\v Using very magic, match BOTH
\u* any number (including 0) of cards
\u followed by a card
( ) (storing this in the \1 group)
( ){2} followed by two repetitions of
\u* any number of cards
\1 followed by the card we matched before
& AND
\u* any number of cards
\u followed by a card
( ) (storing this in the \2 group)
\u* followed by any number of cards
( ) followed by something that
\3 matches the second stored card value
& and
@! doesn't match
\1 the first stored card.
https://adventofcode.com/2023/day/8
This problem is so well suited to solving with macro recordings that
it's almost as if it were made for them!
Our approach is going to be to write LEFT and RIGHT macros, and convert
the first line to a bunch of macro playback commands. So let's do that
first. In order to wrap around at the end of the line, we add a new WRAP
macro command at the end of the line. We will use the 'a mark to
keep track of our position in the L/R commands, and the 'b mark to
keep track of our current node.
AW<Esc>0
Next we convert the Ls, Rs, and W into macro playback commands by
preceding each with an ampersand:
qqqqqi@<Esc>ll@qq@q
Now we jump to the bottom of the buffer and add a counter:
Go0<Esc>
We also need to ensure that the recursive macro we're going to record
will stop when it gets to the ZZZ line. We can do this by tweaking
its LEFT and RIGHT so that they no longer match anywhere.
/^ZZZ<CR>3wi_<Esc>W.
Now we can record the LEFT macro. (Moving to the start of the line
before recording probably isn't necessary here but whatevs. It makes it
easier for those playing along at home to see what the macro will do
when played back.) We move to the LEFT element and then search for it
elsewhere in the file. We use the command-line mode's <C-R><C-W>
feature to insert the word under the cursor into our search command (see
:help c_CTRL-R_CTRL-W), and append a space to ensure we find the node
and not one of the LEFT/RIGHT commands elsewhere. We drop a mark 'b
so we can return to the current node later and then update the step
counter with <C-A>.
0ql3w/<C-R><C-W><Space><CR>mbG<C-A>qu
The RIGHT macro is almost the same, just moving to the RIGHT element
instead:
qr3W/<C-R><C-W><Space><CR>mbG<C-A>qu
The WRAP macro is even simpler. It just moves to the start of the first
line and overwrites the current 'a mark.
qwggmaq
In order to start our route we need to be at the AAA node. I didn't
properly read the question and started at line 3 instead the first time
I tried recording my macro and it was several MILLION steps and HOURS of
macro playback before I realised what an IDIOT I am. Facepalm FOREVER.
/^AAA<CR>mb
Finally we record and playback our recursive macro to plot out our
route. This one is pretty simple. It:
- Jumps to the 'a mark to get to the L/R commands,
- Yanks the current LEFT, RIGHT, or WRAP command, storing it in the yank
register "0,
- Updates the 'a mark,
- Jumps to the 'b mark to get back to the current node,
- Plays back the L/R/W command from the yank register with @0,
- Rinse, repeat!
qqqqq`ay2l2lma'b@0@qq@q
MacroTASTIC!
@pheeria
Copy link

pheeria commented Dec 6, 2023

Impressive! 🚀
I feel like Day 2, Part 1 is missing a q macro :)
Something like "= I assume?

@sedm0784
Copy link
Author

sedm0784 commented Dec 6, 2023

Haha yeah, well spotted! I meant to add a note about that. It’s actually using the q macro from Day 1 Part 2, which happened to still be in my "q register. I'll update it to clarify.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment