Skip to content

Commit 9102776

Browse files
authored
Baconian cipher with any 2 symbols
Updated to allow encoding and decoding with any 2 characters
1 parent e2a78d4 commit 9102776

File tree

1 file changed

+145
-72
lines changed

1 file changed

+145
-72
lines changed

ciphers/baconian_cipher.py

Lines changed: 145 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,162 @@
11
"""
2-
Program to encode and decode Baconian or Bacon's Cipher
3-
Wikipedia reference : https://en.wikipedia.org/wiki/Bacon%27s_cipher
2+
Bacon's/Baconian Cipher Encoder and Decoder
3+
4+
provides functions to encode and decode messages
5+
using Bacon's/Baconian cipher with any 2 symbols, not just A & B.
6+
7+
Source: https://en.wikipedia.org/wiki/Bacon's_cipher
48
"""
59

6-
encode_dict = {
7-
"a": "AAAAA",
8-
"b": "AAAAB",
9-
"c": "AAABA",
10-
"d": "AAABB",
11-
"e": "AABAA",
12-
"f": "AABAB",
13-
"g": "AABBA",
14-
"h": "AABBB",
15-
"i": "ABAAA",
16-
"j": "BBBAA",
17-
"k": "ABAAB",
18-
"l": "ABABA",
19-
"m": "ABABB",
20-
"n": "ABBAA",
21-
"o": "ABBAB",
22-
"p": "ABBBA",
23-
"q": "ABBBB",
24-
"r": "BAAAA",
25-
"s": "BAAAB",
26-
"t": "BAABA",
27-
"u": "BAABB",
28-
"v": "BBBAB",
29-
"w": "BABAA",
30-
"x": "BABAB",
31-
"y": "BABBA",
32-
"z": "BABBB",
33-
" ": " ",
10+
ENCODE_DICT = {
11+
"a": "AAAAA", "b": "AAAAB", "c": "AAABA", "d": "AAABB",
12+
"e": "AABAA", "f": "AABAB", "g": "AABBA", "h": "AABBB",
13+
"i": "ABAAA", "j": "BBBAA", "k": "ABAAB", "l": "ABABA",
14+
"m": "ABABB", "n": "ABBAA", "o": "ABBAB", "p": "ABBBA",
15+
"q": "ABBBB", "r": "BAAAA", "s": "BAAAB", "t": "BAABA",
16+
"u": "BAABB", "v": "BBBAB", "w": "BABAA", "x": "BABAB",
17+
"y": "BABBA", "z": "BABBB", " ": " ",
3418
}
3519

20+
DECODE_DICT = {value: key for key, value in ENCODE_DICT.items()}
3621

37-
decode_dict = {value: key for key, value in encode_dict.items()}
3822

39-
40-
def encode(word: str) -> str:
23+
def encode(message: str, symbols: tuple[str, str] = ("A", "B")) -> str:
4124
"""
42-
Encodes to Baconian cipher
43-
44-
>>> encode("hello")
45-
'AABBBAABAAABABAABABAABBAB'
46-
>>> encode("hello world")
47-
'AABBBAABAAABABAABABAABBAB BABAAABBABBAAAAABABAAAABB'
48-
>>> encode("hello world!")
49-
Traceback (most recent call last):
50-
...
51-
Exception: encode() accepts only letters of the alphabet and spaces
25+
Encode a message using Bacon's cipher.
26+
27+
Parameters
28+
----------
29+
message : str
30+
The message to encode (letters and spaces only).
31+
symbols : tuple[str, str], optional
32+
Custom symbols to replace A and B, by default ("A", "B").
33+
34+
Returns
35+
-------
36+
str
37+
Encoded message using the chosen symbols.
38+
39+
Raises
40+
------
41+
ValueError
42+
If the message contains invalid characters.
43+
44+
Examples
45+
--------
46+
>>> encode("abc")
47+
'AAAAA AAAAB AAABA'
48+
>>> encode("abc", symbols=("X", "Y"))
49+
'XXXXX XXXXY XXYXX'
50+
>>> encode("hi there")
51+
'AABBB ABAAA BAABA AABBB AABAA BAAAA AABAA'
5252
"""
53-
encoded = ""
54-
for letter in word.lower():
55-
if letter.isalpha() or letter == " ":
56-
encoded += encode_dict[letter]
53+
a_sym, b_sym = symbols
54+
encoded_message = ""
55+
56+
for char in message.lower():
57+
if char.isalpha() or char == " ":
58+
bacon = ENCODE_DICT[char]
59+
encoded_message += bacon.replace("A", a_sym).replace("B", b_sym)
5760
else:
58-
raise Exception("encode() accepts only letters of the alphabet and spaces")
59-
return encoded
61+
raise ValueError("Message can only contain letters and spaces.")
62+
63+
return encoded_message
6064

6165

62-
def decode(coded: str) -> str:
66+
def decode(cipher: str, symbols: tuple[str, str] = ("A", "B")) -> str:
6367
"""
64-
Decodes from Baconian cipher
65-
66-
>>> decode("AABBBAABAAABABAABABAABBAB BABAAABBABBAAAAABABAAAABB")
67-
'hello world'
68-
>>> decode("AABBBAABAAABABAABABAABBAB")
69-
'hello'
70-
>>> decode("AABBBAABAAABABAABABAABBAB BABAAABBABBAAAAABABAAAABB!")
71-
Traceback (most recent call last):
72-
...
73-
Exception: decode() accepts only 'A', 'B' and spaces
68+
Decode a Bacon's cipher message.
69+
70+
Parameters
71+
----------
72+
cipher : str
73+
The encoded message using two symbols.
74+
symbols : tuple[str, str], optional
75+
Symbols used in the cipher, by default ("A", "B").
76+
77+
Returns
78+
-------
79+
str
80+
Decoded message.
81+
82+
Raises
83+
------
84+
ValueError
85+
If the cipher contains invalid symbols or cannot be decoded.
86+
87+
Examples
88+
--------
89+
>>> decode("AAAAA AAAAB AAABA")
90+
'abc'
91+
>>> decode("XXXXX XXXXY XXYXX", symbols=("X","Y"))
92+
'abc'
7493
"""
75-
if set(coded) - {"A", "B", " "} != set():
76-
raise Exception("decode() accepts only 'A', 'B' and spaces")
77-
decoded = ""
78-
for word in coded.split():
79-
while len(word) != 0:
80-
decoded += decode_dict[word[:5]]
81-
word = word[5:]
82-
decoded += " "
83-
return decoded.strip()
94+
sym1, sym2 = symbols
95+
unique_symbols = set(cipher.replace(" ", ""))
96+
if unique_symbols - {sym1, sym2}:
97+
raise ValueError(f"Cipher must contain only symbols {sym1} and {sym2}.")
8498

99+
candidates = []
100+
for mapping in [(sym1, sym2), (sym2, sym1)]:
101+
s1, s2 = mapping
102+
standard = cipher.replace(s1, "A").replace(s2, "B")
103+
try:
104+
decoded = ""
105+
for word in standard.split():
106+
while word:
107+
chunk = word[:5]
108+
if chunk not in DECODE_DICT:
109+
raise ValueError
110+
decoded += DECODE_DICT[chunk]
111+
word = word[5:]
112+
decoded += " "
113+
candidates.append(decoded.strip())
114+
except ValueError:
115+
candidates.append(None)
85116

86-
if __name__ == "__main__":
87-
from doctest import testmod
117+
for candidate in candidates:
118+
if candidate is not None:
119+
return candidate
120+
121+
raise ValueError("No valid decoding found.")
122+
123+
124+
def detect_unique_symbols(cipher: str) -> tuple[str, str]:
125+
"""
126+
Detects the two unique symbols used in a cipher.
127+
128+
Parameters
129+
----------
130+
cipher : str
131+
Encoded message containing exactly two unique symbols.
88132
89-
testmod()
133+
Returns
134+
-------
135+
tuple[str, str]
136+
The two unique symbols found in the cipher.
137+
138+
Raises
139+
------
140+
ValueError
141+
If cipher does not contain exactly two unique symbols.
142+
143+
Examples
144+
--------
145+
>>> detect_unique_symbols("XXXYX YXXYX")
146+
('X', 'Y')
147+
"""
148+
letters_only = [char for char in set(cipher.replace(" ", "")) if char.isalpha()]
149+
if len(letters_only) != 2:
150+
raise ValueError("Cipher must contain exactly two unique alphabetic symbols.")
151+
return tuple(letters_only)
152+
153+
154+
if __name__ == "__main__":
155+
# Example usage
156+
cipher_text = (
157+
"FEEFE EEFFF EEFEE EFFFF FEEFF EFEEE EEEFE "
158+
"EFEEF EEEEF FEEEE EFFEF FEFEE EFFEE EEFEF EFFEF FEFEF"
159+
)
160+
sym1, sym2 = detect_unique_symbols(cipher_text)
161+
decoded_message = decode(cipher_text, symbols=(sym1, sym2))
162+
print(decoded_message) # Expected: 'the quick brown fox'

0 commit comments

Comments
 (0)