|
1 | 1 | """ |
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 |
4 | 8 | """ |
5 | 9 |
|
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", " ": " ", |
34 | 18 | } |
35 | 19 |
|
| 20 | +DECODE_DICT = {value: key for key, value in ENCODE_DICT.items()} |
36 | 21 |
|
37 | | -decode_dict = {value: key for key, value in encode_dict.items()} |
38 | 22 |
|
39 | | - |
40 | | -def encode(word: str) -> str: |
| 23 | +def encode(message: str, symbols: tuple[str, str] = ("A", "B")) -> str: |
41 | 24 | """ |
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' |
52 | 52 | """ |
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) |
57 | 60 | 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 |
60 | 64 |
|
61 | 65 |
|
62 | | -def decode(coded: str) -> str: |
| 66 | +def decode(cipher: str, symbols: tuple[str, str] = ("A", "B")) -> str: |
63 | 67 | """ |
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' |
74 | 93 | """ |
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}.") |
84 | 98 |
|
| 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) |
85 | 116 |
|
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. |
88 | 132 |
|
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