Skip to content

Commit 52a987e

Browse files
FEROS01pre-commit-ci[bot]cclauss
authored
Add docstrings and doctests and fix a bug ciphers/trifid_cipher.py (#10716)
* Added docstrings,doctests and fixed a bug * Added docstrings,doctests and fixed a bug * Added docstrings,doctests and fixed a bug * Added docstrings and doctests with a bug fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Added docstrings and doctests with a bug fix * Update ciphers/trifid_cipher.py Co-authored-by: Christian Clauss <cclauss@me.com> * Update ciphers/trifid_cipher.py Co-authored-by: Christian Clauss <cclauss@me.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Docstrings edit * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update trifid_cipher.py * Update pyproject.toml --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Christian Clauss <cclauss@me.com>
1 parent 5799376 commit 52a987e

File tree

2 files changed

+134
-59
lines changed

2 files changed

+134
-59
lines changed

Diff for: ciphers/trifid_cipher.py

+133-58
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,35 @@
1-
# https://en.wikipedia.org/wiki/Trifid_cipher
1+
"""
2+
The trifid cipher uses a table to fractionate each plaintext letter into a trigram,
3+
mixes the constituents of the trigrams, and then applies the table in reverse to turn
4+
these mixed trigrams into ciphertext letters.
5+
6+
https://en.wikipedia.org/wiki/Trifid_cipher
7+
"""
8+
29
from __future__ import annotations
310

11+
# fmt: off
12+
TEST_CHARACTER_TO_NUMBER = {
13+
"A": "111", "B": "112", "C": "113", "D": "121", "E": "122", "F": "123", "G": "131",
14+
"H": "132", "I": "133", "J": "211", "K": "212", "L": "213", "M": "221", "N": "222",
15+
"O": "223", "P": "231", "Q": "232", "R": "233", "S": "311", "T": "312", "U": "313",
16+
"V": "321", "W": "322", "X": "323", "Y": "331", "Z": "332", "+": "333",
17+
}
18+
# fmt: off
419

5-
def __encrypt_part(message_part: str, character_to_number: dict[str, str]) -> str:
6-
one, two, three = "", "", ""
7-
tmp = []
20+
TEST_NUMBER_TO_CHARACTER = {val: key for key, val in TEST_CHARACTER_TO_NUMBER.items()}
821

9-
for character in message_part:
10-
tmp.append(character_to_number[character])
1122

12-
for each in tmp:
23+
def __encrypt_part(message_part: str, character_to_number: dict[str, str]) -> str:
24+
"""
25+
Arrange the triagram value of each letter of 'message_part' vertically and join
26+
them horizontally.
27+
28+
>>> __encrypt_part('ASK', TEST_CHARACTER_TO_NUMBER)
29+
'132111112'
30+
"""
31+
one, two, three = "", "", ""
32+
for each in (character_to_number[character] for character in message_part):
1333
one += each[0]
1434
two += each[1]
1535
three += each[2]
@@ -20,12 +40,16 @@ def __encrypt_part(message_part: str, character_to_number: dict[str, str]) -> st
2040
def __decrypt_part(
2141
message_part: str, character_to_number: dict[str, str]
2242
) -> tuple[str, str, str]:
23-
tmp, this_part = "", ""
43+
"""
44+
Convert each letter of the input string into their respective trigram values, join
45+
them and split them into three equal groups of strings which are returned.
46+
47+
>>> __decrypt_part('ABCDE', TEST_CHARACTER_TO_NUMBER)
48+
('11111', '21131', '21122')
49+
"""
50+
this_part = "".join(character_to_number[character] for character in message_part)
2451
result = []
25-
26-
for character in message_part:
27-
this_part += character_to_number[character]
28-
52+
tmp = ""
2953
for digit in this_part:
3054
tmp += digit
3155
if len(tmp) == len(message_part):
@@ -38,97 +62,148 @@ def __decrypt_part(
3862
def __prepare(
3963
message: str, alphabet: str
4064
) -> tuple[str, str, dict[str, str], dict[str, str]]:
65+
"""
66+
A helper function that generates the triagrams and assigns each letter of the
67+
alphabet to its corresponding triagram and stores this in a dictionary
68+
("character_to_number" and "number_to_character") after confirming if the
69+
alphabet's length is 27.
70+
71+
>>> test = __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVwxYZ+')
72+
>>> expected = ('IAMABOY','ABCDEFGHIJKLMNOPQRSTUVWXYZ+',
73+
... TEST_CHARACTER_TO_NUMBER, TEST_NUMBER_TO_CHARACTER)
74+
>>> test == expected
75+
True
76+
77+
Testing with incomplete alphabet
78+
>>> __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVw')
79+
Traceback (most recent call last):
80+
...
81+
KeyError: 'Length of alphabet has to be 27.'
82+
83+
Testing with extra long alphabets
84+
>>> __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVwxyzzwwtyyujjgfd')
85+
Traceback (most recent call last):
86+
...
87+
KeyError: 'Length of alphabet has to be 27.'
88+
89+
Testing with punctuations that are not in the given alphabet
90+
>>> __prepare('am i a boy?','abCdeFghijkLmnopqrStuVwxYZ+')
91+
Traceback (most recent call last):
92+
...
93+
ValueError: Each message character has to be included in alphabet!
94+
95+
Testing with numbers
96+
>>> __prepare(500,'abCdeFghijkLmnopqrStuVwxYZ+')
97+
Traceback (most recent call last):
98+
...
99+
AttributeError: 'int' object has no attribute 'replace'
100+
"""
41101
# Validate message and alphabet, set to upper and remove spaces
42102
alphabet = alphabet.replace(" ", "").upper()
43103
message = message.replace(" ", "").upper()
44104

45105
# Check length and characters
46106
if len(alphabet) != 27:
47107
raise KeyError("Length of alphabet has to be 27.")
48-
for each in message:
49-
if each not in alphabet:
50-
raise ValueError("Each message character has to be included in alphabet!")
108+
if any(char not in alphabet for char in message):
109+
raise ValueError("Each message character has to be included in alphabet!")
51110

52111
# Generate dictionares
53-
numbers = (
54-
"111",
55-
"112",
56-
"113",
57-
"121",
58-
"122",
59-
"123",
60-
"131",
61-
"132",
62-
"133",
63-
"211",
64-
"212",
65-
"213",
66-
"221",
67-
"222",
68-
"223",
69-
"231",
70-
"232",
71-
"233",
72-
"311",
73-
"312",
74-
"313",
75-
"321",
76-
"322",
77-
"323",
78-
"331",
79-
"332",
80-
"333",
81-
)
82-
character_to_number = {}
83-
number_to_character = {}
84-
for letter, number in zip(alphabet, numbers):
85-
character_to_number[letter] = number
86-
number_to_character[number] = letter
112+
character_to_number = dict(zip(alphabet, TEST_CHARACTER_TO_NUMBER.values()))
113+
number_to_character = {
114+
number: letter for letter, number in character_to_number.items()
115+
}
87116

88117
return message, alphabet, character_to_number, number_to_character
89118

90119

91120
def encrypt_message(
92121
message: str, alphabet: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ.", period: int = 5
93122
) -> str:
123+
"""
124+
encrypt_message
125+
===============
126+
127+
Encrypts a message using the trifid_cipher. Any punctuatuions that
128+
would be used should be added to the alphabet.
129+
130+
PARAMETERS
131+
----------
132+
133+
* message: The message you want to encrypt.
134+
* alphabet (optional): The characters to be used for the cipher .
135+
* period (optional): The number of characters you want in a group whilst
136+
encrypting.
137+
138+
>>> encrypt_message('I am a boy')
139+
'BCDGBQY'
140+
141+
>>> encrypt_message(' ')
142+
''
143+
144+
>>> encrypt_message(' aide toi le c iel ta id era ',
145+
... 'FELIXMARDSTBCGHJKNOPQUVWYZ+',5)
146+
'FMJFVOISSUFTFPUFEQQC'
147+
148+
"""
94149
message, alphabet, character_to_number, number_to_character = __prepare(
95150
message, alphabet
96151
)
97-
encrypted, encrypted_numeric = "", ""
98152

153+
encrypted_numeric = ""
99154
for i in range(0, len(message) + 1, period):
100155
encrypted_numeric += __encrypt_part(
101156
message[i : i + period], character_to_number
102157
)
103158

159+
encrypted = ""
104160
for i in range(0, len(encrypted_numeric), 3):
105161
encrypted += number_to_character[encrypted_numeric[i : i + 3]]
106-
107162
return encrypted
108163

109164

110165
def decrypt_message(
111166
message: str, alphabet: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ.", period: int = 5
112167
) -> str:
168+
"""
169+
decrypt_message
170+
===============
171+
172+
Decrypts a trifid_cipher encrypted message .
173+
174+
PARAMETERS
175+
----------
176+
177+
* message: The message you want to decrypt .
178+
* alphabet (optional): The characters used for the cipher.
179+
* period (optional): The number of characters used in grouping when it
180+
was encrypted.
181+
182+
>>> decrypt_message('BCDGBQY')
183+
'IAMABOY'
184+
185+
Decrypting with your own alphabet and period
186+
>>> decrypt_message('FMJFVOISSUFTFPUFEQQC','FELIXMARDSTBCGHJKNOPQUVWYZ+',5)
187+
'AIDETOILECIELTAIDERA'
188+
"""
113189
message, alphabet, character_to_number, number_to_character = __prepare(
114190
message, alphabet
115191
)
116-
decrypted_numeric = []
117-
decrypted = ""
118192

119-
for i in range(0, len(message) + 1, period):
193+
decrypted_numeric = []
194+
for i in range(0, len(message), period):
120195
a, b, c = __decrypt_part(message[i : i + period], character_to_number)
121196

122197
for j in range(len(a)):
123198
decrypted_numeric.append(a[j] + b[j] + c[j])
124199

125-
for each in decrypted_numeric:
126-
decrypted += number_to_character[each]
127-
128-
return decrypted
200+
return "".join(number_to_character[each] for each in decrypted_numeric)
129201

130202

131203
if __name__ == "__main__":
204+
import doctest
205+
206+
doctest.testmod()
132207
msg = "DEFEND THE EAST WALL OF THE CASTLE."
133208
encrypted = encrypt_message(msg, "EPSDUCVWYM.ZLKXNBTFGORIJHAQ")
134209
decrypted = decrypt_message(encrypted, "EPSDUCVWYM.ZLKXNBTFGORIJHAQ")

Diff for: pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -135,5 +135,5 @@ omit = [
135135
sort = "Cover"
136136

137137
[tool.codespell]
138-
ignore-words-list = "3rt,ans,crate,damon,fo,followings,hist,iff,kwanza,manuel,mater,secant,som,sur,tim,zar"
138+
ignore-words-list = "3rt,ans,crate,damon,fo,followings,hist,iff,kwanza,manuel,mater,secant,som,sur,tim,toi,zar"
139139
skip = "./.*,*.json,ciphers/prehistoric_men.txt,project_euler/problem_022/p022_names.txt,pyproject.toml,strings/dictionary.txt,strings/words.txt"

0 commit comments

Comments
 (0)