Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions exercises/practice/atbash-cipher/.approaches/config.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
{
"introduction": {
"authors": ["safwansamsudeen"]
"authors": ["safwansamsudeen"],
"contributors": ["yrahcaz7"]
},
"approaches": [
{
"uuid": "920e6d08-e8fa-4bef-b2f4-837006c476ae",
"slug": "mono-function",
"title": "Mono-function",
"blurb": "Use one function for both tasks",
"authors": ["safwansamsudeen"]
"authors": ["safwansamsudeen"],
"contributors": ["yrahcaz7"]
},
{
"uuid": "9a7a17e0-4ad6-4d97-a8b9-c74d47f3e000",
"slug": "separate-functions",
"title": "Separate Functions",
"title": "Separate functions",
"blurb": "Use separate functions, and perhaps helper ones",
"authors": ["safwansamsudeen"]
"authors": ["safwansamsudeen"],
"contributors": ["yrahcaz7"]
}
]
}
44 changes: 27 additions & 17 deletions exercises/practice/atbash-cipher/.approaches/introduction.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,54 @@
# 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.

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.
Next, you have to "clean" up the string to be encoded by removing punctuation/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.
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.

## Approach: Separate functions

We use `str.maketrans()` to create the encoding.
In `encode()`, we use a [generator expression][generator-expression] in `str.join()`.

## 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):
def encode(text):
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)
def decode(text):
return "".join(chr.lower() for chr in text if not chr.isspace()).translate(ENCODING)
```

Read more on this [approach here][approach-separate-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.
## Approach: Mono-function

Notice that 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 also 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 encode(text, decode = False):
line = "".join(ENCODING.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))

def decode(text: str):
def decode(text):
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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,54 @@
## 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.
# Approach: Mono-function

Notice that 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 also 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 encode(text, decode = False):
line = "".join(ENCODING.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))

def decode(text: str):
def decode(text):
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.
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.
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`.

In the calculation of the result, we try to obtain the value of the code point using `dict.get()`, which accepts a default parameter.
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.

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.
If not, we "chunk" the result by joining every five code points with a space.

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 another function (_a higher-order function or [closure][closure]_) that encodes or decodes based on the outer function's parameter:

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
from string import ascii_lowercase as asc_low

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

def code(decode=False):
def code(decode = False):
def func(text):
line = "".join(lowercase.get(chr, chr) for chr in text.lower() if chr.isalnum())
line = "".join(ENCODING.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/
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.

Here, we first call `code()` with no argument and set `encode` to the function that's returned, which performs encoding.
Then we call `code(True)` to get the decoding version of the function and set `decode` to that function.

After that, we can call `encode()` and `decode()` as normal, and both functions successfully perform their indended task.

[closure]: https://realpython.com/python-closure/
[conditional-expression]: https://docs.python.org/3/reference/expressions.html#conditional-expressions
Comment thread
Yrahcaz7 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,8 +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):
def encode(text, decode = False):
line = "".join(ENCODING.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))
def decode(text):
return encode(text, True)
Original file line number Diff line number Diff line change
@@ -1,45 +1,57 @@
## 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.
# Approach: Separate functions

We use `str.maketrans()` to create the encoding.
`str.maketrans()`/`str.translate()` is extremely fast compared to other methods of translation.
If you're interested, you can [read more about it here][str-maketrans].

In `encode()`, we use a [generator expression][generator-expression] in `str.join()`, which is more efficient — and neater — than a list comprehension.

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):
def encode(text):
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)
def decode(text):
return "".join(chr.lower() for chr in text if not chr.isspace()).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.
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.
Then, we translate it and return a version joining every five code points with a space in between.

`decode()` does the exact same thing, except it doesn't return a chunked output and it cleans the input differently.
To clean the input, `decode()` only removes code points that are whitespace characters instead of all non-alphanumeric characters.

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.

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.
As this is a helper function, this is acceptable enough.

You can also make `chunk` recursive:
You can also make `chunk()` recursive, but this is not recommended:

```python
def chunk(text):
if len(text) <= 5:
Expand All @@ -48,4 +60,4 @@ def chunk(text):
```

[generator-expression]: https://www.programiz.com/python-programming/generator
[str-maketrans]: https://www.programiz.com/python-programming/methods/string/maketrans
[str-maketrans]: https://www.programiz.com/python-programming/methods/string/maketrans
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from string import ascii_lowercase
ENCODING = str.maketrans(ascii_lowercase, ascii_lowercase[::-1])

def encode(text: str):
def encode(text):
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):
def decode(text):
return "".join(chr.lower() for chr in text if not chr.isspace()).translate(ENCODING)
6 changes: 4 additions & 2 deletions exercises/practice/rna-transcription/.approaches/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
"slug": "translate-maketrans",
"title": "translate maketrans",
"blurb": "Use translate with maketrans to return the value.",
"authors": ["bobahop"]
"authors": ["bobahop"],
"contributors": ["yrahcaz7"]
},
{
"uuid": "fbc6be87-dec4-4c4b-84cf-fcc1ed2d6d41",
"slug": "dictionary-join",
"title": "dictionary join",
"blurb": "Use a dictionary look-up with join to return the value.",
"authors": ["bobahop"]
"authors": ["bobahop"],
"contributors": ["yrahcaz7"]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ LOOKUP = {'G': 'C', 'C': 'G', 'T': 'A', 'A': 'U'}

def to_rna(dna_strand):
return ''.join(LOOKUP[nucleotide] for nucleotide in dna_strand)

```

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

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

The `join()` method collects the RNA characters back into a string.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ LOOKUP = str.maketrans('GCTA', 'CGAU')

def to_rna(dna_strand):
return dna_strand.translate(LOOKUP)

```

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

def to_rna(dna_strand):
return ''.join(LOOKUP[nucleotide] for nucleotide in dna_strand)

```

For more information, check the [dictionary look-up with `join()` approach][approach-dictionary-join].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ LOOKUP = str.maketrans('GCTA', 'CGAU')

def to_rna(dna_strand):
return dna_strand.translate(LOOKUP)

```

This approach starts by defining a [dictionary][dictionaries] (also called a translation table in this context) by calling the [`maketrans()`][maketrans] method.
Expand All @@ -18,7 +17,7 @@ It indicates that the value is not intended to be changed.
The translation table that is created uses the [Unicode][Unicode] _code points_ (sometimes called the ordinal values) for each letter in the two strings.
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.
However, the functions can deal with any Unicode character.
You can learn more by reading about [strings and their representation in the Exercism Python syllabus][concept-string].
You can learn more by reading about [strings and their representation in the Exercism Python syllabus][concept-strings].

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.

Expand Down
Loading