Skip to content

Commit 79149c3

Browse files
Yrahcaz7BethanyG
andauthored
RNA Transcription and Atbash Cipher approach cleanup (exercism#4191)
* [Atbash Cipher] Greatly improve approaches * [RNA Transcription] Typo fix & grammar fix * Apply suggestions from code review Co-authored-by: BethanyG <BethanyG@users.noreply.github.com> * fix characters vs code points --------- Co-authored-by: BethanyG <BethanyG@users.noreply.github.com>
1 parent 140b712 commit 79149c3

10 files changed

Lines changed: 106 additions & 75 deletions

File tree

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
{
22
"introduction": {
3-
"authors": ["safwansamsudeen"]
3+
"authors": ["safwansamsudeen"],
4+
"contributors": ["yrahcaz7"]
45
},
56
"approaches": [
67
{
78
"uuid": "920e6d08-e8fa-4bef-b2f4-837006c476ae",
89
"slug": "mono-function",
910
"title": "Mono-function",
1011
"blurb": "Use one function for both tasks",
11-
"authors": ["safwansamsudeen"]
12+
"authors": ["safwansamsudeen"],
13+
"contributors": ["yrahcaz7"]
1214
},
1315
{
1416
"uuid": "9a7a17e0-4ad6-4d97-a8b9-c74d47f3e000",
1517
"slug": "separate-functions",
16-
"title": "Separate Functions",
18+
"title": "Separate functions",
1719
"blurb": "Use separate functions, and perhaps helper ones",
18-
"authors": ["safwansamsudeen"]
20+
"authors": ["safwansamsudeen"],
21+
"contributors": ["yrahcaz7"]
1922
}
2023
]
2124
}

exercises/practice/atbash-cipher/.approaches/introduction.md

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,54 @@
11
# Introduction
2+
23
Atbash cipher in Python can be solved in many ways.
34

45
## General guidance
5-
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.
6-
Then, you have to "clean" up the string to be encoded by removing numbers/whitespace.
6+
7+
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.
8+
Next, you have to "clean" up the string to be encoded by removing punctuation/whitespace.
79
Finally, you break it up into chunks of five before returning it.
810

9-
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.
11+
For decoding, the process is similar — clean up (_which automatically joins the chunks_) and translate using the **_same_** key — realizing that the same key can be used is crucial in solving this in an idiomatic manner.
12+
13+
## Approach: Separate functions
14+
15+
We use `str.maketrans()` to create the encoding.
16+
In `encode()`, we use a [generator expression][generator-expression] in `str.join()`.
1017

11-
## Approach: separate functions
12-
We use `str.maketrans` to create the encoding.
13-
In `encode`, we use a [generator expression][generator-expression] in `str.join`.
1418
```python
1519
from string import ascii_lowercase
20+
1621
ENCODING = str.maketrans(ascii_lowercase, ascii_lowercase[::-1])
1722

18-
def encode(text: str):
23+
def encode(text):
1924
res = "".join(chr for chr in text.lower() if chr.isalnum()).translate(ENCODING)
2025
return " ".join(res[index:index+5] for index in range(0, len(res), 5))
2126

22-
def decode(text: str):
23-
return "".join(chr.lower() for chr in text if chr.isalnum()).translate(ENCODING)
27+
def decode(text):
28+
return "".join(chr.lower() for chr in text if not chr.isspace()).translate(ENCODING)
2429
```
30+
2531
Read more on this [approach here][approach-separate-functions].
2632

27-
## Approach: mono-function
28-
Notice that there the majority of the code is repetitive?
29-
A fun way to solve this would be to keep it all inside the `encode` function, and merely chunk it if `decode` is False:
30-
For variation, this approach shows a different way to translate the text.
33+
## Approach: Mono-function
34+
35+
Notice that the majority of the code is repetitive?
36+
A fun way to solve this would be to keep it all inside the `encode()` function, and merely chunk it if `decode` is `False`:
37+
For variation, this approach also shows a different way to translate the text.
38+
3139
```python
3240
from string import ascii_lowercase as asc_low
41+
3342
ENCODING = {chr: asc_low[id] for id, chr in enumerate(asc_low[::-1])}
3443

35-
def encode(text: str, decode: bool = False):
36-
res = "".join(ENCODING.get(chr, chr) for chr in text.lower() if chr.isalnum())
37-
return res if decode else " ".join(res[index:index+5] for index in range(0, len(res), 5))
44+
def encode(text, decode = False):
45+
line = "".join(ENCODING.get(chr, chr) for chr in text.lower() if chr.isalnum())
46+
return line if decode else " ".join(line[index:index+5] for index in range(0, len(line), 5))
3847

39-
def decode(text: str):
48+
def decode(text):
4049
return encode(text, True)
4150
```
51+
4252
For more detail, [read here][approach-mono-function].
4353

4454
[approach-separate-functions]: https://exercism.org/tracks/python/exercises/atbash-cipher/approaches/separate-functions
Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,54 @@
1-
## Approach: Mono-function
2-
Notice that there the majority of the code is repetitive?
3-
A fun way to solve this would be to keep it all inside the `encode` function, and merely chunk it if `decode` is False:
4-
For variation, this approach shows a different way to translate the text.
1+
# Approach: Mono-function
2+
3+
Notice that the majority of the code is repetitive?
4+
A fun way to solve this would be to keep it all inside the `encode()` function, and merely chunk it if `decode` is `False`:
5+
For variation, this approach also shows a different way to translate the text.
6+
57
```python
68
from string import ascii_lowercase as asc_low
9+
710
ENCODING = {chr: asc_low[id] for id, chr in enumerate(asc_low[::-1])}
811

9-
def encode(text: str, decode: bool = False):
10-
res = "".join(ENCODING.get(chr, chr) for chr in text.lower() if chr.isalnum())
11-
return res if decode else " ".join(res[index:index+5] for index in range(0, len(res), 5))
12+
def encode(text, decode = False):
13+
line = "".join(ENCODING.get(chr, chr) for chr in text.lower() if chr.isalnum())
14+
return line if decode else " ".join(line[index:index+5] for index in range(0, len(line), 5))
1215

13-
def decode(text: str):
16+
def decode(text):
1417
return encode(text, True)
1518
```
16-
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.
17-
We access the character at that index and set it to the value of `c` - so `z` translates to `a`.
1819

19-
In the calculation of the result, we try to obtain the value of the character using `dict.get`, which accepts a default parameter.
20-
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.
20+
Here, we use a dictionary comprehension in which we reverse the order of the ASCII lowercase digits and enumerate through them — that is, `z` is at index 0, `y` is at index 1, and so on.
21+
For each code point, we set the value of `chr` in the resulting dictionary to the code point at the respective index — so `z` translates to `a`.
22+
23+
In the calculation of the result, we try to obtain the value of the code point using `dict.get()`, which accepts a default parameter.
24+
In this case, the code point itself is the default — that is, numbers won't be found in the translation key, and thus should remain as numbers.
25+
26+
We use a [conditional expression (also known as a ternary operator)][conditional-expression] to check if we actually mean to decode the function, in which case we return the result as is.
27+
If not, we "chunk" the result by joining every five code points with a space.
2128

22-
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.
23-
If not, we chunk the result by joining every five characters with a space.
29+
Another possible way to solve this would be to use a function that returns another function (_a higher-order function or [closure][closure]_) that encodes or decodes based on the outer function's parameter:
2430

25-
Another possible way to solve this would be to use a function that returns a function that encodes or decodes based on the parameters:
2631
```python
27-
from string import ascii_lowercase as alc
32+
from string import ascii_lowercase as asc_low
2833

29-
lowercase = {chr: alc[id] for id, chr in enumerate(alc[::-1])}
34+
ENCODING = {chr: asc_low[id] for id, chr in enumerate(asc_low[::-1])}
3035

31-
def code(decode=False):
36+
def code(decode = False):
3237
def func(text):
33-
line = "".join(lowercase.get(chr, chr) for chr in text.lower() if chr.isalnum())
38+
line = "".join(ENCODING.get(chr, chr) for chr in text.lower() if chr.isalnum())
3439
return line if decode else " ".join(line[index:index+5] for index in range(0, len(line), 5))
3540
return func
3641

37-
3842
encode = code()
3943
decode = code(True)
4044
```
41-
The logic is the same - we've instead used one function that generates two _other_ functions based on the boolean value of its parameter.
42-
`encode` is set to the function that's returned, and performs encoding.
43-
`decode` is set a function that _decodes_.
4445

45-
[ternary-operator]: https://www.tutorialspoint.com/ternary-operator-in-python
46-
[decorator]: https://realpython.com/primer-on-python-decorators/
46+
The logic is the same — the only change is that now we use use one function that generates two _other_ functions based on the boolean value of its parameter.
47+
48+
Here, we first call `code()` with no argument and set `encode` to the function that's returned, which performs encoding.
49+
Then we call `code(True)` to get the decoding version of the function and set `decode` to that function.
50+
51+
After that, we can call `encode()` and `decode()` as normal, and both functions successfully perform their indended task.
52+
53+
[closure]: https://realpython.com/python-closure/
54+
[conditional-expression]: https://docs.python.org/3/reference/expressions.html#conditional-expressions
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from string import ascii_lowercase as asc_low
22
ENCODING = {chr: asc_low[id] for id, chr in enumerate(asc_low[::-1])}
33

4-
def encode(text: str, decode: bool = False):
5-
res = "".join(ENCODING.get(chr, chr) for chr in text.lower() if chr.isalnum())
6-
return res if decode else " ".join(res[index:index+5] for index in range(0, len(res), 5))
7-
def decode(text: str):
4+
def encode(text, decode = False):
5+
line = "".join(ENCODING.get(chr, chr) for chr in text.lower() if chr.isalnum())
6+
return line if decode else " ".join(line[index:index+5] for index in range(0, len(line), 5))
7+
def decode(text):
88
return encode(text, True)
Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,57 @@
1-
## Approach: Separate Functions
2-
We use `str.maketrans` to create the encoding.
3-
`.maketrans`/`.translate` is extremely fast compared to other methods of translation.
4-
If you're interested, [read more][str-maketrans] about it.
1+
# Approach: Separate functions
2+
3+
We use `str.maketrans()` to create the encoding.
4+
`str.maketrans()`/`str.translate()` is extremely fast compared to other methods of translation.
5+
If you're interested, you can [read more about it here][str-maketrans].
6+
7+
In `encode()`, we use a [generator expression][generator-expression] in `str.join()`, which is more efficient — and neater — than a list comprehension.
58

6-
In `encode`, we use a [generator expression][generator-expression] in `str.join`, which is more efficient - and neater - than a list comprehension.
79
```python
810
from string import ascii_lowercase
11+
912
ENCODING = str.maketrans(ascii_lowercase, ascii_lowercase[::-1])
1013

11-
def encode(text: str):
14+
def encode(text):
1215
res = "".join(chr for chr in text.lower() if chr.isalnum()).translate(ENCODING)
1316
return " ".join(res[index:index+5] for index in range(0, len(res), 5))
1417

15-
def decode(text: str):
16-
return "".join(chr.lower() for chr in text if chr.isalnum()).translate(ENCODING)
18+
def decode(text):
19+
return "".join(chr.lower() for chr in text if not chr.isspace()).translate(ENCODING)
1720
```
18-
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.
19-
Then, we translate it and return a version joining every five characters with a space in between.
2021

21-
`decode` does the exact same thing, except it doesn't return a chunked output.
22-
Instead of cleaning the input by checking that it's alphanumeric, we check that it's not a whitespace character.
22+
In `encode()`, we first join together every code point that is an alphanumeric character — as we use `text.lower()`, the characters are all lowercase as needed.
23+
Then, we translate it and return a version joining every five code points with a space in between.
24+
25+
`decode()` does the exact same thing, except it doesn't return a chunked output and it cleans the input differently.
26+
To clean the input, `decode()` only removes code points that are whitespace characters instead of all non-alphanumeric characters.
2327

2428
It might be cleaner to use helper functions:
29+
2530
```python
2631
from string import ascii_lowercase
32+
2733
ENCODING = str.maketrans(ascii_lowercase, ascii_lowercase[::-1])
34+
35+
2836
def clean(text):
2937
return "".join([chr.lower() for chr in text if chr.isalnum()])
38+
3039
def chunk(text):
3140
return " ".join(text[index:index+5] for index in range(0, len(text), 5))
3241

42+
3343
def encode(text):
3444
return chunk(clean(text).translate(ENCODING))
3545

3646
def decode(text):
3747
return clean(text).translate(ENCODING)
3848
```
39-
Note that checking that `chr` _is_ alphanumeric achieves the same result as checking that it's _not_ whitespace, although it's not as explicit.
49+
50+
Note that for `decode()`, checking that `chr` _is_ alphanumeric achieves the same result as checking that it _is not_ whitespace, although it's not as explicit.
4051
As this is a helper function, this is acceptable enough.
4152

42-
You can also make `chunk` recursive:
53+
You can also make `chunk()` recursive, but this is not recommended:
54+
4355
```python
4456
def chunk(text):
4557
if len(text) <= 5:
@@ -48,4 +60,4 @@ def chunk(text):
4860
```
4961

5062
[generator-expression]: https://www.programiz.com/python-programming/generator
51-
[str-maketrans]: https://www.programiz.com/python-programming/methods/string/maketrans
63+
[str-maketrans]: https://www.programiz.com/python-programming/methods/string/maketrans
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from string import ascii_lowercase
22
ENCODING = str.maketrans(ascii_lowercase, ascii_lowercase[::-1])
33

4-
def encode(text: str):
4+
def encode(text):
55
res = "".join(chr for chr in text.lower() if chr.isalnum()).translate(ENCODING)
66
return " ".join(res[index:index+5] for index in range(0, len(res), 5))
7-
def decode(text: str):
7+
def decode(text):
88
return "".join(chr.lower() for chr in text if not chr.isspace()).translate(ENCODING)

exercises/practice/rna-transcription/.approaches/config.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@
99
"slug": "translate-maketrans",
1010
"title": "translate maketrans",
1111
"blurb": "Use translate with maketrans to return the value.",
12-
"authors": ["bobahop"]
12+
"authors": ["bobahop"],
13+
"contributors": ["yrahcaz7"]
1314
},
1415
{
1516
"uuid": "fbc6be87-dec4-4c4b-84cf-fcc1ed2d6d41",
1617
"slug": "dictionary-join",
1718
"title": "dictionary join",
1819
"blurb": "Use a dictionary look-up with join to return the value.",
19-
"authors": ["bobahop"]
20+
"authors": ["bobahop"],
21+
"contributors": ["yrahcaz7"]
2022
}
2123
]
2224
}

