Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Approaches for Atbash Cipher #3457

Merged
merged 13 commits into from
Jul 31, 2023
21 changes: 21 additions & 0 deletions exercises/practice/atbash-cipher/.approaches/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"introduction": {
"authors": ["safwansamsudeen"]
},
"approaches": [
{
"uuid": "920e6d08-e8fa-4bef-b2f4-837006c476ae",
"slug": "mono-function",
"title": "Mono-function",
"blurb": "Use one function for both tasks",
"authors": ["safwansamsudeen"]
},
{
"uuid": "9a7a17e0-4ad6-4d97-a8b9-c74d47f3e000",
"slug": "separate-functions",
"title": "Separate Functions",
"blurb": "Use separate functions, and perhaps helper ones",
"authors": ["safwansamsudeen"]
}
]
}
46 changes: 46 additions & 0 deletions exercises/practice/atbash-cipher/.approaches/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Introduction
Atbash cipher in Python can be solved in many ways.

## General guidance
The first thing is to have a "key" mapping - possibly in a `dict` or `str.maketrans`, otherwise the value would have to be calculated on the fly.
Then, you have to "clean" up the string to be encoded by removing numbers/whitespace.
Finally, you break it up into chunks of five before returning it.

For decoding, it's similar - clean up (which automatically joins the chunks) and translate using the _same_ key - the realization that the same key can be used is crucial in solving this in an idiomatic manner.

## Approach: separate functions
We use `str.maketrans` to create the encoding.
In `encode`, we use a [generator expression][generator expression] in `str.join`.
```python
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)
```
Read more on this [approach here][approach-seperate-functions].

## 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.
```python
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)
```
For more detail, [read here][approach-mono-function].

[approach-separate-functions]: https://exercism.org/tracks/python/exercises/atbash-cipher/approaches/separate-functions
[approach-mono-function]: https://exercism.org/tracks/python/exercises/atbash-cipher/approaches/mono-function
[generator expression]: https://www.programiz.com/python-programming/generator
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
## 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.
```python
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][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:
```python
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_.

[ternary-operator]: https://www.tutorialspoint.com/ternary-operator-in-python
[decorator]: https://realpython.com/primer-on-python-decorators/
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
## 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][str-maketrans] about it.

In `encode`, we use a [generator expression][generator-expression] in `str.join`, which is more efficient - and neater - than a list comprehension.
```python
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:
```python
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:
```python
def chunk(text):
if len(text) <= 5:
return text
return text[:5] + " " + chunk(text[5:])
```

[generator-expression]: https://www.programiz.com/python-programming/generator
[str-maketrans]: https://www.programiz.com/python-programming/methods/string/maketrans
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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 not chr.isspace()).translate(ENCODING)
Loading