# Reddit Challenge #380 Python

I'm completed the Reddit challenge #380 (source).

Here is an abbreviated description:

Normally [in Morse code], you would indicate where one letter ends and the next begins, for instance with a space between the letters' codes, but for this challenge, just smoosh all the coded letters together into a single string consisting of only dashes and dots.

An obvious problem with this system is that decoding is ambiguous. For instance, both bits and three encode to the same string, so you can't tell which one you would decode to without more information.

I've done the best I can, but wanted a few insights to what I could do that would be better.

Code

The code is the solution to the Reddit challenge. In the main method, the class is initialised then prints each of the solutions out. Please note that the main challenge and optional challenges 1-4 are included. I did not include optional challenge 5. The class on initialisation converts and groups the words from a file 'wordlist.txt' that was sourced in the reddit post. I will not include the file here as it is pretty long but here is the source. Please ensure you rename the file to 'wordlist.txt'.

Unorthodox choices

1. morse_alphabet variable

PyLint highlighted it as a 'line-too-long' exception. I disabled it since shorting the variable name would not have helped but I'm unsure if I should have split up the line. I kept it there as it was easier than typing it out for each individual letter but maybe it was best to split up the letters?

2. count_longest_dash_run function

It seems like it was an unorthodox solution, maybe there was a way to do this without the comparison?

3. The line repeated in the optional challenges print(key+":"+str(value))

Should I have separated it into a function or would it have been overkill?

4. Documentation

I'm unsure if the documentation, usually people put in return type or parameter information but i'm unsure what it should look like or if what i'm doing is correct.

5. Static methods and class methods

Should I include @typemethod for all the functions or is this specific to certain situations such as the static method?

Python Code

"""
This module is a solution the the programming exercise found on reddit.
Exercise:
This solution is not posted in the reddit thread.
This solution completes the challenge and the optional challenges 1-4.
On running this script, you should see the results with the console.
Each set of results will be labeled with which challenge they refer to.
"""
class Morsecoder:
"""
This class handles generating the morse code for all of the words listed in the wordlist.txt
and storing them. It also handles generating the result for each challenge.
"""
def __init__(self):
""" Create a list of words and its morse code.
Also creates a dict compiling the words with the same more code together. """
# pylint: disable=line-too-long
morse_alphabet = ".- -... -.-. -.. . ..-. --. .... .. .--- -.- .-.. -- -. --- .--. --.- .-. ... - ..- ...- .-- -..- -.-- --.."
self.morse_characters = morse_alphabet.split(" ")
self.words = []
self.encoded_words = {}
self.encode_word_list()
self.find_matching_codes()

def get_morse_character(self, char):
""" Retrieves the morse character of the character.
Return none is the character int is not int the lower case range. """
char_int = ord(char)-97
if char_int >= len(self.morse_characters) or char_int < 0:
return None
return self.morse_characters[char_int]

def encode_string(self, string):
""" Encodes the entire string into the morse code.
Raises an Exception if the string is not a lowercase alphabet only string. """
if string.islower() and string.isalpha():
morsecode = ''
for char in string:
morsecode += self.get_morse_character(char)
return morsecode
ex = "'"+string+"' could not be converted."
ex += "The string must only contain lowercase alphabet characters"
raise Exception(ex)

def encode_word_list(self):
""" Reads the wordlist.txt file and encodes each line into morse code.
The original word and the morse code is stored within the words variables. """
with open("wordlist.txt", 'r') as file:
content = [x.strip() for x in content]

for word in content:
encoded = self.encode_string(word)
self.words.append([word, encoded])

def find_matching_codes(self):
""" Find morse codes that are the same and groups the words under the morse code. """
for word, encoded in self.words:
if encoded in self.encoded_words:
self.encoded_words[encoded].append(word)
else:
self.encoded_words[encoded] = [word]

@staticmethod
def count_longest_dash_run(string):
""" static method that count the number of '-' in a row and retrieved the
number of the longest '-' in a row within the string. """
dashruns = string.split('.')
longest_dash_run = 0
for dashrun in dashruns:
length = len(dashrun)
if length > longest_dash_run:
longest_dash_run = length
return longest_dash_run

@staticmethod
def is_palindrome(word):
""" static method that checks if the word is a palindrome. """
return word == word[::-1]

def challenge(self):
""" prints out the result of the challenge. """
print("Challenge Result:")
print(self.encode_string("sos"))
print(self.encode_string("daily"))
print(self.encode_string("programmer"))
print(self.encode_string("bits"))
print(self.encode_string("three"))

