2013-11-15

It's a factor (plus a little Prime Numbers)

Given a list of numbers, this challenge has you replace each number with a list of its factors. It sounds complicated, but it can be done very fast with a (somewhat) simple macro.

cGyiw@"+<C-V>>r P#w<Esc>P99@.ddZZ

cGyiw@"+^V>r P#w^[P99@.ddZZ for 25.

This is the highly-optimized solution. We'll get back to it. But first, let's use a simplified version so it's easier to see what's going on.

dGPqqyiw@"+i ^[P#w@qq@qddZZ for 26.

  • dGP: Creates a blank line at the bottom of the file, which will be used as a "junk line". More on that later. This exploits the way Vim handles blank files; a blank file and a file with a single empty line are the same for editing purposes. You could also make the blank line with Go^[, but that leaves the cursor at the bottom, which is not where we want it.
  • qq..@qq@q: Makes and runs a recursive macro. (.. is a placeholder for the commands to run.) This macro needs to run 66 times (2 digits), so using a recursive call instead of a number argument doesn't gain or lose any strokes.
  • yiw: The cursor will always be at the beginning of the word we're yanking in this macro, but those words may be 1 or 2 digits long. yw or ye wouldn't behave reliably. The yanked word will be used both for pasting and running as a macro.
  • @"+: The "" register contains the number we just yanked, and now we're running it as a macro. This lets us use it as a numeric argument to +. If the number was 3, this will move the cursor down 3 lines.
  • i ^[P: Insert a space, and the yanked number before it.
  • #: The easy way back to where we were before the + command. Only noobs use marks.
  • w: The macro needs to run from every word, so we'd better move the cursor to the next word.
  • dd: Delete the junk line when you're done. It took 5 strokes to make and delete. It had better be worth it...

As the macro progresses, it will run into the numbers it pasted before. The 2 from line 2 will be copied to line 4, where it will be copied to line 6, and so on. The numbers will be put in the right order automatically; the 3 on line 6 came from line 3, which got there before the 2 because it came from line 4.

The junk line is necessary to hold the factors that would otherwise be used if the challenge was longer. The 11 from line 11 belongs on line 22, but there is no line 22. Without the junk line, the 11 would get dropped off on the last line, line 20. That's bad, because 20 is not divisible by 11.

When the cursor eventually reaches the last line, the macro will try to run the + command and fail. Failure is useful, especially when you're running a recursive macro, which would run forever otherwise (or until you hit ^C). What's interesting is that the macro didn't fail before. If you run 2j from the second-to-last line, it won't fail a macro, even though jj would. But running j with any number argument (or none) will fail a macro on the last line.

cGyiw@"+^V>r P#w^[P99@.ddZZ

Now that you know how the simplified 26 works, let's see what optimizations brought it down to 25.

This solution writes a macro directly into insert mode, and runs it from the ". register, which is where text typed in insert mode goes. A typical way of writing a @. macro is with i..^[u, with the macro replacing .. and the u cleaning up the mess you just made. That's 3 strokes of overhead, which is the same overhead as using qa..q to record a macro into the "a register.

The reason the @. macro saves strokes is that you can merge some of those 3 strokes of overhead with other commands in the solution. We were using dG anyway to make the junk line, but cG is just as fast, and leaves you in insert mode. The text you're typing is on the junk line, so it will get deleted at the end anyway. Leave it, and you can skip the undo. That's 2 strokes saved.

The big caveat of @. macros is that you can't embed another insert inside of them. You can use the i command, but you can't escape. The 26 used insert mode to add a space. To make @. work, you'll need to do something else.

As a workaround, we'll add a tab instead, using ^V>, and make it a space with r . (You could add the tab with >> as well, but the cursor will be left after the indent, and you'll waste a stroke moving it back.) This costs one more stroke than i ^[, but it's necessary if you want to save two strokes by using a @. macro.

^V in insert mode is usually used for entering literal control characters, or entering a character by its ASCII value. It does nothing with > as its argument, but you can confirm it's in ". anyway by running :display.

Just one more adjustment: r overwrites the ". register, so we won't be able to use a recursive macro. When you run a register as a macro with a number argument, the same version will run each time, even if the register itself changes. A recursive macro is run separately for each repetition, so once ". gets changed to a single space, the macro is gone.

This macro needs to run 66 times. You can repeat it anywhere from 66 to 99 times (or more, if you like wasting strokes.), since it fails on 67 anyway.

Prime Numbers alternate solution

I originally came up with this macro as another way to do Prime Numbers. It shouldn't be surprising that if you can list all a number's factors on a line, you can filter out those that don't have any. It's not quite as fast as the best solution, but it's pretty close.

iyiw@"+<C-V>>P#w@.<Esc>"=r<Tab>2,542)<CR>P@.:g/<Tab>/d<CR>ZZ

iyiw@"+^V>P#w@.^["=r^I2,542)^MP@.:g/^I/d^MZZ for 38.

The macro is very similar to the one used before. In this challenge, we start on the junk line, so you can just write the macro there. There's no reason to bother with spaces between numbers; stick with tabs and save a couple strokes.

Before you run the macro, you need an array of numbers. The fastest way to make one is with the range() function, pasted using the expression register "=. Numbers below 2 will just get in the way, so don't make them.

Note: Vim's method of turning lists into strings for pasting changed in version 7.3.272. Since that version, a newline is included at the end of the resulting string. Before, it wasn't. I used 542 as an argument to range() (one larger than necessary) to ensure the solution works with any version.

Once the macro is done (and it takes a few seconds), all the composite number lines (and the junk line) will have tabs in them. Use :g/^I/d^M to remove those lines, and you're done.

Read the manual

  • :help 10.1, or "How do I macro?".
  • :help text-objects
  • :help registers. You may find yourself referring to this often.
  • :help i_CTRL-V. For writing literal control characters in insert mode, or writing a character by its ascii value. (Or for an actual ^V in a @. macro.)
  • :help v_b_<. To (un)indent the left side of a block visual.
  • :help "=. The expression register, for using vimscript in an editing command.
  • :help range(). Useful for making a number array from scratch.
  • :help 'lazyredraw'. One reason the Prime Numbers macro took so long is thousands of screen redraws. This option can turn those off.

Similar challenges

2 comments: