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
+
2
9
from __future__ import annotations
3
10
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
4
19
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 ()}
8
21
9
- for character in message_part :
10
- tmp .append (character_to_number [character ])
11
22
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 ):
13
33
one += each [0 ]
14
34
two += each [1 ]
15
35
three += each [2 ]
@@ -20,12 +40,16 @@ def __encrypt_part(message_part: str, character_to_number: dict[str, str]) -> st
20
40
def __decrypt_part (
21
41
message_part : str , character_to_number : dict [str , str ]
22
42
) -> 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 )
24
51
result = []
25
-
26
- for character in message_part :
27
- this_part += character_to_number [character ]
28
-
52
+ tmp = ""
29
53
for digit in this_part :
30
54
tmp += digit
31
55
if len (tmp ) == len (message_part ):
@@ -38,97 +62,148 @@ def __decrypt_part(
38
62
def __prepare (
39
63
message : str , alphabet : str
40
64
) -> 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
+ """
41
101
# Validate message and alphabet, set to upper and remove spaces
42
102
alphabet = alphabet .replace (" " , "" ).upper ()
43
103
message = message .replace (" " , "" ).upper ()
44
104
45
105
# Check length and characters
46
106
if len (alphabet ) != 27 :
47
107
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!" )
51
110
52
111
# 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
+ }
87
116
88
117
return message , alphabet , character_to_number , number_to_character
89
118
90
119
91
120
def encrypt_message (
92
121
message : str , alphabet : str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ." , period : int = 5
93
122
) -> 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
+ """
94
149
message , alphabet , character_to_number , number_to_character = __prepare (
95
150
message , alphabet
96
151
)
97
- encrypted , encrypted_numeric = "" , ""
98
152
153
+ encrypted_numeric = ""
99
154
for i in range (0 , len (message ) + 1 , period ):
100
155
encrypted_numeric += __encrypt_part (
101
156
message [i : i + period ], character_to_number
102
157
)
103
158
159
+ encrypted = ""
104
160
for i in range (0 , len (encrypted_numeric ), 3 ):
105
161
encrypted += number_to_character [encrypted_numeric [i : i + 3 ]]
106
-
107
162
return encrypted
108
163
109
164
110
165
def decrypt_message (
111
166
message : str , alphabet : str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ." , period : int = 5
112
167
) -> 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
+ """
113
189
message , alphabet , character_to_number , number_to_character = __prepare (
114
190
message , alphabet
115
191
)
116
- decrypted_numeric = []
117
- decrypted = ""
118
192
119
- for i in range (0 , len (message ) + 1 , period ):
193
+ decrypted_numeric = []
194
+ for i in range (0 , len (message ), period ):
120
195
a , b , c = __decrypt_part (message [i : i + period ], character_to_number )
121
196
122
197
for j in range (len (a )):
123
198
decrypted_numeric .append (a [j ] + b [j ] + c [j ])
124
199
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 )
129
201
130
202
131
203
if __name__ == "__main__" :
204
+ import doctest
205
+
206
+ doctest .testmod ()
132
207
msg = "DEFEND THE EAST WALL OF THE CASTLE."
133
208
encrypted = encrypt_message (msg , "EPSDUCVWYM.ZLKXNBTFGORIJHAQ" )
134
209
decrypted = decrypt_message (encrypted , "EPSDUCVWYM.ZLKXNBTFGORIJHAQ" )
0 commit comments