String#tr in ruby (like tr in Linux) complete with figuring out slashes.

It seems like I’ve seen quite a few programming puzzles in the last few weeks that involved translating mistyped input in which the hands were shifted (right) on the keyboard. My first thought was the tr utility in *nix operating systems, but didn’t immediately go looking for or notice that ruby has a tr method on string. However, after doing a trivial implementation involving keyboard rows like the following, I stumbled on the tr method.

  # initial array of characters/strings to shift back to the left with [index-1]
  KEYBOARD_ROWS= [
    '`1234567890-=',
    'qwertyuiop[]\\', # need to escape the backslash or else debugging pain
    "asdfghjkl;'", # double-quotes here because single quote embedded
    'zxcvbnm,./',
    '~!@#\$%^&*()_+',
    'QWERTYUIOP{}|',
    'ASDFGHJKL:"',
    'ZXCVBNM<>?'
  ].join

Attempting to rewrite this for .tr presented a few challenges, however. If you are substituting for \, -, or ~, you have to escape the characters. You also have to escape them from their string representation, which makes for some head-spinning levels of escaping (zsh users who run shell commands through kubectl might be familiar with this pain as well):

# puts '\\~-'.tr('\\', 'a') # doesn't match because \ is passed to tr and not escaped
a~-
# puts '\\~-'.tr('\\\\', 'a') # now \\ is passed to tr, which is
a~-
# puts '\\~-'.tr("\\\\\\", 'a') # with double quotes, you need an extra pair, for 6 total.
a~-
# puts '\\~-'.tr('\\~', 'b') # the escaping backslash needs to be doubled
\b-
# puts '\\~-'.tr("\\\~", 'b') # the escaping backslash needs to be tripled
\b-
# puts '\\~-'.tr('\\-', 'c') # the escaping backslash needs to be doubled
\~c
# puts '\\~-'.tr("\\\-", 'c') # the escaping backslash needs to be tripled
\~c

So if you’re going to use translate to “shift” hands back to the left, the two arguments to tr, SHIFTED_KEYBOARD_ROWS and UNSHIFTED_KEYBOARD_ROWS would have to be defined with the following escaping:

  SHIFTED_KEYBOARD_ROWS =
    [
      '1234567890\\-=',
      'wertyuiop[]\\\\', # 4x backslash = backslash
      "sdfghjkl;'",
      'xcvbnm,./',
      '!@#\$%\^&*()_+',
      'WERTYUIOP{}|',
      'SDFGHJKL:"',
      'XCVBNM<>?'
  ].join

  UNSHIFTED_KEYBOARD_ROWS= [
    '`1234567890\-',
    'qwertyuiop[]', # need to escape the backslash or else debugging pain
    'asdfghjkl;',
    'zxcvbnm,.',
    '~!@#\$%\^&*()_',
    'QWERTYUIOP{}',
    'ASDFGHJKL:',
    'ZXCVBNM<>?'
  ].join

  def self.translate(string)
    string.tr(SHIFTED_KEYBOARD_ROWS, UNSHIFTED_KEYBOARD_ROWS)
  end