diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/exercise/ps4b.py b/exercise/ps4b.py index 4e5e101..d8b8794 100644 --- a/exercise/ps4b.py +++ b/exercise/ps4b.py @@ -5,14 +5,15 @@ import string + ### HELPER CODE ### def load_words(file_name): ''' file_name (string): the name of the file containing the list of words to load - + Returns: a list of valid words. Words are strings of lowercase letters. - + Depending on the size of the word list, this function may take a while to finish. ''' @@ -26,6 +27,7 @@ def load_words(file_name): print(" ", len(wordlist), "words loaded.") return wordlist + def is_word(word_list, word): ''' Determines if word is a valid word, ignoring @@ -33,7 +35,7 @@ def is_word(word_list, word): word_list (list): list of words in the dictionary. word (string): a possible word. - + Returns: True if word is in word_list, False otherwise Example: @@ -46,6 +48,7 @@ def is_word(word_list, word): word = word.strip(" !@#$%^&*()-_+={}[]|\:;'<>?,./\"") return word in word_list + def get_story_string(): """ Returns: a story in encrypted text. @@ -55,39 +58,46 @@ def get_story_string(): f.close() return story + ### END HELPER CODE ### WORDLIST_FILENAME = 'words.txt' + class Message(object): def __init__(self, text): ''' Initializes a Message object - + text (string): the message's text a Message object has two attributes: self.message_text (string, determined by input text) self.valid_words (list, determined using helper function load_words) ''' - pass #delete this line and replace with your code here + + self.message_text = text + self.valid_words = load_words(WORDLIST_FILENAME) + # pass #delete this line and replace with your code here def get_message_text(self): ''' Used to safely access self.message_text outside of the class - + Returns: self.message_text ''' - pass #delete this line and replace with your code here + return self.message_text + # pass #delete this line and replace with your code here def get_valid_words(self): ''' Used to safely access a copy of self.valid_words outside of the class. This helps you avoid accidentally mutating class attributes. - + Returns: a COPY of self.valid_words ''' - pass #delete this line and replace with your code here + return self.valid_words.copy() + pass # delete this line and replace with your code here def build_shift_dict(self, shift): ''' @@ -95,35 +105,52 @@ def build_shift_dict(self, shift): The dictionary maps every uppercase and lowercase letter to a character shifted down the alphabet by the input shift. The dictionary should have 52 keys of all the uppercase letters and all the lowercase - letters only. - - shift (integer): the amount by which to shift every letter of the + letters only. + + shift (integer): the amount by which to shift every letter of the alphabet. 0 <= shift < 26 - Returns: a dictionary mapping a letter (string) to - another letter (string). + Returns: a dictionary mapping a letter (string) to + another letter (string). ''' - pass #delete this line and replace with your code here + if shift not in range(0, 27): + raise ValueError("Invalid shift value") + alphabet = "abcdefghijklmnopqrstuvwxyz" + d1 = {ch: alphabet[i + shift - (len(alphabet))] for i, ch in enumerate(alphabet)} + alphabet = alphabet.upper() + d2 = {ch: alphabet[i + shift - (len(alphabet))] for i, ch in enumerate(alphabet)} + return {**d1, **d2} + + pass # delete this line and replace with your code here def apply_shift(self, shift): ''' Applies the Caesar Cipher to self.message_text with the input shift. Creates a new string that is self.message_text shifted down the - alphabet by some number of characters determined by the input shift - + alphabet by some number of characters determined by the input shift + shift (integer): the shift with which to encrypt the message. 0 <= shift < 26 Returns: the message text (string) in which every character is shifted down the alphabet by the input shift ''' - pass #delete this line and replace with your code here + d = self.build_shift_dict(shift) + l = [] + for ch in self.get_message_text(): + if ch in d.keys(): + l.append(d[ch]) + else: + l.append(ch) + return "".join(l) + # pass #delete this line and replace with your code here + class PlaintextMessage(Message): def __init__(self, text, shift): ''' - Initializes a PlaintextMessage object - + Initializes a PlaintextMessage object + text (string): the message's text shift (integer): the shift associated with this message @@ -135,57 +162,84 @@ def __init__(self, text, shift): self.message_text_encrypted (string, created using shift) ''' - pass #delete this line and replace with your code here + self.message_text = text + self.valid_words = load_words(WORDLIST_FILENAME) + self.shift = shift + self.encryption_dict = self.build_shift_dict(shift) + self.message_text_encrypted = self.apply_shift(shift) + # pass #delete this line and replace with your code here def get_shift(self): ''' Used to safely access self.shift outside of the class - + Returns: self.shift ''' - pass #delete this line and replace with your code here + return self.shift + # pass #delete this line and replace with your code here def get_encryption_dict(self): ''' Used to safely access a copy self.encryption_dict outside of the class - + Returns: a COPY of self.encryption_dict ''' - pass #delete this line and replace with your code here + return self.encryption_dict.copy() + # pass #delete this line and replace with your code here def get_message_text_encrypted(self): ''' Used to safely access self.message_text_encrypted outside of the class - + Returns: self.message_text_encrypted ''' - pass #delete this line and replace with your code here + return self.message_text_encrypted + # pass #delete this line and replace with your code here def change_shift(self, shift): ''' - Changes self.shift of the PlaintextMessage and updates other - attributes determined by shift. - + Changes self.shift of the PlaintextMessage and updates other + attributes determined by shift. + shift (integer): the new shift that should be associated with this message. 0 <= shift < 26 Returns: nothing ''' - pass #delete this line and replace with your code here + self.shift = shift + self.encryption_dict = self.build_shift_dict(shift) + self.message_text_encrypted = self.apply_shift(shift) + # pass #delete this line and replace with your code here class CiphertextMessage(Message): def __init__(self, text): ''' Initializes a CiphertextMessage object - + text (string): the message's text a CiphertextMessage object has two attributes: self.message_text (string, determined by input text) self.valid_words (list, determined using helper function load_words) ''' - pass #delete this line and replace with your code here + self.message_text = text + self.valid_words = load_words(WORDLIST_FILENAME) + # pass #delete this line and replace with your code here + + def apply_shift_down(self, shift, msg): + alphabet = "abcdefghijklmnopqrstuvwxyz" + d1 = {ch: alphabet[i - shift] for i, ch in enumerate(alphabet)} + alphabet = alphabet.upper() + d2 = {ch: alphabet[i - shift] for i, ch in enumerate(alphabet)} + d = {**d1, **d2} + l = [] + for ch in msg: + if ch in d.keys(): + l.append(d[ch]) + else: + l.append(ch) + return "".join(l) def decrypt_message(self): ''' @@ -193,32 +247,45 @@ def decrypt_message(self): and find the "best" one. We will define "best" as the shift that creates the maximum number of real words when we use apply_shift(shift) on the message text. If s is the original shift value used to encrypt - the message, then we would expect 26 - s to be the best shift value + the message, then we would expect 26 - s to be the best shift value for decrypting it. - Note: if multiple shifts are equally good such that they all create - the maximum number of valid words, you may choose any of those shifts + Note: if multiple shifts are equally good such that they all create + the maximum number of valid words, you may choose any of those shifts (and their corresponding decrypted messages) to return Returns: a tuple of the best shift value used to decrypt the message and the decrypted message text using that shift value ''' - pass #delete this line and replace with your code here + count = [0] * 27 + for k in range(0, 27): + for word in self.get_message_text().split(" "): + if is_word(self.get_valid_words(), self.apply_shift_down(k, word)): + count[k] += 1 + return self.apply_shift_down(count.index(max(count)), self.get_message_text()) + + # pass #delete this line and replace with your code here + if __name__ == '__main__': + # #Example test case (PlaintextMessage) + # plaintext = PlaintextMessage('hello', 2) + # print('Expected Output: jgnnq') + # print('Actual Output:', plaintext.get_message_text_encrypted()) + # + # #Example test case (CiphertextMessage) + # ciphertext = CiphertextMessage('jgnnq') + # print('Expected Output:', (24, 'hello')) + # print('Actual Output:', ciphertext.decrypt_message()) + + # TODO: WRITE YOUR TEST CASES HERE + + # TODO: best shift value and unencrypted story + msg = load_words("story.txt") + ciphertext = CiphertextMessage(" ".join(msg)) + print(ciphertext.decrypt_message()) + # pass #delete this line and replace with your code here + + + -# #Example test case (PlaintextMessage) -# plaintext = PlaintextMessage('hello', 2) -# print('Expected Output: jgnnq') -# print('Actual Output:', plaintext.get_message_text_encrypted()) -# -# #Example test case (CiphertextMessage) -# ciphertext = CiphertextMessage('jgnnq') -# print('Expected Output:', (24, 'hello')) -# print('Actual Output:', ciphertext.decrypt_message()) - - #TODO: WRITE YOUR TEST CASES HERE - - #TODO: best shift value and unencrypted story - - pass #delete this line and replace with your code here diff --git a/exercise/ps4c.py b/exercise/ps4c.py index 1ceb239..58e0ba8 100644 --- a/exercise/ps4c.py +++ b/exercise/ps4c.py @@ -4,20 +4,23 @@ # Time Spent: x:xx import string -from ps4a import get_permutations +from itertools import permutations + + +# from ps4a import get_permutations ### HELPER CODE ### def load_words(file_name): ''' - file_name (string): the name of the file containing - the list of words to load - + file_name (string): the name of the file containing + the list of words to load + Returns: a list of valid words. Words are strings of lowercase letters. - + Depending on the size of the word list, this function may take a while to finish. ''' - + print("Loading word list from file...") # inFile: file inFile = open(file_name, 'r') @@ -28,6 +31,7 @@ def load_words(file_name): print(" ", len(wordlist), "words loaded.") return wordlist + def is_word(word_list, word): ''' Determines if word is a valid word, ignoring @@ -35,7 +39,7 @@ def is_word(word_list, word): word_list (list): list of words in the dictionary. word (string): a possible word. - + Returns: True if word is in word_list, False otherwise Example: @@ -59,68 +63,83 @@ def is_word(word_list, word): CONSONANTS_LOWER = 'bcdfghjklmnpqrstvwxyz' CONSONANTS_UPPER = 'BCDFGHJKLMNPQRSTVWXYZ' + class SubMessage(object): def __init__(self, text): ''' Initializes a SubMessage object - + text (string): the message's text A SubMessage object has two attributes: self.message_text (string, determined by input text) self.valid_words (list, determined using helper function load_words) ''' - pass #delete this line and replace with your code here - + self.message_text = text + self.valid_words = load_words(WORDLIST_FILENAME) + # pass #delete this line and replace with your code here + def get_message_text(self): ''' Used to safely access self.message_text outside of the class - + Returns: self.message_text ''' - pass #delete this line and replace with your code here + return self.message_text + # pass #delete this line and replace with your code here def get_valid_words(self): ''' Used to safely access a copy of self.valid_words outside of the class. This helps you avoid accidentally mutating class attributes. - + Returns: a COPY of self.valid_words ''' - pass #delete this line and replace with your code here - + return self.valid_words.copy() + # pass #delete this line and replace with your code here + def build_transpose_dict(self, vowels_permutation): ''' vowels_permutation (string): a string containing a permutation of vowels (a, e, i, o, u) - + Creates a dictionary that can be used to apply a cipher to a letter. The dictionary maps every uppercase and lowercase letter to an - uppercase and lowercase letter, respectively. Vowels are shuffled - according to vowels_permutation. The first letter in vowels_permutation + uppercase and lowercase letter, respectively. Vowels are shuffled + according to vowels_permutation. The first letter in vowels_permutation corresponds to a, the second to e, and so on in the order a, e, i, o, u. - The consonants remain the same. The dictionary should have 52 + The consonants remain the same. The dictionary should have 52 keys of all the uppercase letters and all the lowercase letters. Example: When input "eaiuo": Mapping is a->e, e->a, i->i, o->u, u->o and "Hello World!" maps to "Hallu Wurld!" - Returns: a dictionary mapping a letter (string) to - another letter (string). + Returns: a dictionary mapping a letter (string) to + another letter (string). ''' - - pass #delete this line and replace with your code here - + d1 = dict(zip(VOWELS_LOWER, vowels_permutation.lower())) + d2 = dict(zip(VOWELS_UPPER, vowels_permutation.upper())) + return {**d1, **d2} + + # pass #delete this line and replace with your code here + def apply_transpose(self, transpose_dict): ''' transpose_dict (dict): a transpose dictionary - - Returns: an encrypted version of the message text, based + + Returns: an encrypted version of the message text, based on the dictionary ''' - - pass #delete this line and replace with your code here - + l = [] + for ch in self.get_message_text(): + if ch in transpose_dict.keys(): + l.append(transpose_dict[ch]) + else: + l.append(ch) + return "".join(l) + # pass #delete this line and replace with your code here + + class EncryptedSubMessage(SubMessage): def __init__(self, text): ''' @@ -132,31 +151,56 @@ def __init__(self, text): self.message_text (string, determined by input text) self.valid_words (list, determined using helper function load_words) ''' - pass #delete this line and replace with your code here + self.message_text = text + self.valid_words = load_words(WORDLIST_FILENAME) + # pass #delete this line and replace with your code here def decrypt_message(self): ''' - Attempt to decrypt the encrypted message - + Attempt to decrypt the encrypted message + Idea is to go through each permutation of the vowels and test it on the encrypted message. For each permutation, check how many words in the decrypted text are valid English words, and return the decrypted message with the most English words. - - If no good permutations are found (i.e. no permutations result in + + If no good permutations are found (i.e. no permutations result in at least 1 valid word), return the original string. If there are multiple permutations that yield the maximum number of words, return any one of them. - Returns: the best decrypted message - + Returns: the best decrypted message + Hint: use your function from Part 4A ''' - pass #delete this line and replace with your code here - + dic = {k: 0 for k in self.get_permutations(VOWELS_LOWER)} + msg = SubMessage(self.message_text) + for perm in self.get_permutations(VOWELS_LOWER): + for word in msg.apply_transpose(self.build_transpose_dict(perm)).split(" "): + if is_word(self.valid_words, word): + dic[perm] += 1 + msg = SubMessage(self.message_text) + for perm in dic.keys(): + if dic[perm] == max(dic.values()): + return msg.apply_transpose(self.build_transpose_dict(perm)) + return self.message_text + + def get_permutations(self, chars): + first_char = chars[0] + if len(chars) == 1: + return [first_char] + elif len(chars) == 2: + return [first_char + chars[1], chars[1] + first_char] + else: + l = [] + for perm in self.get_permutations(chars[1:]): + for k in range(0, len(perm) + 1): + l.append(perm[:k] + first_char + perm[k:]) + return l + # pass #delete this line and replace with your code here -if __name__ == '__main__': +if __name__ == '__main__': # Example test case message = SubMessage("Hello World!") permutation = "eaiuo" @@ -166,5 +210,14 @@ def decrypt_message(self): print("Actual encryption:", message.apply_transpose(enc_dict)) enc_message = EncryptedSubMessage(message.apply_transpose(enc_dict)) print("Decrypted message:", enc_message.decrypt_message()) - - #TODO: WRITE YOUR TEST CASES HERE + + # TODO: WRITE YOUR TEST CASES HERE + from ps4b import * + + story = CiphertextMessage(" ".join(load_words("story.txt"))).decrypt_message() + for perm in EncryptedSubMessage("").get_permutations(VOWELS_LOWER): + smsg = SubMessage(story) + encrypted = smsg.apply_transpose(smsg.build_transpose_dict(perm)) + decrypted = EncryptedSubMessage(encrypted).decrypt_message() + print(decrypted) +