Skip to content

Commit

Permalink
Approaches for Atbash Cipher (#3457)
Browse files Browse the repository at this point in the history
* first round

* correct typo

* correct yet another typo

* Apply suggestions from code review

Co-authored-by: BethanyG <BethanyG@users.noreply.github.com>

* replace most single variable names

* replace i to index

* Apply suggestions from code review

Committing suggestions so the PR can be merged.

---------

Co-authored-by: BethanyG <BethanyG@users.noreply.github.com>
  • Loading branch information
safwansamsudeen and BethanyG authored Jul 31, 2023
1 parent 6c9a7ea commit e3edaf8
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 0 deletions.
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)

0 comments on commit e3edaf8

Please sign in to comment.