Skip to content

Rhaptos Software Development

Personal tools
You are here: Home » Development » Tuesday Python Challenge » Challenge #3: Encoding, 31 July 2006

Challenge #3: Encoding, 31 July 2006

Document Actions
I've played with this as a Python Challenge one before, but it's still a good exercise.
A simple cryptography method is to rotate letters. Write a function to decode a sentence with the following pattern:

f -> i
h -> k
x -> a

Don't worry about upper case.

Provide the decoded message in "wkh udlq lq vsdlq vwdbv pdlqob lq wkh sodlq".

Can you do it in one line? Is this the most time-efficient way? Is your method easily reversible (that is, how much different is an encoder from the decoder)?

Answers and discussion next Monday.

Answers and Discussion

Here we wanted to transform a string into another through mapping. As is usual in programming we can trade time for space. In this case the space is very small, so that's probably the best solution, but let's look at a non-mapping version first:

def code(st): return ''.join([x is ' ' and x or chr(((ord(x)+3)-97) % 26+97) for x in st])

This version takes advantage of the mapping of ASCII characters to numbers, and uses the 'mod' operator for rounding the corner at the end of the alphabet. The math could be collapsed a little, but is displayed full for clarity. The magic number 97 is the ASCII number for 'a', as can be seen with

>>> ord('a')
97

There's the ternary operator in there too, since we're not mapping spaces and need to have a special case. This is nicely reversible, too, just by changing one plus sign to minus.

def decode(st): return ''.join([x is ' ' and x or chr(((ord(x)-3)-97) % 26+97) for x in st])

Now, chr and ord are probably pretty cheap operations, seeing as how the character is actually represented by that number. But we do have a little math and some boolean stuff.

Python has a great mapping type, the dictionary, so if we can stand that in memory, it has constant time access for each character.

import string
rotated = dict(zip(string.ascii_lowercase[3:]+string.ascii_lowercase[:3], string.ascii_lowercase))
rotated[' '] = ' '

Then we just have to apply the mapping. We've seen a couple ways to do this before. One simple one is::

def code(st): return ''.join([rotated[c] for c in st])

But we have a nice built-in function we can take advantage of. Kyle provided a solution with 'translate'. It looks like::

