Instructions
Create an implementation of the Atbash cipher, an ancient encryption system created in the Middle East.
The Atbash cipher is a simple substitution cipher that relies on transposing all the letters in the alphabet such that the resulting alphabet is backwards.
The first letter is replaced with the last letter, the second with the second-last, and so on.
An Atbash cipher for the Latin alphabet would be as follows:
Plain: abcdefghijklmnopqrstuvwxyz
Cipher: zyxwvutsrqponmlkjihgfedcba
It is a very weak cipher because it only has one possible key, and it is a simple mono-alphabetic substitution cipher.
However, this may not have been an issue in the cipher’s time.
Ciphertext is written out in groups of fixed length, the traditional group size being 5 letters, leaving numbers unchanged, and punctuation is excluded.
This is to make it harder to guess things based on word boundaries.
All text will be encoded as lowercase letters.
Examples
- Encoding
test gives gvhg
- Encoding
x123 yes gives c123b vh
- Decoding
gvhg gives test
- Decoding
gsvjf rxpyi ldmul cqfnk hlevi gsvoz abwlt gives thequickbrownfoxjumpsoverthelazydog
Dig Deeper
Mono-function
Approach: Mono-function
Notice that there the majority of the code is repetitive?
A fun way to solve this would be to keep it all inside the encode function, and merely chunk it if decode is False:
For variation, this approach shows a different way to translate the text.
from string import ascii_lowercase as asc_low
ENCODING = {chr: asc_low[id] for id, chr in enumerate(asc_low[::-1])}
def encode(text: str, decode: bool = False):
res = "".join(ENCODING.get(chr, chr) for chr in text.lower() if chr.isalnum())
return res if decode else " ".join(res[index:index+5] for index in range(0, len(res), 5))
def decode(text: str):
return encode(text, True)
To explain the translation: we use a dict comprehension in which we reverse the ASCII lowercase digits, and enumerate through them - that is, z is 0, y is 1, and so on.
We access the character at that index and set it to the value of c - so z translates to a.
In the calculation of the result, we try to obtain the value of the character using dict.get, which accepts a default parameter.
In this case, the character itself is the default - that is, numbers won’t be found in the translation key, and thus should remain as numbers.
We use a ternary operator to check if we actually mean to decode the function, in which case we return the result as is.
If not, we chunk the result by joining every five characters with a space.
Another possible way to solve this would be to use a function that returns a function that encodes or decodes based on the parameters:
from string import ascii_lowercase as alc
lowercase = {chr: alc[id] for id, chr in enumerate(alc[::-1])}
def code(decode=False):
def func(text):
line = "".join(lowercase.get(chr, chr) for chr in text.lower() if chr.isalnum())
return line if decode else " ".join(line[index:index+5] for index in range(0, len(line), 5))
return func
encode = code()
decode = code(True)
The logic is the same - we’ve instead used one function that generates two other functions based on the boolean value of its parameter.
encode is set to the function that’s returned, and performs encoding.
decode is set a function that decodes.
Separate Functions
Approach: Separate Functions
We use str.maketrans to create the encoding.
.maketrans/.translate is extremely fast compared to other methods of translation.
If you’re interested, read more about it.
In encode, we use a generator expression in str.join, which is more efficient - and neater - than a list comprehension.
from string import ascii_lowercase
ENCODING = str.maketrans(ascii_lowercase, ascii_lowercase[::-1])
def encode(text: str):
res = "".join(chr for chr in text.lower() if chr.isalnum()).translate(ENCODING)
return " ".join(res[index:index+5] for index in range(0, len(res), 5))
def decode(text: str):
return "".join(chr.lower() for chr in text if chr.isalnum()).translate(ENCODING)
In encode, we first join together every character if the character is alphanumeric - as we use text.lower(), the characters are all lowercase as needed.
Then, we translate it and return a version joining every five characters with a space in between.
decode does the exact same thing, except it doesn’t return a chunked output.
Instead of cleaning the input by checking that it’s alphanumeric, we check that it’s not a whitespace character.
It might be cleaner to use helper functions:
from string import ascii_lowercase
ENCODING = str.maketrans(ascii_lowercase, ascii_lowercase[::-1])
def clean(text):
return "".join([chr.lower() for chr in text if chr.isalnum()])
def chunk(text):
return " ".join(text[index:index+5] for index in range(0, len(text), 5))
def encode(text):
return chunk(clean(text).translate(ENCODING))
def decode(text):
return clean(text).translate(ENCODING)
Note that checking that chr is alphanumeric achieves the same result as checking that it’s not whitespace, although it’s not as explicit.
As this is a helper function, this is acceptable enough.
You can also make chunk recursive:
def chunk(text):
if len(text) <= 5:
return text
return text[:5] + " " + chunk(text[5:])
Source: Exercism python/atbash-cipher