Advanced Morse Code Decoder in Python | by Kieran Tan Kah Wang | Towards Data Science

In one of my previous post, I’ve designed a simple Morse Code Decoder in Python which is capable of accepting user inputs and outputting them in their original alphanumerical form. One of the limitations of the decoder is that it does not allow the user to input sentences. Remember that we have chosen to represent each alphabet or number using a series of ‘0’ and ‘1’, where ‘0’ represents a dot, and ‘1’ represents a dash. Each alphabet or number is then separated by a ‘*’ in our decoder, as shown in the screenshot below.
In this post, we will improve our simple Morse Code Decoder to be able to decipher sentences as well. Furthermore, we can implement checks in the decoder to inform us of the frequencies in which each alphabet/number, word, or sentence type have been decoded.
One major difference between this decoder and the previous one is that we will be using Python class to build our decoder and the respective analysis checks. A simple explanation about Python class is that it defines a set of instance variables (data values to be represented for each object) and a set of methods (operations) that can be applied to the objects.
We will build a class called Decoder and it will contain several methods, with the first being __init__(self). This is the essential method for object creation and it usually initialize the values of the instance variables of each object. Remember that we chose to use a dictionary structure to store our Morse Code representation in Python, where each alphabet or number is represented by a series of ‘0’s and ‘1’s. As we are adding sentences analysis checks too, we have also added three punctuation marks “ . ” , “ , ” and “ ? ”.
class Decoder:    def __init__(self):        self.the_dict = dict(zip(('A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9','.',',','?'),('01','1000','1010','100','0','0010','110','0000','00','0111','101','0100','11','10','111','0110','1101','010','000','1','001','0001','011','1001','1011','1100','11111','01111','00111','00011','00001','00000','10000','11000','11100','11110','010101','110011','001100')))        
The next essential method in a class creation is returning/printing the output. With just the __init__ (self) method, we cannot display any results if we assign a variable to this class. We will add a __str__ (self) method which works the same way as print(variable), but allows us to customize the way the results will be displayed.
def __str__ (self):        dict_str = ""        for key,value in self.the_dict.items():            dict_str += "Character: " + str(key) + " ->" + " Code: " + str(value) + "\n"        return dict_str
Once we have defined the above two methods, we can try displaying the dictionary structure we have defined earlier by assigning a variable name to the class and printing the result. Notice that my_dict now is an object of class Decoder.
The last method we will define in this class is the decoding of the Morse Code itself called decode (self, morse_code_sequence). Note that in this method, other than defining self, we added another morse_code_sequence. This means that to call this method, we will have to pass a variable through this method (i.e. the Morse Code sequence) which works similarly to calling a function and passing a value through it. We will also include the error checks we have implemented previously in our Simple Morse Code Decoder mainly:
User can only enter 0, 1 and *User can only enter Morse Code Representation that has been pre-defined (e.g. 0000000 is not a valid representation)Morse Code sequences must end with a punctuation mark (i.e. full stop, comma, or question mark)
User can only enter 0, 1 and *
User can only enter Morse Code Representation that has been pre-defined (e.g. 0000000 is not a valid representation)
Morse Code sequences must end with a punctuation mark (i.e. full stop, comma, or question mark)
def decode (self,morse_code_sequence):        character = list(self.the_dict.keys())        code = list(self.the_dict.values())        reverse_dict = dict(zip(code,character))              # Error check 1, checking for invalid characters (i.e. other than 0,1,*)        i=0        while i!=len(morse_code_sequence):            if morse_code_sequence[i]=="0" or morse_code_sequence[i]=="1" or morse_code_sequence[i]=="*":                i += 1            else:                return "Invalid characters in morse code sequence, it should only consists of 0, 1 and *"                break        # Proceed for error check 2 once the morse_code_sequence does not contain invalid characters        if i == len(morse_code_sequence):            splitted_morse_code_sequence = morse_code_sequence.split("***")             morse_code_word = []            for j in range(0,len(splitted_morse_code_sequence)):                morse_code_word.append(splitted_morse_code_sequence[j].split("*")) #[['001','100','110'], ['90','9','9'], ['abc','17']]             morse_code_char=[]            # Initialise a result list as the format of morse_code_word            result = morse_code_word            # split into list containing all character's morse code            for k in range(0,len(morse_code_word)):                for l in range (0,len(morse_code_word[k])):                    morse_code_char.append(morse_code_word[k][l])            # Check 2, terminate with punctuation marks i.e 010101, 110011, 001100            if morse_code_char[len(morse_code_char)-1]=="010101" or morse_code_char[len(morse_code_char)-1]=="110011" or morse_code_char[len(morse_code_char)-1]=="001100" :                # Check 3, check if character's morse code representation is valid as per the dictionary                for k in range(0,len(morse_code_word)):                    for l in range (0,len(morse_code_word[k])):                        if morse_code_word[k][l] in reverse_dict.keys():                            result[k][l]=reverse_dict[morse_code_word[k][l]]                        else:                            return "Invalid morse code representation"            else:                return "Invalid morse code sequence input, morse code sequence should end with punctuation characters"                        # Create an empty list to append result in and convert into readable string format output            printed_result = []            for i in range(0,len(result)):                for j in range(0,len(result[i])):                    printed_result.append(result[i][j])                 printed_result.append(" ")                        return ''.join(printed_result)
The next class we will build is the Character Analyser class, which will tell the user the frequencies of each character (i.e. alphabet or number) being decoded. Similar to our Decoder class, the two essential methods __init__(self) and __str__(self) will be defined.
The third method analyse_characters (self, decoded_sequence) will be created to analyze the characters, with the decoded sequence being passed through this method. How we will count the characters is that for each character in the decoded sequence, we will add them into an empty dictionary. Looping through each character, we will aggregate the counts of the character and display them as a dictionary structure in the end, which we define in the __str__(self) method.
class CharacterAnalyser:    def __init__ (self):        self.the_dict_char = {}          def __str__ (self):        dict_char_str = ""        for key,value in self.the_dict_char.items():            dict_char_str += str(key) + " has been decoded " + str(value) + " time(s) " + "\n"        return dict_char_str        def analyse_characters (self, decoded_sequence):        for each in decoded_sequence:            if each != "?" and each != "." and each != "," and each != " ":                if each not in self.the_dict_char.keys():                    self.the_dict_char[each] = 1                else:                    self.the_dict_char[each] += 1                            return self.the_dict_char
In our next class, we will implement the Word Analyser which will count the number of times certain words are being decoded in the Morse Code sequences. After defining the two essential methods __init__(self) and __str__(self), we will define the third method analyse_words (self, decoded_sequence) that will analyze the word frequencies. The concept is similar to Character Analyser Class whereby we will loop through each word in the decoded sequence, and aggregate the counts in a dictionary.
class WordAnalyser:    def __init__ (self):        self.the_dict_word = {}          def __str__ (self):        dict_word_str = ""        for key,value in self.the_dict_word.items():            dict_word_str += str(key) + " has been decoded " + str(value) + " time(s) " +"\n"        return dict_word_str        def analyse_words (self, decoded_sequence):        decoded_sequence_word=(decoded_sequence.strip()).split(" ")        for each in decoded_sequence_word:            if each != "?" and each != "." and each != ",":                if each not in self.the_dict_word.keys():                    self.the_dict_word[each] = 1                else:                    self.the_dict_word[each] += 1                   return self.the_dict_word
The last analyzer class we will build is a Sentence Analyser Class. As we allow the users to key in three punctuation marks of a full stop, comma, and question mark, we can analyze how many times each sentence type is being decoded as per the following:
1. A sentence ending with ‘ ? ’ — Questions
2. A sentence ending with ‘ . ’ — Complete Sentences
3. A sentence ending with ‘ , ’ — Clauses
After defining the essential two classes __init__(self) and __str__(self), the way we will build our third method analyse_sentences (self, decoded_sequence) is that we will split the sequences into individual words/punctuation marks and loop through each of them. If a question mark is found, we will add a count to ‘Questions’ and so forth.
class SentenceAnalyser:    def __init__ (self):        self.the_dict_sentence = dict(zip(("Clauses","Complete Sentences","Questions"),(0,0,0)))          def __str__ (self):        dict_sentence_str = ""        for key,value in self.the_dict_sentence.items():            dict_sentence_str += str(key) + " has been decoded " + str(value) + " times(s) " + "\n"        return dict_sentence_str        def analyse_sentences (self, decoded_sequence):        decoded_sequence_sentence = decoded_sequence.split(" ")        for each in decoded_sequence_sentence:            if each == ",":                self.the_dict_sentence["Clauses"] += 1            elif each == ".":                self.the_dict_sentence["Complete Sentences"] += 1            elif each == "?":                self.the_dict_sentence["Questions"] += 1                        return self.the_dict_sentence
In this last section, we will put all the above 4 classes together and create a program that provides the user the capability to enter Morse Code sequences and choosing the type of analysis he/she is interested in. The way we put all the classes together is using the main() method which is the starting point of any program. Below is the whole code which I will walk you guys through.
# import all the 4 classesfrom decoder_29442826 import Decoderfrom character_29442826 import CharacterAnalyserfrom word_29442826 import WordAnalyserfrom sentence_29442826 import SentenceAnalyser        def main():    # set 4 variables to each of the 4 classes    my_dict_1 = Decoder()    my_dict_2 = CharacterAnalyser()    my_dict_3 = WordAnalyser()    my_dict_4 = SentenceAnalyser()    # initialise empty list to store user input    user_input_list = []    # print the structure of morse code representation for user's reference    print(str(my_dict_1))    # asking user to input morse code sequences for decoding    while True:        user_input_morse = input ("Enter sequences of Morse Code to be decoded based on representation shown, leave blank to stop input: ")        # Proceed for error checking of user input if it's not empty        if len(user_input_morse)!=0:            # Error check 1, checking for invalid characters other than 0, 1, *            if my_dict_1.decode(user_input_morse)=="Invalid characters in morse code sequence, it should only consists of 0, 1 and *":                print ("Invalid characters in morse code sequence, it should only consists of 0, 1 and *" + "\n")            # Error check 2, checking for minimum requirement of one set of ***            elif "***" not in user_input_morse:                print ("Error input, the Morse Code sequence should have at least one set of ***" + "\n")            # Error check 3, checking for punctuation at the end of user input, i.e. 010101, 110011, 001100            elif my_dict_1.decode(user_input_morse)=="Invalid morse code sequence input, morse code sequence should end with punctuation characters":                print("Invalid morse code sequence input, morse code sequence should end with punctuation characters" + "\n")            # Error check 4, checking for invalid morse code representation as per the pre-defined morse code dictionary            elif my_dict_1.decode(user_input_morse)=="Invalid morse code representation":                print("Invalid morse code representation" + "\n")            # add user input into list for decoding if no errors found            else:                user_input_list.append(user_input_morse)        # Stop asking for input once user leaved blank        else:            print("You have chosen to stop inputting morse code sequences" + "\n")            break        # empty string to store decoded sequences    overall_decoded_sequence=""    # decoding the morse code sequences in user_input_list    for each in user_input_list:        overall_decoded_sequence += my_dict_1.decode(each)    # character analysis of all the decoded morse code sequence       my_dict_2.analyse_characters(overall_decoded_sequence)    # word analysis of all the decoded morse code sequence      my_dict_3.analyse_words(overall_decoded_sequence)    # sentence analysis of all the decoded morse code sequence      my_dict_4.analyse_sentences(overall_decoded_sequence)        if len(overall_decoded_sequence)!=0:    # asking user to choose level of analysis from character, word, sentence or all        while True:            user_input_level = input ("Please choose the level of analysis from character, word, sentence or all, leave blank to quit:")            # if character analysis chosen, return result of character analysis only            if user_input_level == "character":                print("Decoded Morse Code Sequence(s):")                for each in user_input_list:                    print(str(each) + " has been decoded to: " + my_dict_1.decode(each))                print("\n" + "Characters Analysis:" + "\n" + str(my_dict_2))            # if word analysis chosen, return result of word analysis only            elif user_input_level == "word":                print("Decoded Morse Code Sequence(s):")                for each in user_input_list:                    print(str(each) + " has been decoded to: " + my_dict_1.decode(each))                print("\n" + "Words Analysis:" + "\n" + str(my_dict_3))            # if sentence analysis chosen, return result of sentence analysis only            elif user_input_level == "sentence":                print("Decoded Morse Code Sequence(s):")                for each in user_input_list:                    print(str(each) + " has been decoded to: " + my_dict_1.decode(each))                print("\n" + "Sentence Analysis:" + "\n" + str(my_dict_4))            # if all has been chose, return result of character, word and sentence analysis            elif user_input_level == "all":                print("Decoded Morse Code Sequence(s):")                for each in user_input_list:                    print(str(each) + " has been decoded to: " + my_dict_1.decode(each))                print("\n" + "Characters Analysis:" + "\n" + str(my_dict_2))                print("Words Analysis:" + "\n" + str(my_dict_3))                print("Sentence Analysis:" + "\n" + str(my_dict_4))            # if user leaved blank, terminate the whole program            elif user_input_level == "":                print("You have chosen to terminate the whole program")                break            # prompt the user for level of analysis again if invalid input has been found            else:                print("Invalid input, choose the level of analysis from character, word, sentence or all, leave blank to quit")    else:        print("No morse code sequences were entered")if __name__ == "__main__":    main()
The way this program will work is that once the user runs this program, the Morse Code representation will first be displayed (using the first Decoder class). Users will be prompted to enter a Morse Code sequence and can continue inputting for decoding till they decided to stop inputting by leaving a blank.
If the user chose to quit inputting Morse Code Sequences and did not input any Morse Code Sequence or all Morse Code Sequences that were entered were invalid, the program will terminate with the message “No morse code sequences were entered”.
Once the user has entered a Morse Code sequence or a series of them and keyed in a blank to indicate he/she wants to stop inputting, the program will then prompt the user to choose the level of analysis (i.e. Character, Word, Sentence or All).
Character Analysis will call upon the character_analyser class and return the characters found in the sequences entered by users along with their decoded frequencies.
Word Analysis will call upon word_analyser class and return the words found in the sequences entered by users along with their decoded frequencies.
Sentence Analysis will call upon sentence_analyser and return the counts of the number of times Questions, Clauses & Complete Sentences have been decoded based on ‘ ? ’, ‘ , ’ and ‘ . ’ respectively.
Users can also choose “all” and all three analyses will be displayed at once.
Once the user is done with the analysis, he/she can terminate the program by inputting a blank input and the program will exit itself.
This article has described the process of building a more advanced Morse Code Decoder compared to the previous one which was only capable of accepting user inputs and outputting them in their original alphanumerical form. The new decoder allows the users to decode sentences and getting aggregated analysis of character/word/sentence being decoded. I hope you guys enjoy this article and this is more advanced in terms of coding proficiency as we were defining our Python classes in this decoder. Nonetheless, have fun improving your Morse Code Decoder!