exercises/practice/rna-transcription/.approaches/dictionary-join/content.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ LOOKUP = {'G': 'C', 'C': 'G', 'T': 'A', 'A': 'U'}
66

77
def to_rna(dna_strand):
88
return ''.join(LOOKUP[nucleotide] for nucleotide in dna_strand)
9-
109
```
1110

1211
This approach starts by defining a [dictionary][dictionaries] to map the DNA values to RNA values.
@@ -18,7 +17,7 @@ It indicates that the value is not intended to be changed.
1817
In the `to_rna()` function, the [`join()`][join] method is called on an empty string,
1918
and is passed the list created from a [generator expression][generator-expression].
2019

21-
The generator expression iterates each character in the input,
20+
The generator expression iterates over each code point in the input,
2221
looks up the DNA character in the look-up dictionary, and outputs its matching RNA character as an element in the list.
2322

2423
The `join()` method collects the RNA characters back into a string.

exercises/practice/rna-transcription/.approaches/introduction.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ LOOKUP = str.maketrans('GCTA', 'CGAU')
1818

1919
def to_rna(dna_strand):
2020
return dna_strand.translate(LOOKUP)
21-
2221
```
2322

2423
For more information, check the [`translate()` with `maketrans()` approach][approach-translate-maketrans].
@@ -31,7 +30,6 @@ LOOKUP = {'G': 'C', 'C': 'G', 'T': 'A', 'A': 'U'}
3130