def code(st): return st.translate(string.maketrans(string.lowercase[3:]+string.lowercase[:3], string.lowercase)

The 'maketrans' method is a shortcut for how I built 'rotated' above. The string method 'translate' leaves unmapped characters untouched, so we don't have to deal with spaces. The simple translate above could be made to do this.

Creation of a reverse-lookup dictionary isn't hard, and it can apply special cases like spaces and other characters we want to preserve much more easily. If we were to add capital letters, this way would handle it very simply, but the solution above would get complex.

Jenn supplied a solution similar to my first above, but cleverly splits on words instead of special-casing the space. Here the round-the-bend problem was solved with a special test instead of 'mod'. Here's the code::

   encoded='wkh udlq lq vsdlq vwdbv pdlqob lq wkh sodlq'
   decoded=[]

   # Spaces aren't encoded, so handle each word separately
   for w in encoded.split():
     localword=''
     for l in w:
       # Handle a and b, which need to "wrap"; "a" is ASCII 97, and our offset is 3
       # If the letter's not a or b, find the ordinal
       #   that's three less and convert back to a letter
       if ord(l)-3>=97:
         localword=localword+chr(ord(l)-3)
       # If the letter three positions back is less than "a", go forward instead
       else:
         localword=localword+chr(ord(l)+23)
     # Make a list of the decoded words
     decoded.append(localword)

   # Reconstitute a string
   print ' '.join(decoded)

The one-line version is:

   ' '.join([''.join([(ord(l)-3)>96 and chr(ord(l)-3) or chr(ord(l)+23) for l in w]) for w in encoded.split()])

She also has a more general version.

Chuck also has a variant of the first way::

 ''.join(map(lambda i: (i < 99) and chr(i+23) or chr(i-3), map(lambda c: (c == ' ') and 9 or ord(c), s1)))

One other popular way to solve this is to use a list of the alphabet and increase its index instead of using ord and chr. For instance, via Chuck::

abc = 'abcdefghijklmnopqrstuvwxyz '
print ''.join(map(lambda i: i > 25 and abc[i] or (i < 3 and abc[i+23] or abc[i-3]), map(lambda c: abc.find(c), s1)))

This might be made to use 'mod' as well.
Created by jccooper
Last modified 2006-08-09 17:24

Solutions here

Posted by jccooper at 2006-07-31 12:03
To prevent spoilers, post your solutions in reply to this post.

Re: Solutions here

Posted by kclarks at 2006-07-31 14:05
Here is my method: (I will admit, though, that the only reason I have this answer is because I had seen basically the same problem in another python challenge and this was an answer that other people had suggested)

It is one line as long as you don't count importing the string module:
>>> import string
>>> "wkh udlq lq vsdlq vwdbv pdlqob lq wkh sodlq".translate(string.maketrans(string.lowercase[3:]+string.lowercase[:3], string.lowercase))
'the rain in spain stays mainly in the plain'

It is very easily reversible, just switch around the parameters in the call to maketrans:
>>> 'the rain in spain stays mainly in the plain'.translate(string.maketrans(string.lowercase, string.lowercase[3:]+string.lowercase[:3]))
'wkh udlq lq vsdlq vwdbv pdlqob lq wkh sodlq'

Re: Solutions here

Posted by cbearden at 2006-08-01 09:41
Hey Cameron, did you set up this challenge to get us to use the ternary operator techniques developed in the prior challenge?

My basic approach was to use ord() to convert the characters to integers, then to subtract 3 from the integers, and then to use chr() to convert the integers back to characters. This could easily be done in one line with two map() functions.

There are two exceptions I have to account for. The alphabetic characters I am interested in are all in the range 97-122 (inclusive), and as I count backwards from 'a' (97), 'b' (98), or 'c' (99), I want to wind up at the high end of the range with 'x' (120), 'y' (121), and 'z' (122). So, for 'a', 'b', or 'c', I need to add 23 instead of subtracting 3 in order to "take them around the Horn" and decode them as 'x', 'y', and 'z'.

Second, the integer value of ' ' is 32, which is of course not in the range (97-122) I otherwise want to handle. In fact, I want it to stay unchanged, so I have to find a way to passi it unchanged through the function.

As noted above, I use two map()s. The inner map() uses a ternary construct to test the input char to see if it is a space. If it is, 9 is returned; if not, the int value of the char is returned via ord().

The outer map() also uses a ternary construct to decide if it needs to subtract 3 from the int value or add 23 to the int value before converting back to a char via chr(). If the int value is less than 99 ('d'), then 23 is added to the int to "take it around the Horn" before conversion back to a char. Otherwise, 3 is subtracted from the int value before conversion back to a char (the base case). Because the value of a space passed to this outer map() is 9, 23 is added to the value yielding 32, which is the int value of a space. Thus both spaces and the encoding of 'a' 'b' 'c' are correctly handled.

s1 = 'wkh udlq lq vsdlq vwdbv pdlqob lq wkh sodlq'
s2 = 'the rain in spain stays mainly in the plain'

print ''.join(map(lambda i: (i < 99) and chr(i+23) or chr(i-3), map(lambda c: (c == ' ') and 9 or ord(c), s1)))

Decoding is just the reverse of encoding:

print ''.join(map(lambda i: (i > 119) and chr(i-23) or chr(i+3), map(lambda c: (c == ' ') and 29 or ord(c), s2)))

I admit I have no idea if this is the most time-efficient way. I suspect that it is pretty efficient, since ord() and chr() are probably rather simple, low-level functions.

I can think of another interesting (to me, at least) way of doing this, and if I get time, I'll respond with it as well.

Thank you for indulging my prolixity.

Re: Solutions here

Posted by cbearden at 2006-08-01 09:56
Here's my other solution:

abc = 'abcdefghijklmnopqrstuvwxyz '
print ''.join(map(lambda i: i > 25 and abc[i] or (i < 3 and abc[i+23] or abc[i-3]), map(lambda c: abc.find(c), s1)))

To make it truly a one-line solution, replace the variable abc with the literal value in each occurrence.

This solution can be extended to handle punctuation and other non-alpha characters that should be passed through without conversion by appending them to the abc string.

Re: Solutions here

Posted by jenn at 2006-08-01 17:49
Well, crud; I've got indentation in my answer, and that doesn't come through in simpleblog. Check it out at http://mountainbunker.org/~jenn/python-challenge-3.txt. My one-liner was:

' '.join([''.join([(ord(l)-3)>96 and chr(ord(l)-3) or chr(ord(l)+23) for l in w]) for w in encoded.split()])

...but I did generic/reversible and translate()-based ones too.

Re: Challenge #3: Encoding, 31 July 2006

Posted by jenn at 2006-08-10 11:14
> 'mod' operator for rounding the corner
> at the end of the alphabet

Oh ho! Knew I was missing something clever there. Cool.