def optional_challenge_one(self):
""" prints out the result of the first optional challenge. """
print("Optional Challenge One Result:")
for key in self.encoded_words:
value = self.encoded_words[key]
if len(value) >= 13:
print(key+":"+str(value))

def optional_challenge_two(self):
""" prints out the result of the second optional challenge. """
print("Optional Challenge Two Result:")
for key in self.encoded_words:
value = self.encoded_words[key]
if self.count_longest_dash_run(key) == 15:
print(key+":"+str(value))

def optional_challenge_three(self):
""" prints out the result of the third optional challenge. """
print("Optional Challenge Three Result:")
selected = {}
for pair in self.words:
word = pair[0]
encoded = pair[1]
if len(word) == 21:
selected[encoded] = word

for key in selected:
if key.count('.') == key.count('-'):
value = selected[key]
print(key+":"+str(value))

def optional_challenge_four(self):
""" prints out the result of the fourth optional challenge. """
print("Optional Challenge Four Result:")
for pair in self.words:
key = pair[0]
value = pair[1]
if len(key) == 13:
if self.is_palindrome(value):
print(str(value)+":"+key)

if __name__ == "__main__":
CODER = Morsecoder()
CODER.challenge()
CODER.optional_challenge_one()
CODER.optional_challenge_two()
CODER.optional_challenge_three()
CODER.optional_challenge_four()


• Please always include an abbreviated description of the programming challenge in your question in case external links become invalid. I've done so for this post, just keep in mind to include for future questions. – AlexV Mar 2 '20 at 12:18
• @AlexV Thank you. I will do so in future posts. – Meerfall the dewott Mar 2 '20 at 13:05

1. The line-too-long can be fixed by wrapping the long string. There are at least two obvious ways to do it. First, use a triple quoted string. This lets the string include span multiple lines. A possible problem is that the string now includes the extra spaces at the beginning of the second and third lines. However, those will get removed by the call to split().

morse_alphabet = """.- -... -.-. -.. . ..-. --. .... .. .---
-.- .-.. -- -. --- .--. --.- .-. ... -
..- ...- .-- -..- -.-- --.."""


A second possibility is to take advantage of the interpreter concatenating adjacent strings.

morse_alphabet = (".- -... -.-. -.. . ..-. --. .... .. .---"
" -.- .-.. -- -. --- .--. --.- .-. ... -"
" ..- ...- .-- -..- -.-- --..")


In both cases, morse_alphabet = morse_alphabet.split() will split the string on whitespace characters and return a list of the morse codes.

2. A comparison is needed somewhere, to find the longest length of '-'s. It can be explicit, as in your code. Or implicit, as in the built in max() function. The code can be simplified by using a generator expression to calculate the lengths of the dash runs.

def count_longest_dash_run(code):
return max(len(dashrun) for dashrun in code.split('.'))

3. For a small program like this, pulling out the print(...) into a separate function is probably overkill. But using either an f-string or str.format() or would be better than concatenating strings using '+'.

print(f"{key}: {value}")


or

print("{}: {}".format(key, value))


I find type hints to needlessly clutter the code for small programs like this. For larger projects they can help document expected types.

Docstrings should tell someone how to use the thing being documented (class, function, module, method, etc.). They should be complete and correct. Here, the docstring for encode_string() says "Encodes the entire string into the morse code.", which is not correct. It encodes the string into morse code, without spaces between the letters.

The docstring for challenge() says "prints out the result of the challenge." But no one will remember what the challenge was next week/month/year. Similarly for the optional challenges.

Other things.

You can iterate over a list of 2-tuples like so:

for key, value in self.words:
...


The encoding could be done using str.maktrans() and str.translate().

Using collections.defaultdict lets you skip doing a special case for when a key isn't in the dictionary. For example, if __init__ had self.encoded_words = defaultdict(list), then

def find_matching_codes(self):
for word, encoded in self.words:
self.encoded_words[encoded].append(word)

• Thank you for your help!. Regarding 5, I've seen '@Classmethod' and similar thing to the '@staticmethod', i'm just wondering if I should have included them or not or if they are even necessary to the code at all. – Meerfall the dewott Mar 4 '20 at 15:15
• @Meerfallthedewott, @classmethod is mostly useful for things like alternate constructors (e.g., SomeClass.from_string() or SomeClass.from_json()) that provide a way to create a class instance other than __init__(). The answers to this question discuss the "why" of staticmethods. – RootTwo Mar 4 '20 at 18:50