A student of mine was thinking out loud about encoding messages and it got stuck in my head. The next day I challenged my Coder School students to try it. How do you even replace a letter with a number? You could use dozens of if-statements:
for letter in message:
if letter == 'a':
print(1)
elif letter == 'b':
print(2)
And so on. But an easier way is to create a string of characters:
ALPHA = "abcdefghijklmnopqrstuvwxyz ',.?"
Now each letter has a number, an index, its place in the string. The letter a is ALPHA[0], b is ALPHA[1] and so on . You can replace each letter with its index this way:
def encode(msg):
'''takes a message and prints number code'''
for letter in msg:
print(ALPHA.index(letter), end=' ')
The last bit is thanks to Python 3, where print is a function. After printing the number you can specify printing something at the end, like a space. And it doesn't automatically print a line break. Running encode('call me ishmael.') we get
2 0 11 11 26 12 4 26 8 18 7 12 0 4 11 28
The decode function will require a little Python trickery. The numbers can be fed back in as a list if you're patient enough to type tons of commas, or it can be copied and pasted in, as a string inside quotes, to a function like
decode('2 0 11 11 26 12 4 26 8 18 7 12 0 4 11 28')
The decode function converts the string to a list using Python's split() function. Then you can just iterate over the list, printing the letter in the ALPHA list with that index number.
def decode(msg):
'''takes numbers and prints decoded letters'''
msg2 = msg.split() #converts string to list
for item in msg2:
print(ALPHA[int(item)],end = '')
So decoding another message, like
decode('8 26 22 0 13 19 26 19 14 26 7 14 11 3 26 24 14 20 17 26 7 0 13 3 28')
we get
i want to hold your hand.
But that cipher wouldn't fool spies even from a thousand years ago. We need to make it a little sneakier. We could get rid of the spaces if all the numbers were 2-digits long:
def encode(msg):
'''takes a message and returns number code'''
code = ''
for letter in msg:
#get the index of letter and
#convert it into a string
num = str(ALPHA.index(letter))
if len(num) == 1: #if it's only one digit
num = '0' + num #add a zero in front
code += num #add that to the code
print(code)
Now execute encode("beautiful is better than ugly.") and you'll get a more confusing-looking code:
010400201908052011260818260104191904172619070013262006112428
Now decoding it is fairly simple. You just have to take 2-digit slices from the string and print out the element of ALPHA that has that index.
def decode(msg):
'''takes numbers and prints decoded letters'''
for n in range(0,len(msg),2): #n goes up by 2's
print(ALPHA[int(msg[n:n+2])],end='') #take 2-digit slices
Your enemy might notice a bunch of zeroes. My student's suggestion was to convert to binary numbers, make all the numbers 5-digits, then string them together! I already have a binary converter in my book Hacking Math Class, so we'll assume you have the binary function. You've converted the letters to numbers, then send them to this function:
def binary5(number):
'''converts number to 5-digit binary'''
number = binary(number) #get the binary form
number = str(number) #convert to string form
x = len(number) #to find number of digits
number = (5-x)*'0' + number #add zeroes if it's not 5
print(number)
Now entering binary5(5) will add zeros to the front to make it a 5 digit number:
00101
I created strings of even and odd numbers for random choosing:
EVENS = '02468'
ODDS = '13579'
Here's the code (instead of printing) for converting the zeros to any even digit and the ones to any odd digit:
#convert to random evens and odds
binrand = '' #string for random evens or odds
for digit in number: #go over every character
if int(digit) % 2 == 0: #if its integer form is even
#replace with random even digit
binrand += random.choice(EVENS)
else: #otherwise, replace it with random odd digit
binrand += random.choice(ODDS)
return binrand
Here's the new encode function:
def encode(msg):
'''takes a message and returns number code'''
code = '' #empty string for numbers
for letter in msg: #goes over every letter
#take the index of that letter, convert to
#5-digit binary and add that to number string
code += binary5(ALPHA.index(letter))
print(code)
print()#blank line
Now running the encode function will encode our message nicely.
>>> encode("what's my age again?")
9217424331466441621919055700927185047984178469303480480845780254853858208286417028442012044592133956
The first 5-digit slice, '92174', using the evens = '0' and odds = '1' transform, is the binary number 10110. That's 22 in decimal, or w's place in the ALPHA string.
The decode function reverses the process. First it takes the message and slices it into 5-digit slices.
def decode(msg):
'''decodes a message'''
msg2 = [] #list for 5-digit numbers
for n in range(0,len(msg),5): #n goes up by 5's
#add each 5-digit slice to list
msg2.append(int(msg[n:n+5]))
Then it creates a list to store the 5-digit binary numbers and converts all the even digits to zeros and all the odds to ones.
msg3 = [] #list for 5-digit binary numbers
for number in msg2:
number2 = str(number) #turn it into a string
number3 = '' #empty string for binaries
for digit in number2: #go through digit by digit
if digit in EVENS: #if digit is even
digit = '0' #replace by 0
else: digit = '1' #or replace by 1
number3 += digit #add to binary string
msg3.append(number3) #add binary string to message
#print(msg3)
Finally you have to convert all the binary numbers to decimals. It's a good thing leading zeros are ignored by Python's 'int' function. That saves us a step. I already have a function for converting binary to decimal, (called "binDec") so I didn't give the code here.
for number in msg3: #go through binaries
#convert to int, then decimal, then letter:
print(ALPHA[binDec(int(number))],end = '')
print() #blank line after message
Now entering a nonsensical string of unbreakable code will yield a message of power and beauty:
>>> decode('164732271528482768355715312256534944844083853656139
161595498463010797645033218905045253958')
that's all, folks!
Update: Naturally my Python mentor Paddy Gaunt showed me how to do it in an eighth of the code.
import numpy as np
ALPHA = "abcdefghijklmnopqrstuvwxyz ',.?"
ALPHA = {c:i for i,c in enumerate(ALPHA)} # make it into a dict
def encode(str):
narr = np.array([ALPHA[c] for c in str], dtype=np.uint8)
narr = np.unpackbits(narr).reshape(-1, 8)[:,3:].reshape(-1)
evod = np.array([np.random.choice([0,2,4,6,8], len(narr)),
np.random.choice([1,3,5,7,9], len(narr))])
return evod[narr, np.arange(len(narr))]
print(encode("the quick brown fox jumps over the lazy dog"))
No comments:
Post a Comment