Lucky 13

A couple of days ago, David Sparks tweeted a link to a site that will convert any text message into the equivalent string of ones and zeros.

I just sent an email calling a friend a really bad name in binary and it was kind of fun.…
David Sparks (@MacSparky) Wed Feb 20 2013 7:23 PM CST

I immediately thought of doing the same thing as a TextExpander snippet. I managed to get the conversion both ways working, but I wasn’t entirely happy with the way it handled Unicode.1 And because the binary strings are so long, I wasn’t sure where I’d use them. I certainly wouldn’t be able use it in Twitter messages except for very short ones.

But as I was thinking about text conversions, I realized that a snippet for doing the classic ROT13 conversion would be fun, simple, and would lead to converted strings that were the same length as the originals. And I’d only need to write one snippet, because ROT13 is symmetric: the transformation that encodes also decodes. Even better, ROT13 is, by its very nature, ASCII only—no need to worry about Unicode.

I wanted to write my converter in Python, and there’s a Stack Overflow question on ROT13 in Python that has a very concise answer, but it had, I thought, an odd way of ordering the characters in the translation table. I changed that around but basically kept the same structure:

 1:  #!/usr/bin/python
 3:  from string import maketrans
 4:  from sys import stdin, stdout
 6:  alpha = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
 7:  rot13 = 'nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM'
 8:  r13table = maketrans(alpha, rot13)
10:  orig =
11:  stdout.write(orig.translate(r13table))

The key lines are 8 and 11. The maketrans function from the string module creates a translation table which, when used as the argument to the translate method, converts every character in the first string to the character at the corresponding position in the second string. Characters that aren’t in the first string are passed through unchanged.

I saved the script as rot13 in my ~/bin folder and made it executable. Then I created a TextExpander snippet that applied the ROT13 conversion to the contents of the clipboard and printed it out. I gave the snippet the obvious abbreviation: ;rot13.

ROT13 TextExpander snippet

To use the snippet, I

  1. Select the text I want to convert.
  2. Cut it (⌘X).
  3. Type ;rot13.

Rnfl nf cvr.

One last note: Instead of writing a Python script for the conversion and a shell script in TextExpander that calls it, I could have put the whole thing in the TE script. I decided to split the two apart so I’d have a filter that I could use outside of TextExpander if I ever needed one. If I were a bigger fan of Services, I’d fire up Automator and make a service that applies this same filter.

  1. It worked on every string I tested it on, but I just wasn’t confident that it would always work. Unicode does that to me. 

6 Responses to “Lucky 13”

  1. Lauri Ranta says:

    It would be easier to use tr:

    LC_ALL=C tr A-Za-z N-ZA-Mn-za-m
    ruby -p -e '$!("A-Za-z", "N-ZA-Mn-za-m")'

    Some locales have different collation orders. If LC_ALL is set, just changing LC_COLLATE wouldn’t work:

    $ export LC_ALL=fr_FR.ISO8859-1
    $ echo B | tr A-C 1-9
    $ echo B | LC_COLLATE=C tr A-C 1-9
    $ echo B | LC_ALL=C tr A-C 1-9
  2. Dave C. says:

    Yup, I thought the same as Lauri: a quick shell script using ‘tr’.

    pbpaste | tr a-zA-Z n-za-mN-ZA-M

    What I do like about your python script is the ability to toss it into Pythonista on iOS.

  3. Carl says:

    ROT-13 is already built into Python 2: "string".encode("rot-13").

    I’ll post more when I get back home on my Mac.

  4. Carl says:

    Here’s the Python 3:

    >>> import codecs
    >>> rot13 = codecs.getencoder("rot-13")
    >>> rot13("Hello World")
    ('Uryyb Jbeyq', 11)

    The reason for the increased complexity of the Python 3 code is that part of the 2 to 3 switch was getting serious about the meaning of “encode” and “decode.” In Python 3, “encode” always means “from Unicode string to bytes” and “decode” always means “from bytes to Unicode string.” Since ROT-13 is a string to string transformation, it didn’t make sense to make it available through the .encode method, as was done in Python 2.

  5. Dr. Drang says:

    Of course tr is a cleaner solution. Another example of my thinking getting stuck in a rut. Thanks, Lauri and Dave!

    As for Python’s built-in “rot13” encoding scheme, I deliberately avoided that because of the differences between Python 2 and Python 3. It’s also discussed on the Stack Exchange page I linked to.

  6. Edwin Arneson says:

    If you happen to be in Firefox, there’s a extension for that, Leet Key. Cf.