3231
def to_rna(dna_strand):
3332
return ''.join(LOOKUP[nucleotide] for nucleotide in dna_strand)
34-
3533
```
3634

3735
For more information, check the [dictionary look-up with `join()` approach][approach-dictionary-join].

exercises/practice/rna-transcription/.approaches/translate-maketrans/content.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ LOOKUP = str.maketrans('GCTA', 'CGAU')
66

77
def to_rna(dna_strand):
88
return dna_strand.translate(LOOKUP)
9-
109
```
1110

1211
This approach starts by defining a [dictionary][dictionaries] (also called a translation table in this context) by calling the [`maketrans()`][maketrans] method.
@@ -18,7 +17,7 @@ It indicates that the value is not intended to be changed.
1817
The translation table that is created uses the [Unicode][Unicode] _code points_ (sometimes called the ordinal values) for each letter in the two strings.
1918
As Unicode was designed to be backwards compatible with [ASCII][ASCII] and because the exercise uses Latin letters, the code points in the translation table can be interpreted as ASCII.
2019
However, the functions can deal with any Unicode character.
21-
You can learn more by reading about [strings and their representation in the Exercism Python syllabus][concept-string].
20+
You can learn more by reading about [strings and their representation in the Exercism Python syllabus][concept-strings].
2221

2322
The Unicode value for "G" in the first string is the key for the Unicode value of "C" in the second string, and so on.
2423

0 commit comments

Comments
 (0)