From 7e90f6a5be11f211aa48f120b28e1c96f302e520 Mon Sep 17 00:00:00 2001 From: Freddy Pringle Date: Mon, 7 Dec 2020 21:06:40 +0100 Subject: [PATCH 1/5] Added solution for Project Euler problem 59 --- project_euler/problem_059/__init__.py | 0 project_euler/problem_059/p059_cipher.txt | 1 + project_euler/problem_059/sol1.py | 121 ++++++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 project_euler/problem_059/__init__.py create mode 100644 project_euler/problem_059/p059_cipher.txt create mode 100644 project_euler/problem_059/sol1.py diff --git a/project_euler/problem_059/__init__.py b/project_euler/problem_059/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_059/p059_cipher.txt b/project_euler/problem_059/p059_cipher.txt new file mode 100644 index 000000000000..b3b3247298d1 --- /dev/null +++ b/project_euler/problem_059/p059_cipher.txt @@ -0,0 +1 @@ +36,22,80,0,0,4,23,25,19,17,88,4,4,19,21,11,88,22,23,23,29,69,12,24,0,88,25,11,12,2,10,28,5,6,12,25,10,22,80,10,30,80,10,22,21,69,23,22,69,61,5,9,29,2,66,11,80,8,23,3,17,88,19,0,20,21,7,10,17,17,29,20,69,8,17,21,29,2,22,84,80,71,60,21,69,11,5,8,21,25,22,88,3,0,10,25,0,10,5,8,88,2,0,27,25,21,10,31,6,25,2,16,21,82,69,35,63,11,88,4,13,29,80,22,13,29,22,88,31,3,88,3,0,10,25,0,11,80,10,30,80,23,29,19,12,8,2,10,27,17,9,11,45,95,88,57,69,16,17,19,29,80,23,29,19,0,22,4,9,1,80,3,23,5,11,28,92,69,9,5,12,12,21,69,13,30,0,0,0,0,27,4,0,28,28,28,84,80,4,22,80,0,20,21,2,25,30,17,88,21,29,8,2,0,11,3,12,23,30,69,30,31,23,88,4,13,29,80,0,22,4,12,10,21,69,11,5,8,88,31,3,88,4,13,17,3,69,11,21,23,17,21,22,88,65,69,83,80,84,87,68,69,83,80,84,87,73,69,83,80,84,87,65,83,88,91,69,29,4,6,86,92,69,15,24,12,27,24,69,28,21,21,29,30,1,11,80,10,22,80,17,16,21,69,9,5,4,28,2,4,12,5,23,29,80,10,30,80,17,16,21,69,27,25,23,27,28,0,84,80,22,23,80,17,16,17,17,88,25,3,88,4,13,29,80,17,10,5,0,88,3,16,21,80,10,30,80,17,16,25,22,88,3,0,10,25,0,11,80,12,11,80,10,26,4,4,17,30,0,28,92,69,30,2,10,21,80,12,12,80,4,12,80,10,22,19,0,88,4,13,29,80,20,13,17,1,10,17,17,13,2,0,88,31,3,88,4,13,29,80,6,17,2,6,20,21,69,30,31,9,20,31,18,11,94,69,54,17,8,29,28,28,84,80,44,88,24,4,14,21,69,30,31,16,22,20,69,12,24,4,12,80,17,16,21,69,11,5,8,88,31,3,88,4,13,17,3,69,11,21,23,17,21,22,88,25,22,88,17,69,11,25,29,12,24,69,8,17,23,12,80,10,30,80,17,16,21,69,11,1,16,25,2,0,88,31,3,88,4,13,29,80,21,29,2,12,21,21,17,29,2,69,23,22,69,12,24,0,88,19,12,10,19,9,29,80,18,16,31,22,29,80,1,17,17,8,29,4,0,10,80,12,11,80,84,67,80,10,10,80,7,1,80,21,13,4,17,17,30,2,88,4,13,29,80,22,13,29,69,23,22,69,12,24,12,11,80,22,29,2,12,29,3,69,29,1,16,25,28,69,12,31,69,11,92,69,17,4,69,16,17,22,88,4,13,29,80,23,25,4,12,23,80,22,9,2,17,80,70,76,88,29,16,20,4,12,8,28,12,29,20,69,26,9,69,11,80,17,23,80,84,88,31,3,88,4,13,29,80,21,29,2,12,21,21,17,29,2,69,12,31,69,12,24,0,88,20,12,25,29,0,12,21,23,86,80,44,88,7,12,20,28,69,11,31,10,22,80,22,16,31,18,88,4,13,25,4,69,12,24,0,88,3,16,21,80,10,30,80,17,16,25,22,88,3,0,10,25,0,11,80,17,23,80,7,29,80,4,8,0,23,23,8,12,21,17,17,29,28,28,88,65,75,78,68,81,65,67,81,72,70,83,64,68,87,74,70,81,75,70,81,67,80,4,22,20,69,30,2,10,21,80,8,13,28,17,17,0,9,1,25,11,31,80,17,16,25,22,88,30,16,21,18,0,10,80,7,1,80,22,17,8,73,88,17,11,28,80,17,16,21,11,88,4,4,19,25,11,31,80,17,16,21,69,11,1,16,25,2,0,88,2,10,23,4,73,88,4,13,29,80,11,13,29,7,29,2,69,75,94,84,76,65,80,65,66,83,77,67,80,64,73,82,65,67,87,75,72,69,17,3,69,17,30,1,29,21,1,88,0,23,23,20,16,27,21,1,84,80,18,16,25,6,16,80,0,0,0,23,29,3,22,29,3,69,12,24,0,88,0,0,10,25,8,29,4,0,10,80,10,30,80,4,88,19,12,10,19,9,29,80,18,16,31,22,29,80,1,17,17,8,29,4,0,10,80,12,11,80,84,86,80,35,23,28,9,23,7,12,22,23,69,25,23,4,17,30,69,12,24,0,88,3,4,21,21,69,11,4,0,8,3,69,26,9,69,15,24,12,27,24,69,49,80,13,25,20,69,25,2,23,17,6,0,28,80,4,12,80,17,16,25,22,88,3,16,21,92,69,49,80,13,25,6,0,88,20,12,11,19,10,14,21,23,29,20,69,12,24,4,12,80,17,16,21,69,11,5,8,88,31,3,88,4,13,29,80,22,29,2,12,29,3,69,73,80,78,88,65,74,73,70,69,83,80,84,87,72,84,88,91,69,73,95,87,77,70,69,83,80,84,87,70,87,77,80,78,88,21,17,27,94,69,25,28,22,23,80,1,29,0,0,22,20,22,88,31,11,88,4,13,29,80,20,13,17,1,10,17,17,13,2,0,88,31,3,88,4,13,29,80,6,17,2,6,20,21,75,88,62,4,21,21,9,1,92,69,12,24,0,88,3,16,21,80,10,30,80,17,16,25,22,88,29,16,20,4,12,8,28,12,29,20,69,26,9,69,65,64,69,31,25,19,29,3,69,12,24,0,88,18,12,9,5,4,28,2,4,12,21,69,80,22,10,13,2,17,16,80,21,23,7,0,10,89,69,23,22,69,12,24,0,88,19,12,10,19,16,21,22,0,10,21,11,27,21,69,23,22,69,12,24,0,88,0,0,10,25,8,29,4,0,10,80,10,30,80,4,88,19,12,10,19,9,29,80,18,16,31,22,29,80,1,17,17,8,29,4,0,10,80,12,11,80,84,86,80,36,22,20,69,26,9,69,11,25,8,17,28,4,10,80,23,29,17,22,23,30,12,22,23,69,49,80,13,25,6,0,88,28,12,19,21,18,17,3,0,88,18,0,29,30,69,25,18,9,29,80,17,23,80,1,29,4,0,10,29,12,22,21,69,12,24,0,88,3,16,21,3,69,23,22,69,12,24,0,88,3,16,26,3,0,9,5,0,22,4,69,11,21,23,17,21,22,88,25,11,88,7,13,17,19,13,88,4,13,29,80,0,0,0,10,22,21,11,12,3,69,25,2,0,88,21,19,29,30,69,22,5,8,26,21,23,11,94 \ No newline at end of file diff --git a/project_euler/problem_059/sol1.py b/project_euler/problem_059/sol1.py new file mode 100644 index 000000000000..e301ed2766d7 --- /dev/null +++ b/project_euler/problem_059/sol1.py @@ -0,0 +1,121 @@ +""" +Each character on a computer is assigned a unique code and the preferred standard is +ASCII (American Standard Code for Information Interchange). +For example, uppercase A = 65, asterisk (*) = 42, and lowercase k = 107. + +A modern encryption method is to take a text file, convert the bytes to ASCII, then +XOR each byte with a given value, taken from a secret key. The advantage with the +XOR function is that using the same encryption key on the cipher text, restores +the plain text; for example, 65 XOR 42 = 107, then 107 XOR 42 = 65. + +For unbreakable encryption, the key is the same length as the plain text message, and +the key is made up of random bytes. The user would keep the encrypted message and the +encryption key in different locations, and without both "halves", it is impossible to +decrypt the message. + +Unfortunately, this method is impractical for most users, so the modified method is +to use a password as a key. If the password is shorter than the message, which is +likely, the key is repeated cyclically throughout the message. The balance for this +method is using a sufficiently long password key for security, but short enough to +be memorable. + +Your task has been made easy, as the encryption key consists of three lower case +characters. Using p059_cipher.txt (right click and 'Save Link/Target As...'), a +file containing the encrypted ASCII codes, and the knowledge that the plain text +must contain common English words, decrypt the message and find the sum of the ASCII +values in the original text. +""" + + +import os.path +import string +from itertools import cycle, product +from typing import List, Optional, Set, Tuple + +VALID_CHARS: str = ( + string.ascii_letters + string.digits + string.punctuation + string.whitespace +) +LOWERCASE_INTS: List[int] = list(map(ord, string.ascii_lowercase)) +VALID_INTS: Set[int] = set(map(ord, VALID_CHARS)) + +COMMON_WORDS = ["the", "be", "to", "of", "and", "in", "that", "have"] + + +def test_three_characters(ciphertext: List[int], key: Tuple[int, ...]) -> Optional[str]: + """ + Given an encrypted message and a possible 3-character key, decrypt the message. + If the decrypted message contains a invalid character, i.e. not an ASCII letter, + a digit, punctuation or whitespace, then we know the key is incorrect, so return + None. + >>> test_three_characters([0, 17, 20, 4, 27], [104, 116, 120]) + 'hello' + >>> test_three_characters([68, 10, 300, 4, 27], [104, 116, 120]) is None + True + """ + decoded: str = "" + keychar: int + cipherchar: int + decodedchar: int + + for keychar, cipherchar in zip(cycle(key), ciphertext): + decodedchar = cipherchar ^ keychar + if decodedchar not in VALID_INTS: + return None + decoded += chr(decodedchar) + + return decoded + + +def filter_valid_chars(ciphertext: List[int]) -> List[str]: + """ + Given an encrypted message, test all 3-character strings to try and find the + key. Return a list of the possible decrypted messages. + """ + possibles: List[str] = [] + for key in product(LOWERCASE_INTS, repeat=3): + encoded = test_three_characters(ciphertext, key) + if encoded is not None: + possibles.append(encoded) + return possibles + + +def filter_common_word(possibles: List[str], common_word: str) -> List[str]: + """ + Given a list of possible decoded messages, narrow down the possibilities + for checking for the presence of a specified common word. Only decoded messages + containing common_word will be returned. + """ + return [possible for possible in possibles if common_word in possible] + + +def solution() -> int: + """ + Test the ciphertext against all possible 3-character keys, then narrow down the + possibilities by filtering using common words until there's only one possible + decoded message. + """ + data: str + script_dir: str = os.path.abspath(os.path.dirname(__file__)) + cipher_file: str = os.path.join(script_dir, "p059_cipher.txt") + ciphertext: List[int] + possibles: List[str] + common_word: str + decoded_text: str + + with open(cipher_file, "r") as file: + data = file.read() + + ciphertext = list(map(int, data.strip().split(","))) + + possibles = filter_valid_chars(ciphertext) + for common_word in COMMON_WORDS: + possibles = filter_common_word(possibles, common_word) + if len(possibles) == 1: + break + + decoded_text = possibles[0] + return sum(map(ord, decoded_text)) + + +if __name__ == "__main__": + print(f"{solution() = }") From 3f30bb65b282e4afd66411d45b30638fbc480e9d Mon Sep 17 00:00:00 2001 From: github-actions <${GITHUB_ACTOR}@users.noreply.github.com> Date: Sat, 12 Dec 2020 09:35:15 +0000 Subject: [PATCH 2/5] updating DIRECTORY.md --- DIRECTORY.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/DIRECTORY.md b/DIRECTORY.md index 7eec7e0811dd..929a986b0f3b 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -345,6 +345,7 @@ ## Linear Algebra * Src + * [Conjugate Gradient](https://github.com/TheAlgorithms/Python/blob/master/linear_algebra/src/conjugate_gradient.py) * [Lib](https://github.com/TheAlgorithms/Python/blob/master/linear_algebra/src/lib.py) * [Polynom For Points](https://github.com/TheAlgorithms/Python/blob/master/linear_algebra/src/polynom_for_points.py) * [Power Iteration](https://github.com/TheAlgorithms/Python/blob/master/linear_algebra/src/power_iteration.py) @@ -695,6 +696,8 @@ * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_057/sol1.py) * Problem 058 * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_058/sol1.py) + * Problem 059 + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_059/sol1.py) * Problem 062 * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_062/sol1.py) * Problem 063 From b5223b2a58b385ce6c264804543e70bc51511345 Mon Sep 17 00:00:00 2001 From: Freddy Pringle Date: Sat, 12 Dec 2020 12:51:44 +0100 Subject: [PATCH 3/5] Formatting, type hints, no more evil map functions --- project_euler/problem_059/sol1.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/project_euler/problem_059/sol1.py b/project_euler/problem_059/sol1.py index e301ed2766d7..d6e076adea45 100644 --- a/project_euler/problem_059/sol1.py +++ b/project_euler/problem_059/sol1.py @@ -27,29 +27,29 @@ """ -import os.path import string from itertools import cycle, product +from pathlib import Path from typing import List, Optional, Set, Tuple VALID_CHARS: str = ( string.ascii_letters + string.digits + string.punctuation + string.whitespace ) -LOWERCASE_INTS: List[int] = list(map(ord, string.ascii_lowercase)) -VALID_INTS: Set[int] = set(map(ord, VALID_CHARS)) +LOWERCASE_INTS: List[int] = [ord(letter) for letter in string.ascii_lowercase] +VALID_INTS: Set[int] = {ord(char) for char in VALID_CHARS} -COMMON_WORDS = ["the", "be", "to", "of", "and", "in", "that", "have"] +COMMON_WORDS: List[str] = ["the", "be", "to", "of", "and", "in", "that", "have"] -def test_three_characters(ciphertext: List[int], key: Tuple[int, ...]) -> Optional[str]: +def try_key(ciphertext: List[int], key: Tuple[int, ...]) -> Optional[str]: """ Given an encrypted message and a possible 3-character key, decrypt the message. If the decrypted message contains a invalid character, i.e. not an ASCII letter, a digit, punctuation or whitespace, then we know the key is incorrect, so return None. - >>> test_three_characters([0, 17, 20, 4, 27], [104, 116, 120]) + >>> try_key([0, 17, 20, 4, 27], (104, 116, 120)) 'hello' - >>> test_three_characters([68, 10, 300, 4, 27], [104, 116, 120]) is None + >>> try_key([68, 10, 300, 4, 27], (104, 116, 120)) is None True """ decoded: str = "" @@ -73,7 +73,7 @@ def filter_valid_chars(ciphertext: List[int]) -> List[str]: """ possibles: List[str] = [] for key in product(LOWERCASE_INTS, repeat=3): - encoded = test_three_characters(ciphertext, key) + encoded = try_key(ciphertext, key) if encoded is not None: possibles.append(encoded) return possibles @@ -94,18 +94,15 @@ def solution() -> int: possibilities by filtering using common words until there's only one possible decoded message. """ - data: str - script_dir: str = os.path.abspath(os.path.dirname(__file__)) - cipher_file: str = os.path.join(script_dir, "p059_cipher.txt") ciphertext: List[int] possibles: List[str] common_word: str decoded_text: str + data: str = ( + Path(__file__).parent.joinpath("p059_cipher.txt").read_text(encoding="utf-8") + ) - with open(cipher_file, "r") as file: - data = file.read() - - ciphertext = list(map(int, data.strip().split(","))) + ciphertext = [int(number) for number in data.strip().split(",")] possibles = filter_valid_chars(ciphertext) for common_word in COMMON_WORDS: @@ -114,7 +111,7 @@ def solution() -> int: break decoded_text = possibles[0] - return sum(map(ord, decoded_text)) + return sum([ord(char) for char in decoded_text]) if __name__ == "__main__": From b82271484eeeb940e4eb4df70dc608e785987550 Mon Sep 17 00:00:00 2001 From: Freddy Pringle Date: Sat, 12 Dec 2020 13:14:45 +0100 Subject: [PATCH 4/5] Doctests --- project_euler/problem_059/sol1.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/project_euler/problem_059/sol1.py b/project_euler/problem_059/sol1.py index d6e076adea45..7e8b631f63a0 100644 --- a/project_euler/problem_059/sol1.py +++ b/project_euler/problem_059/sol1.py @@ -70,6 +70,12 @@ def filter_valid_chars(ciphertext: List[int]) -> List[str]: """ Given an encrypted message, test all 3-character strings to try and find the key. Return a list of the possible decrypted messages. + >>> from itertools import cycle + >>> text = "The enemy's gate is down" + >>> key = "end" + >>> encoded = [ord(k) ^ ord(c) for k,c in zip(cycle(key), text)] + >>> text in filter_valid_chars(encoded) + True """ possibles: List[str] = [] for key in product(LOWERCASE_INTS, repeat=3): @@ -84,8 +90,12 @@ def filter_common_word(possibles: List[str], common_word: str) -> List[str]: Given a list of possible decoded messages, narrow down the possibilities for checking for the presence of a specified common word. Only decoded messages containing common_word will be returned. + >>> filter_common_word(['asfla adf', 'I am here', ' !?! #a'], 'am') + ['I am here'] + >>> filter_common_word(['athla amf', 'I am here', ' !?! #a'], 'am') + ['athla amf', 'I am here'] """ - return [possible for possible in possibles if common_word in possible] + return [possible for possible in possibles if common_word in possible.lower()] def solution() -> int: From 32a2d4afe66fdc80ae6e6c0d648b90952df69099 Mon Sep 17 00:00:00 2001 From: Freddy Pringle Date: Sun, 13 Dec 2020 10:11:09 +0100 Subject: [PATCH 5/5] Added doctests for Project Euler problem 59 --- project_euler/problem_059/sol1.py | 8 ++++---- project_euler/problem_059/test_cipher.txt | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 project_euler/problem_059/test_cipher.txt diff --git a/project_euler/problem_059/sol1.py b/project_euler/problem_059/sol1.py index 7e8b631f63a0..1f55029b2613 100644 --- a/project_euler/problem_059/sol1.py +++ b/project_euler/problem_059/sol1.py @@ -98,19 +98,19 @@ def filter_common_word(possibles: List[str], common_word: str) -> List[str]: return [possible for possible in possibles if common_word in possible.lower()] -def solution() -> int: +def solution(filename: str = "p059_cipher.txt") -> int: """ Test the ciphertext against all possible 3-character keys, then narrow down the possibilities by filtering using common words until there's only one possible decoded message. + >>> solution("test_cipher.txt") + 3000 """ ciphertext: List[int] possibles: List[str] common_word: str decoded_text: str - data: str = ( - Path(__file__).parent.joinpath("p059_cipher.txt").read_text(encoding="utf-8") - ) + data: str = Path(__file__).parent.joinpath(filename).read_text(encoding="utf-8") ciphertext = [int(number) for number in data.strip().split(",")] diff --git a/project_euler/problem_059/test_cipher.txt b/project_euler/problem_059/test_cipher.txt new file mode 100644 index 000000000000..27c53740cc1a --- /dev/null +++ b/project_euler/problem_059/test_cipher.txt @@ -0,0 +1 @@ +63,13,28,75,0,23,14,8,0,76,22,89,12,4,13,14,69,16,24,69,29,4,18,23,69,69,59,14,69,11,14,4,29,18