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+
29from __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
2040def __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(
3862def __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
91120def 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
110165def 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
131203if __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" )
0 commit comments