Challenge #3: Encoding, 31 July 2006
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.
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.
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.

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'
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.
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.
' '.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.