-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Approaches for Atbash Cipher (#3457)
* 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
1 parent
6c9a7ea
commit e3edaf8
Showing
6 changed files
with
180 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
46
exercises/practice/atbash-cipher/.approaches/introduction.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
46 changes: 46 additions & 0 deletions
46
exercises/practice/atbash-cipher/.approaches/mono-function/content.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
8 changes: 8 additions & 0 deletions
8
exercises/practice/atbash-cipher/.approaches/mono-function/snippet.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
51 changes: 51 additions & 0 deletions
51
exercises/practice/atbash-cipher/.approaches/separate-functions/content.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
8 changes: 8 additions & 0 deletions
8
exercises/practice/atbash-cipher/.approaches/separate-functions/snippet.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |