4c3f14b00ab3c8ce8c0f37aae144d9470bdb8d35
5 from itertools
import zip_longest
, cycle
, chain
, count
7 from numpy
import matrix
8 from numpy
import linalg
9 from language_models
import *
18 if letter
in string
.ascii_lowercase
:
19 return ord(letter
) - ord('a')
20 elif letter
in string
.ascii_uppercase
:
21 return ord(letter
) - ord('A')
25 def unpos(number
): return chr(number
% 26 + ord('a'))
28 modular_division_table
= [[0]*26 for _
in range(26)]
32 modular_division_table
[b
][c
] = a
35 def every_nth(text
, n
, fillvalue
=''):
36 """Returns n strings, each of which consists of every nth character,
37 starting with the 0th, 1st, 2nd, ... (n-1)th character
39 >>> every_nth(string.ascii_lowercase, 5)
40 ['afkpuz', 'bglqv', 'chmrw', 'dinsx', 'ejoty']
41 >>> every_nth(string.ascii_lowercase, 1)
42 ['abcdefghijklmnopqrstuvwxyz']
43 >>> every_nth(string.ascii_lowercase, 26) # doctest: +NORMALIZE_WHITESPACE
44 ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
45 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
46 >>> every_nth(string.ascii_lowercase, 5, fillvalue='!')
47 ['afkpuz', 'bglqv!', 'chmrw!', 'dinsx!', 'ejoty!']
49 split_text
= chunks(text
, n
, fillvalue
)
50 return [cat(l
) for l
in zip_longest(*split_text
, fillvalue
=fillvalue
)]
52 def combine_every_nth(split_text
):
53 """Reforms a text split into every_nth strings
55 >>> combine_every_nth(every_nth(string.ascii_lowercase, 5))
56 'abcdefghijklmnopqrstuvwxyz'
57 >>> combine_every_nth(every_nth(string.ascii_lowercase, 1))
58 'abcdefghijklmnopqrstuvwxyz'
59 >>> combine_every_nth(every_nth(string.ascii_lowercase, 26))
60 'abcdefghijklmnopqrstuvwxyz'
63 for l
in zip_longest(*split_text
, fillvalue
='')])
65 def chunks(text
, n
, fillvalue
=None):
66 """Split a text into chunks of n characters
68 >>> chunks('abcdefghi', 3)
70 >>> chunks('abcdefghi', 4)
72 >>> chunks('abcdefghi', 4, fillvalue='!')
73 ['abcd', 'efgh', 'i!!!']
76 padding
= fillvalue
[0] * (n
- len(text
) % n
)
79 return [(text
+padding
)[i
:i
+n
] for i
in range(0, len(text
), n
)]
81 def transpose(items
, transposition
):
82 """Moves items around according to the given transposition
84 >>> transpose(['a', 'b', 'c', 'd'], (0,1,2,3))
86 >>> transpose(['a', 'b', 'c', 'd'], (3,1,2,0))
88 >>> transpose([10,11,12,13,14,15], (3,2,4,1,5,0))
89 [13, 12, 14, 11, 15, 10]
91 transposed
= [''] * len(transposition
)
92 for p
, t
in enumerate(transposition
):
93 transposed
[p
] = items
[t
]
96 def untranspose(items
, transposition
):
99 >>> untranspose(['a', 'b', 'c', 'd'], [0,1,2,3])
101 >>> untranspose(['d', 'b', 'c', 'a'], [3,1,2,0])
103 >>> untranspose([13, 12, 14, 11, 15, 10], [3,2,4,1,5,0])
104 [10, 11, 12, 13, 14, 15]
106 transposed
= [''] * len(transposition
)
107 for p
, t
in enumerate(transposition
):
108 transposed
[t
] = items
[p
]
111 def deduplicate(text
):
112 return list(collections
.OrderedDict
.fromkeys(text
))
115 def caesar_encipher_letter(accented_letter
, shift
):
116 """Encipher a letter, given a shift amount
118 >>> caesar_encipher_letter('a', 1)
120 >>> caesar_encipher_letter('a', 2)
122 >>> caesar_encipher_letter('b', 2)
124 >>> caesar_encipher_letter('x', 2)
126 >>> caesar_encipher_letter('y', 2)
128 >>> caesar_encipher_letter('z', 2)
130 >>> caesar_encipher_letter('z', -1)
132 >>> caesar_encipher_letter('a', -1)
134 >>> caesar_encipher_letter('A', 1)
136 >>> caesar_encipher_letter('é', 1)
139 # letter = unaccent(accented_letter)
140 # if letter in string.ascii_letters:
141 # if letter in string.ascii_uppercase:
142 # alphabet_start = ord('A')
144 # alphabet_start = ord('a')
145 # return chr(((ord(letter) - alphabet_start + shift) % 26) +
150 letter
= unaccent(accented_letter
)
151 if letter
in string
.ascii_letters
:
152 cipherletter
= unpos(pos(letter
) + shift
)
153 if letter
in string
.ascii_uppercase
:
154 return cipherletter
.upper()
160 def caesar_decipher_letter(letter
, shift
):
161 """Decipher a letter, given a shift amount
163 >>> caesar_decipher_letter('b', 1)
165 >>> caesar_decipher_letter('b', 2)
168 return caesar_encipher_letter(letter
, -shift
)
170 def caesar_encipher(message
, shift
):
171 """Encipher a message with the Caesar cipher of given shift
173 >>> caesar_encipher('abc', 1)
175 >>> caesar_encipher('abc', 2)
177 >>> caesar_encipher('abcxyz', 2)
179 >>> caesar_encipher('ab cx yz', 2)
181 >>> caesar_encipher('Héllo World!', 2)
184 enciphered
= [caesar_encipher_letter(l
, shift
) for l
in message
]
185 return cat(enciphered
)
187 def caesar_decipher(message
, shift
):
188 """Decipher a message with the Caesar cipher of given shift
190 >>> caesar_decipher('bcd', 1)
192 >>> caesar_decipher('cde', 2)
194 >>> caesar_decipher('cd ez ab', 2)
196 >>> caesar_decipher('Jgnnq Yqtnf!', 2)
199 return caesar_encipher(message
, -shift
)
201 def affine_encipher_letter(accented_letter
, multiplier
=1, adder
=0, one_based
=True):
202 """Encipher a letter, given a multiplier and adder
204 >>> cat(affine_encipher_letter(l, 3, 5, True) \
205 for l in string.ascii_letters)
206 'hknqtwzcfiloruxadgjmpsvybeHKNQTWZCFILORUXADGJMPSVYBE'
207 >>> cat(affine_encipher_letter(l, 3, 5, False) \
208 for l in string.ascii_letters)
209 'filoruxadgjmpsvybehknqtwzcFILORUXADGJMPSVYBEHKNQTWZC'
211 # letter = unaccent(accented_letter)
212 # if letter in string.ascii_letters:
213 # if letter in string.ascii_uppercase:
214 # alphabet_start = ord('A')
216 # alphabet_start = ord('a')
217 # letter_number = ord(letter) - alphabet_start
218 # if one_based: letter_number += 1
219 # cipher_number = (letter_number * multiplier + adder) % 26
220 # if one_based: cipher_number -= 1
221 # return chr(cipher_number % 26 + alphabet_start)
224 letter
= unaccent(accented_letter
)
225 if letter
in string
.ascii_letters
:
226 letter_number
= pos(letter
)
227 if one_based
: letter_number
+= 1
228 cipher_number
= (letter_number
* multiplier
+ adder
) % 26
229 if one_based
: cipher_number
-= 1
230 if letter
in string
.ascii_uppercase
:
231 return unpos(cipher_number
).upper()
233 return unpos(cipher_number
)
237 def affine_decipher_letter(letter
, multiplier
=1, adder
=0, one_based
=True):
238 """Encipher a letter, given a multiplier and adder
240 >>> cat(affine_decipher_letter(l, 3, 5, True) \
241 for l in 'hknqtwzcfiloruxadgjmpsvybeHKNQTWZCFILORUXADGJMPSVYBE')
242 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
243 >>> cat(affine_decipher_letter(l, 3, 5, False) \
244 for l in 'filoruxadgjmpsvybehknqtwzcFILORUXADGJMPSVYBEHKNQTWZC')
245 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
247 # if letter in string.ascii_letters:
248 # if letter in string.ascii_uppercase:
249 # alphabet_start = ord('A')
251 # alphabet_start = ord('a')
252 # cipher_number = ord(letter) - alphabet_start
253 # if one_based: cipher_number += 1
254 # plaintext_number = (
255 # modular_division_table[multiplier]
256 # [(cipher_number - adder) % 26])
257 # if one_based: plaintext_number -= 1
258 # return chr(plaintext_number % 26 + alphabet_start)
261 if letter
in string
.ascii_letters
:
262 cipher_number
= pos(letter
)
263 if one_based
: cipher_number
+= 1
265 modular_division_table
[multiplier
]
266 [(cipher_number
- adder
) % 26])
267 if one_based
: plaintext_number
-= 1
268 if letter
in string
.ascii_uppercase
:
269 return unpos(plaintext_number
).upper()
271 return unpos(plaintext_number
)
275 def affine_encipher(message
, multiplier
=1, adder
=0, one_based
=True):
276 """Encipher a message
278 >>> affine_encipher('hours passed during which jerico tried every ' \
279 'trick he could think of', 15, 22, True)
280 'lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh'
282 enciphered
= [affine_encipher_letter(l
, multiplier
, adder
, one_based
)
284 return cat(enciphered
)
286 def affine_decipher(message
, multiplier
=1, adder
=0, one_based
=True):
287 """Decipher a message
289 >>> affine_decipher('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg ' \
290 'jfaoe ls omytd jlaxe mh', 15, 22, True)
291 'hours passed during which jerico tried every trick he could think of'
293 enciphered
= [affine_decipher_letter(l
, multiplier
, adder
, one_based
)
295 return cat(enciphered
)
298 class KeywordWrapAlphabet(Enum
):
304 def keyword_cipher_alphabet_of(keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
305 """Find the cipher alphabet given a keyword.
306 wrap_alphabet controls how the rest of the alphabet is added
309 >>> keyword_cipher_alphabet_of('bayes')
310 'bayescdfghijklmnopqrtuvwxz'
311 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_a)
312 'bayescdfghijklmnopqrtuvwxz'
313 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_last)
314 'bayestuvwxzcdfghijklmnopqr'
315 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_largest)
316 'bayeszcdfghijklmnopqrtuvwx'
318 if wrap_alphabet
== KeywordWrapAlphabet
.from_a
:
319 cipher_alphabet
= cat(deduplicate(sanitise(keyword
) +
320 string
.ascii_lowercase
))
322 if wrap_alphabet
== KeywordWrapAlphabet
.from_last
:
323 last_keyword_letter
= deduplicate(sanitise(keyword
))[-1]
325 last_keyword_letter
= sorted(sanitise(keyword
))[-1]
326 last_keyword_position
= string
.ascii_lowercase
.find(
327 last_keyword_letter
) + 1
328 cipher_alphabet
= cat(
329 deduplicate(sanitise(keyword
) +
330 string
.ascii_lowercase
[last_keyword_position
:] +
331 string
.ascii_lowercase
))
332 return cipher_alphabet
335 def keyword_encipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
336 """Enciphers a message with a keyword substitution cipher.
337 wrap_alphabet controls how the rest of the alphabet is added
340 1 : from the last letter in the sanitised keyword
341 2 : from the largest letter in the sanitised keyword
343 >>> keyword_encipher('test message', 'bayes')
345 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_a)
347 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_last)
349 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_largest)
352 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
353 cipher_translation
= ''.maketrans(string
.ascii_lowercase
, cipher_alphabet
)
354 return unaccent(message
).lower().translate(cipher_translation
)
356 def keyword_decipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
357 """Deciphers a message with a keyword substitution cipher.
358 wrap_alphabet controls how the rest of the alphabet is added
361 1 : from the last letter in the sanitised keyword
362 2 : from the largest letter in the sanitised keyword
364 >>> keyword_decipher('rsqr ksqqbds', 'bayes')
366 >>> keyword_decipher('rsqr ksqqbds', 'bayes', KeywordWrapAlphabet.from_a)
368 >>> keyword_decipher('lskl dskkbus', 'bayes', KeywordWrapAlphabet.from_last)
370 >>> keyword_decipher('qspq jsppbcs', 'bayes', KeywordWrapAlphabet.from_largest)
373 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
374 cipher_translation
= ''.maketrans(cipher_alphabet
, string
.ascii_lowercase
)
375 return message
.lower().translate(cipher_translation
)
378 def vigenere_encipher(message
, keyword
):
381 >>> vigenere_encipher('hello', 'abc')
384 shifts
= [ord(l
) - ord('a') for l
in sanitise(keyword
)]
385 pairs
= zip(message
, cycle(shifts
))
386 return cat([caesar_encipher_letter(l
, k
) for l
, k
in pairs
])
388 def vigenere_decipher(message
, keyword
):
391 >>> vigenere_decipher('hfnlp', 'abc')
394 shifts
= [ord(l
) - ord('a') for l
in sanitise(keyword
)]
395 pairs
= zip(message
, cycle(shifts
))
396 return cat([caesar_decipher_letter(l
, k
) for l
, k
in pairs
])
398 beaufort_encipher
=vigenere_decipher
399 beaufort_decipher
=vigenere_encipher
402 def polybius_grid(keyword
, column_order
, row_order
, letters_to_merge
=None,
403 wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
404 """Grid for a Polybius cipher, using a keyword to rearrange the
408 >>> polybius_grid('a', 'abcde', 'abcde')['x'] == ('e', 'c')
410 >>> polybius_grid('elephant', 'abcde', 'abcde')['e'] == ('a', 'a')
412 >>> polybius_grid('elephant', 'abcde', 'abcde')['b'] == ('b', 'c')
415 alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
=wrap_alphabet
)
416 if letters_to_merge
is None:
417 letters_to_merge
= {'j': 'i'}
419 for k
, l
in zip([(c
, r
) for c
in column_order
for r
in row_order
],
420 [l
for l
in alphabet
if l
not in letters_to_merge
])}
421 for l
in letters_to_merge
:
422 grid
[l
] = grid
[letters_to_merge
[l
]]
425 def polybius_reverse_grid(keyword
, column_order
, row_order
, letters_to_merge
=None,
426 wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
427 """Grid for decrypting using a Polybius cipher, using a keyword to
428 rearrange the alphabet.
430 >>> polybius_reverse_grid('a', 'abcde', 'abcde')['e', 'c'] == 'x'
432 >>> polybius_reverse_grid('elephant', 'abcde', 'abcde')['a', 'a'] == 'e'
434 >>> polybius_reverse_grid('elephant', 'abcde', 'abcde')['b', 'c'] == 'b'
437 alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
=wrap_alphabet
)
438 if letters_to_merge
is None:
439 letters_to_merge
= {'j': 'i'}
441 for k
, l
in zip([(c
, r
) for c
in column_order
for r
in row_order
],
442 [l
for l
in alphabet
if l
not in letters_to_merge
])}
446 def polybius_flatten(pair
, column_first
):
447 """Convert a series of pairs into a single list of characters"""
449 return str(pair
[1]) + str(pair
[0])
451 return str(pair
[0]) + str(pair
[1])
453 def polybius_encipher(message
, keyword
, column_order
, row_order
,
455 letters_to_merge
=None, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
456 """Encipher a message with Polybius cipher, using a keyword to rearrange
460 >>> polybius_encipher('this is a test message for the ' \
461 'polybius decipherment', 'elephant', \
462 [1, 2, 3, 4, 5], [1, 2, 3, 4, 5], \
463 wrap_alphabet=KeywordWrapAlphabet.from_last)
464 '2214445544551522115522511155551543114252542214111352123234442355411135441314115451112122'
465 >>> polybius_encipher('this is a test message for the ' \
466 'polybius decipherment', 'elephant', 'abcde', 'abcde', \
468 'bbadccddccddaebbaaddbbceaaddddaecbaacadadcbbadaaacdaabedbcccdeddbeaabdccacadaadcceaababb'
469 >>> polybius_encipher('this is a test message for the ' \
470 'polybius decipherment', 'elephant', 'abcde', 'abcde', \
472 'bbdaccddccddeabbaaddbbecaaddddeabcaaacadcdbbdaaacaadbadecbccedddebaadbcccadaaacdecaaabbb'
474 grid
= polybius_grid(keyword
, column_order
, row_order
, letters_to_merge
, wrap_alphabet
)
475 return cat(polybius_flatten(grid
[l
], column_first
)
480 def polybius_decipher(message
, keyword
, column_order
, row_order
,
482 letters_to_merge
=None, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
483 """Decipher a message with a Polybius cipher, using a keyword to rearrange
486 >>> polybius_decipher('bbdaccddccddeabbaaddbbecaaddddeabcaaacadcdbbdaaaca'\
487 'adbadecbccedddebaadbcccadaaacdecaaabbb', 'elephant', 'abcde', 'abcde', \
489 'toisisvtestxessvbephktoefhnugiysweqifoekxelt'
491 >>> polybius_decipher('bbdaccddccddeabbaaddbbecaaddddeabcaaacadcdbbdaaaca'\
492 'adbadecbccedddebaadbcccadaaacdecaaabbb', 'elephant', 'abcde', 'abcde', \
494 'thisisatestmessageforthepolybiusdecipherment'
496 grid
= polybius_reverse_grid(keyword
, column_order
, row_order
, letters_to_merge
, wrap_alphabet
)
497 column_index_type
= type(column_order
[0])
498 row_index_type
= type(row_order
[0])
500 pairs
= [(column_index_type(p
[1]), row_index_type(p
[0])) for p
in chunks(message
, 2)]
502 pairs
= [(row_index_type(p
[0]), column_index_type(p
[1])) for p
in chunks(message
, 2)]
503 return cat(grid
[p
] for p
in pairs
if p
in grid
)
506 def transpositions_of(keyword
):
507 """Finds the transpostions given by a keyword. For instance, the keyword
508 'clever' rearranges to 'celrv', so the first column (0) stays first, the
509 second column (1) moves to third, the third column (2) moves to second,
512 If passed a tuple, assume it's already a transposition and just return it.
514 >>> transpositions_of('clever')
516 >>> transpositions_of('fred')
518 >>> transpositions_of((3, 2, 0, 1))
521 if isinstance(keyword
, tuple):
524 key
= deduplicate(keyword
)
525 transpositions
= tuple(key
.index(l
) for l
in sorted(key
))
526 return transpositions
528 def pad(message_len
, group_len
, fillvalue
):
529 padding_length
= group_len
- message_len
% group_len
530 if padding_length
== group_len
: padding_length
= 0
532 for i
in range(padding_length
):
533 if callable(fillvalue
):
534 padding
+= fillvalue()
539 def column_transposition_encipher(message
, keyword
, fillvalue
=' ',
540 fillcolumnwise
=False,
541 emptycolumnwise
=False):
542 """Enciphers using the column transposition cipher.
543 Message is padded to allow all rows to be the same length.
545 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True)
547 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True, emptycolumnwise=True)
549 >>> column_transposition_encipher('hellothere', 'abcdef')
551 >>> column_transposition_encipher('hellothere', 'abcde')
553 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
555 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
557 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
559 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
561 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=True)
563 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=False)
565 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=True)
567 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=False)
569 >>> column_transposition_encipher('hellothere', 'cleverly')
571 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
573 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue=lambda: '*')
576 transpositions
= transpositions_of(keyword
)
577 message
+= pad(len(message
), len(transpositions
), fillvalue
)
579 rows
= every_nth(message
, len(message
) // len(transpositions
))
581 rows
= chunks(message
, len(transpositions
))
582 transposed
= [transpose(r
, transpositions
) for r
in rows
]
584 return combine_every_nth(transposed
)
586 return cat(chain(*transposed
))
588 def column_transposition_decipher(message
, keyword
, fillvalue
=' ',
589 fillcolumnwise
=False,
590 emptycolumnwise
=False):
591 """Deciphers using the column transposition cipher.
592 Message is padded to allow all rows to be the same length.
594 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
596 >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
598 >>> column_transposition_decipher('htehlelroe', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
600 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
602 >>> column_transposition_decipher('heotllrehe', 'clever', fillcolumnwise=True, emptycolumnwise=True)
604 >>> column_transposition_decipher('holrhetlee', 'clever', fillcolumnwise=True, emptycolumnwise=False)
606 >>> column_transposition_decipher('htleehoelr', 'clever', fillcolumnwise=False, emptycolumnwise=True)
608 >>> column_transposition_decipher('hleolteher', 'clever', fillcolumnwise=False, emptycolumnwise=False)
611 transpositions
= transpositions_of(keyword
)
612 message
+= pad(len(message
), len(transpositions
), fillvalue
)
614 rows
= every_nth(message
, len(message
) // len(transpositions
))
616 rows
= chunks(message
, len(transpositions
))
617 untransposed
= [untranspose(r
, transpositions
) for r
in rows
]
619 return combine_every_nth(untransposed
)
621 return cat(chain(*untransposed
))
623 def scytale_encipher(message
, rows
, fillvalue
=' '):
624 """Enciphers using the scytale transposition cipher.
625 Message is padded with spaces to allow all rows to be the same length.
627 >>> scytale_encipher('thequickbrownfox', 3)
629 >>> scytale_encipher('thequickbrownfox', 4)
631 >>> scytale_encipher('thequickbrownfox', 5)
632 'tubn hirf ecoo qkwx '
633 >>> scytale_encipher('thequickbrownfox', 6)
635 >>> scytale_encipher('thequickbrownfox', 7)
636 'tqcrnx hukof eibwo '
638 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
639 # return column_transposition_encipher(message, transpositions,
640 # fillvalue=fillvalue, fillcolumnwise=False, emptycolumnwise=True)
641 transpositions
= [i
for i
in range(rows
)]
642 return column_transposition_encipher(message
, transpositions
,
643 fillvalue
=fillvalue
, fillcolumnwise
=True, emptycolumnwise
=False)
645 def scytale_decipher(message
, rows
):
646 """Deciphers using the scytale transposition cipher.
647 Assumes the message is padded so that all rows are the same length.
649 >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
651 >>> scytale_decipher('tubnhirfecooqkwx', 4)
653 >>> scytale_decipher('tubn hirf ecoo qkwx ', 5)
655 >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
657 >>> scytale_decipher('tqcrnx hukof eibwo ', 7)
660 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
661 # return column_transposition_decipher(message, transpositions,
662 # fillcolumnwise=False, emptycolumnwise=True)
663 transpositions
= [i
for i
in range(rows
)]
664 return column_transposition_decipher(message
, transpositions
,
665 fillcolumnwise
=True, emptycolumnwise
=False)
668 def railfence_encipher(message
, height
, fillvalue
=''):
670 Works by splitting the text into sections, then reading across them to
671 generate the rows in the cipher. The rows are then combined to form the
674 Example: the plaintext "hellotherefriends", with a height of four, written
675 out in the railfence as
680 (with the * showing the one character to finish the last section).
681 Each 'section' is two columns, but unfolded. In the example, the first
684 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 2, fillvalue='!')
685 'hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!'
686 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 3, fillvalue='!')
687 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihr!!lhateihsnefttiaece!'
688 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 5, fillvalue='!')
689 'hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!'
690 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 10, fillvalue='!')
691 'hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!'
692 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 3)
693 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece'
694 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 5)
695 'hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp'
696 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 7)
697 'haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic'
699 sections
= chunks(message
, (height
- 1) * 2, fillvalue
=fillvalue
)
700 n_sections
= len(sections
)
702 rows
= [cat([s
[0] for s
in sections
])]
703 # process the middle rows of the grid
704 for r
in range(1, height
-1):
705 rows
+= [cat([s
[r
:r
+1] + s
[height
*2-r
-2:height
*2-r
-1] for s
in sections
])]
706 # process the bottom row
707 rows
+= [cat([s
[height
- 1:height
] for s
in sections
])]
708 # rows += [wcat([s[height - 1] for s in sections])]
711 def railfence_decipher(message
, height
, fillvalue
=''):
712 """Railfence decipher.
713 Works by reconstructing the grid used to generate the ciphertext, then
714 unfolding the sections so the text can be concatenated together.
716 Example: given the ciphertext 'hhieterelorfnsled' and a height of 4, first
717 work out that the second row has a character missing, find the rows of the
718 grid, then split the section into its two columns.
720 'hhieterelorfnsled' is split into
725 (spaces added for clarity), which is stored in 'rows'. This is then split
726 into 'down_rows' and 'up_rows':
738 These are then zipped together (after the up_rows are reversed) to recover
741 Most of the procedure is about finding the correct lengths for each row then
742 splitting the ciphertext into those rows.
744 >>> railfence_decipher('hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!', 2).strip('!')
745 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
746 >>> railfence_decipher('horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihr!!lhateihsnefttiaece!', 3).strip('!')
747 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
748 >>> railfence_decipher('hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!', 5).strip('!')
749 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
750 >>> railfence_decipher('hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!', 10).strip('!')
751 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
752 >>> railfence_decipher('horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece', 3)
753 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
754 >>> railfence_decipher('hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp', 5)
755 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
756 >>> railfence_decipher('haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic', 7)
757 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
759 # find the number and size of the sections, including how many characters
760 # are missing for a full grid
761 n_sections
= math
.ceil(len(message
) / ((height
- 1) * 2))
762 padding_to_add
= n_sections
* (height
- 1) * 2 - len(message
)
763 # row_lengths are for the both up rows and down rows
764 row_lengths
= [n_sections
] * (height
- 1) * 2
765 for i
in range((height
- 1) * 2 - 1, (height
- 1) * 2 - (padding_to_add
+ 1), -1):
767 # folded_rows are the combined row lengths in the middle of the railfence
768 folded_row_lengths
= [row_lengths
[0]]
769 for i
in range(1, height
-1):
770 folded_row_lengths
+= [row_lengths
[i
] + row_lengths
[-i
]]
771 folded_row_lengths
+= [row_lengths
[height
- 1]]
772 # find the rows that form the railfence grid
775 for i
in folded_row_lengths
:
776 rows
+= [message
[row_start
:row_start
+ i
]]
778 # split the rows into the 'down_rows' (those that form the first column of
779 # a section) and the 'up_rows' (those that ofrm the second column of a
781 down_rows
= [rows
[0]]
783 for i
in range(1, height
-1):
784 down_rows
+= [cat([c
for n
, c
in enumerate(rows
[i
]) if n
% 2 == 0])]
785 up_rows
+= [cat([c
for n
, c
in enumerate(rows
[i
]) if n
% 2 == 1])]
786 down_rows
+= [rows
[-1]]
788 return cat(c
for r
in zip_longest(*(down_rows
+ up_rows
), fillvalue
='') for c
in r
)
790 def make_cadenus_keycolumn(doubled_letters
= 'vw', start
='a', reverse
=False):
791 """Makes the key column for a Cadenus cipher (the column down between the
794 >>> make_cadenus_keycolumn()['a']
796 >>> make_cadenus_keycolumn()['b']
798 >>> make_cadenus_keycolumn()['c']
800 >>> make_cadenus_keycolumn()['v']
802 >>> make_cadenus_keycolumn()['w']
804 >>> make_cadenus_keycolumn()['z']
806 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['a']
808 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['b']
810 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['c']
812 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['i']
814 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['j']
816 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['v']
818 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['z']
821 index_to_remove
= string
.ascii_lowercase
.find(doubled_letters
[0])
822 short_alphabet
= string
.ascii_lowercase
[:index_to_remove
] + string
.ascii_lowercase
[index_to_remove
+1:]
824 short_alphabet
= cat(reversed(short_alphabet
))
825 start_pos
= short_alphabet
.find(start
)
826 rotated_alphabet
= short_alphabet
[start_pos
:] + short_alphabet
[:start_pos
]
827 keycolumn
= {l
: i
for i
, l
in enumerate(rotated_alphabet
)}
828 keycolumn
[doubled_letters
[0]] = keycolumn
[doubled_letters
[1]]
831 def cadenus_encipher(message
, keyword
, keycolumn
, fillvalue
='a'):
832 """Encipher with the Cadenus cipher
834 >>> cadenus_encipher(sanitise('Whoever has made a voyage up the Hudson ' \
835 'must remember the Kaatskill mountains. ' \
836 'They are a dismembered branch of the great'), \
838 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
839 'antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaasuvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned'
840 >>> cadenus_encipher(sanitise('a severe limitation on the usefulness of ' \
841 'the cadenus is that every message must be ' \
842 'a multiple of twenty-five letters long'), \
844 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
845 'systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtofarenuseieeieltarlmentieetogevesitfaisltngeeuvowul'
847 rows
= chunks(message
, len(message
) // 25, fillvalue
=fillvalue
)
849 rotated_columns
= [col
[start
:] + col
[:start
] for start
, col
in zip([keycolumn
[l
] for l
in keyword
], columns
)]
850 rotated_rows
= zip(*rotated_columns
)
851 transpositions
= transpositions_of(keyword
)
852 transposed
= [transpose(r
, transpositions
) for r
in rotated_rows
]
853 return cat(chain(*transposed
))
855 def cadenus_decipher(message
, keyword
, keycolumn
, fillvalue
='a'):
857 >>> cadenus_decipher('antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaa' \
858 'suvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned', \
860 make_cadenus_keycolumn(reverse=True))
861 'whoeverhasmadeavoyageupthehudsonmustrememberthekaatskillmountainstheyareadismemberedbranchofthegreat'
862 >>> cadenus_decipher('systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtof' \
863 'arenuseieeieltarlmentieetogevesitfaisltngeeuvowul', \
865 make_cadenus_keycolumn(reverse=True))
866 'aseverelimitationontheusefulnessofthecadenusisthateverymessagemustbeamultipleoftwentyfiveletterslong'
868 rows
= chunks(message
, len(message
) // 25, fillvalue
=fillvalue
)
869 transpositions
= transpositions_of(keyword
)
870 untransposed_rows
= [untranspose(r
, transpositions
) for r
in rows
]
871 columns
= zip(*untransposed_rows
)
872 rotated_columns
= [col
[-start
:] + col
[:-start
] for start
, col
in zip([keycolumn
[l
] for l
in keyword
], columns
)]
873 rotated_rows
= zip(*rotated_columns
)
874 # return rotated_columns
875 return cat(chain(*rotated_rows
))
878 def hill_encipher(matrix
, message_letters
, fillvalue
='a'):
881 >>> hill_encipher(np.matrix([[7,8], [11,11]]), 'hellothere')
883 >>> hill_encipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
888 sanitised_message
= sanitise(message_letters
)
889 if len(sanitised_message
) % n
!= 0:
890 padding
= fillvalue
[0] * (n
- len(sanitised_message
) % n
)
893 message
= [ord(c
) - ord('a') for c
in sanitised_message
+ padding
]
894 message_chunks
= [message
[i
:i
+n
] for i
in range(0, len(message
), n
)]
895 # message_chunks = chunks(message, len(matrix), fillvalue=None)
896 enciphered_chunks
= [((matrix
* np
.matrix(c
).T
).T
).tolist()[0]
897 for c
in message_chunks
]
898 return cat([chr(int(round(l
)) % 26 + ord('a'))
899 for l
in sum(enciphered_chunks
, [])])
901 def hill_decipher(matrix
, message
, fillvalue
='a'):
904 >>> hill_decipher(np.matrix([[7,8], [11,11]]), 'drjiqzdrvx')
906 >>> hill_decipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
910 adjoint
= linalg
.det(matrix
)*linalg
.inv(matrix
)
911 inverse_determinant
= modular_division_table
[int(round(linalg
.det(matrix
))) % 26][1]
912 inverse_matrix
= (inverse_determinant
* adjoint
) % 26
913 return hill_encipher(inverse_matrix
, message
, fillvalue
)
916 # Where each piece of text ends up in the AMSCO transpositon cipher.
917 # 'index' shows where the slice appears in the plaintext, with the slice
918 # from 'start' to 'end'
919 AmscoSlice
= collections
.namedtuple('AmscoSlice', ['index', 'start', 'end'])
921 class AmscoFillStyle(Enum
):
926 def amsco_transposition_positions(message
, keyword
,
928 fillstyle
=AmscoFillStyle
.continuous
,
929 fillcolumnwise
=False,
930 emptycolumnwise
=True):
931 """Creates the grid for the AMSCO transposition cipher. Each element in the
932 grid shows the index of that slice and the start and end positions of the
933 plaintext that go to make it up.
935 >>> amsco_transposition_positions(string.ascii_lowercase, 'freddy', \
936 fillpattern=(1, 2)) # doctest: +NORMALIZE_WHITESPACE
937 [[AmscoSlice(index=3, start=4, end=6),
938 AmscoSlice(index=2, start=3, end=4),
939 AmscoSlice(index=0, start=0, end=1),
940 AmscoSlice(index=1, start=1, end=3),
941 AmscoSlice(index=4, start=6, end=7)],
942 [AmscoSlice(index=8, start=12, end=13),
943 AmscoSlice(index=7, start=10, end=12),
944 AmscoSlice(index=5, start=7, end=9),
945 AmscoSlice(index=6, start=9, end=10),
946 AmscoSlice(index=9, start=13, end=15)],
947 [AmscoSlice(index=13, start=19, end=21),
948 AmscoSlice(index=12, start=18, end=19),
949 AmscoSlice(index=10, start=15, end=16),
950 AmscoSlice(index=11, start=16, end=18),
951 AmscoSlice(index=14, start=21, end=22)],
952 [AmscoSlice(index=18, start=27, end=28),
953 AmscoSlice(index=17, start=25, end=27),
954 AmscoSlice(index=15, start=22, end=24),
955 AmscoSlice(index=16, start=24, end=25),
956 AmscoSlice(index=19, start=28, end=30)]]
958 transpositions
= transpositions_of(keyword
)
959 fill_iterator
= cycle(fillpattern
)
961 message_length
= len(message
)
965 current_fillpattern
= fillpattern
966 while current_position
< message_length
:
968 if fillstyle
== AmscoFillStyle
.same_each_row
:
969 fill_iterator
= cycle(fillpattern
)
970 if fillstyle
== AmscoFillStyle
.reverse_each_row
:
971 fill_iterator
= cycle(current_fillpattern
)
972 for _
in range(len(transpositions
)):
973 index
= next(indices
)
974 gap
= next(fill_iterator
)
975 row
+= [AmscoSlice(index
, current_position
, current_position
+ gap
)]
976 current_position
+= gap
978 if fillstyle
== AmscoFillStyle
.reverse_each_row
:
979 current_fillpattern
= list(reversed(current_fillpattern
))
980 return [transpose(r
, transpositions
) for r
in grid
]
982 def amsco_transposition_encipher(message
, keyword
,
983 fillpattern
=(1,2), fillstyle
=AmscoFillStyle
.reverse_each_row
):
984 """AMSCO transposition encipher.
986 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(1, 2))
988 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(2, 1))
990 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(1, 2))
992 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(2, 1))
994 >>> amsco_transposition_encipher('hereissometexttoencipher', 'encode')
995 'etecstthhomoerereenisxip'
996 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2))
997 'hetcsoeisterereipexthomn'
998 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
999 'hecsoisttererteipexhomen'
1000 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(2, 1))
1001 'heecisoosttrrtepeixhemen'
1002 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2))
1003 'hxtomephescieretoeisnter'
1004 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
1005 'hxomeiphscerettoisenteer'
1007 grid
= amsco_transposition_positions(message
, keyword
,
1008 fillpattern
=fillpattern
, fillstyle
=fillstyle
)
1009 ct_as_grid
= [[message
[s
.start
:s
.end
] for s
in r
] for r
in grid
]
1010 return combine_every_nth(ct_as_grid
)
1013 def amsco_transposition_decipher(message
, keyword
,
1014 fillpattern
=(1,2), fillstyle
=AmscoFillStyle
.reverse_each_row
):
1015 """AMSCO transposition decipher
1017 >>> amsco_transposition_decipher('hoteelhler', 'abc', fillpattern=(1, 2))
1019 >>> amsco_transposition_decipher('hetelhelor', 'abc', fillpattern=(2, 1))
1021 >>> amsco_transposition_decipher('hotelerelh', 'acb', fillpattern=(1, 2))
1023 >>> amsco_transposition_decipher('hetelorlhe', 'acb', fillpattern=(2, 1))
1025 >>> amsco_transposition_decipher('etecstthhomoerereenisxip', 'encode')
1026 'hereissometexttoencipher'
1027 >>> amsco_transposition_decipher('hetcsoeisterereipexthomn', 'cipher', fillpattern=(1, 2))
1028 'hereissometexttoencipher'
1029 >>> amsco_transposition_decipher('hecsoisttererteipexhomen', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
1030 'hereissometexttoencipher'
1031 >>> amsco_transposition_decipher('heecisoosttrrtepeixhemen', 'cipher', fillpattern=(2, 1))
1032 'hereissometexttoencipher'
1033 >>> amsco_transposition_decipher('hxtomephescieretoeisnter', 'cipher', fillpattern=(1, 3, 2))
1034 'hereissometexttoencipher'
1035 >>> amsco_transposition_decipher('hxomeiphscerettoisenteer', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
1036 'hereissometexttoencipher'
1039 grid
= amsco_transposition_positions(message
, keyword
,
1040 fillpattern
=fillpattern
, fillstyle
=fillstyle
)
1041 transposed_sections
= [s
for c
in [l
for l
in zip(*grid
)] for s
in c
]
1042 plaintext_list
= [''] * len(transposed_sections
)
1044 for slice in transposed_sections
:
1045 plaintext_list
[slice.index
] = message
[current_pos
:current_pos
-slice.start
+slice.end
][:len(message
[slice.start
:slice.end
])]
1046 current_pos
+= len(message
[slice.start
:slice.end
])
1047 return cat(plaintext_list
)
1050 def bifid_grid(keyword
, wrap_alphabet
, letter_mapping
):
1051 """Create the grids for a Bifid cipher
1053 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
1054 if letter_mapping
is None:
1055 letter_mapping
= {'j': 'i'}
1056 translation
= ''.maketrans(letter_mapping
)
1057 cipher_alphabet
= cat(collections
.OrderedDict
.fromkeys(cipher_alphabet
.translate(translation
)))
1058 f_grid
= {k
: ((i
// 5) + 1, (i
% 5) + 1)
1059 for i
, k
in enumerate(cipher_alphabet
)}
1060 r_grid
= {((i
// 5) + 1, (i
% 5) + 1): k
1061 for i
, k
in enumerate(cipher_alphabet
)}
1062 return translation
, f_grid
, r_grid
1064 def bifid_encipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
,
1065 letter_mapping
=None, period
=None, fillvalue
=None):
1068 >>> bifid_encipher("indiajelly", 'iguana')
1070 >>> bifid_encipher("indiacurry", 'iguana', period=4)
1072 >>> bifid_encipher("indiacurry", 'iguana', period=4, fillvalue='x')
1075 translation
, f_grid
, r_grid
= bifid_grid(keyword
, wrap_alphabet
, letter_mapping
)
1077 t_message
= message
.translate(translation
)
1078 pairs0
= [f_grid
[l
] for l
in sanitise(t_message
)]
1080 chunked_pairs
= [pairs0
[i
:i
+period
] for i
in range(0, len(pairs0
), period
)]
1081 if len(chunked_pairs
[-1]) < period
and fillvalue
:
1082 chunked_pairs
[-1] += [f_grid
[fillvalue
]] * (period
- len(chunked_pairs
[-1]))
1084 chunked_pairs
= [pairs0
]
1087 for c
in chunked_pairs
:
1088 items
= sum(list(list(i
) for i
in zip(*c
)), [])
1089 p
= [(items
[i
], items
[i
+1]) for i
in range(0, len(items
), 2)]
1092 return cat(r_grid
[p
] for p
in pairs1
)
1095 def bifid_decipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
,
1096 letter_mapping
=None, period
=None, fillvalue
=None):
1097 """Decipher with bifid cipher
1099 >>> bifid_decipher('ibidonhprm', 'iguana')
1101 >>> bifid_decipher("ibnhgaqltm", 'iguana', period=4)
1103 >>> bifid_decipher("ibnhgaqltzml", 'iguana', period=4)
1106 translation
, f_grid
, r_grid
= bifid_grid(keyword
, wrap_alphabet
, letter_mapping
)
1108 t_message
= message
.translate(translation
)
1109 pairs0
= [f_grid
[l
] for l
in sanitise(t_message
)]
1111 chunked_pairs
= [pairs0
[i
:i
+period
] for i
in range(0, len(pairs0
), period
)]
1112 if len(chunked_pairs
[-1]) < period
and fillvalue
:
1113 chunked_pairs
[-1] += [f_grid
[fillvalue
]] * (period
- len(chunked_pairs
[-1]))
1115 chunked_pairs
= [pairs0
]
1118 for c
in chunked_pairs
:
1119 items
= [j
for i
in c
for j
in i
]
1121 p
= [(items
[i
], items
[i
+gap
]) for i
in range(gap
)]
1124 return cat(r_grid
[p
] for p
in pairs1
)
1126 class PocketEnigma(object):
1127 """A pocket enigma machine
1128 The wheel is internally represented as a 26-element list self.wheel_map,
1129 where wheel_map[i] == j shows that the position i places on from the arrow
1130 maps to the position j places on.
1132 def __init__(self
, wheel
=1, position
='a'):
1133 """initialise the pocket enigma, including which wheel to use and the
1134 starting position of the wheel.
1136 The wheel is either 1 or 2 (the predefined wheels) or a list of letter
1139 The position is the letter pointed to by the arrow on the wheel.
1142 [25, 4, 23, 10, 1, 7, 9, 5, 12, 6, 3, 17, 8, 14, 13, 21, 19, 11, 20, 16, 18, 15, 24, 2, 22, 0]
1146 self
.wheel1
= [('a', 'z'), ('b', 'e'), ('c', 'x'), ('d', 'k'),
1147 ('f', 'h'), ('g', 'j'), ('i', 'm'), ('l', 'r'), ('n', 'o'),
1148 ('p', 'v'), ('q', 't'), ('s', 'u'), ('w', 'y')]
1149 self
.wheel2
= [('a', 'c'), ('b', 'd'), ('e', 'w'), ('f', 'i'),
1150 ('g', 'p'), ('h', 'm'), ('j', 'k'), ('l', 'n'), ('o', 'q'),
1151 ('r', 'z'), ('s', 'u'), ('t', 'v'), ('x', 'y')]
1153 self
.make_wheel_map(self
.wheel1
)
1155 self
.make_wheel_map(self
.wheel2
)
1157 self
.validate_wheel_spec(wheel
)
1158 self
.make_wheel_map(wheel
)
1159 if position
in string
.ascii_lowercase
:
1160 self
.position
= ord(position
) - ord('a')
1162 self
.position
= position
1164 def make_wheel_map(self
, wheel_spec
):
1165 """Expands a wheel specification from a list of letter-letter pairs
1166 into a full wheel_map.
1168 >>> pe.make_wheel_map(pe.wheel2)
1169 [2, 3, 0, 1, 22, 8, 15, 12, 5, 10, 9, 13, 7, 11, 16, 6, 14, 25, 20, 21, 18, 19, 4, 24, 23, 17]
1171 self
.validate_wheel_spec(wheel_spec
)
1172 self
.wheel_map
= [0] * 26
1173 for p
in wheel_spec
:
1174 self
.wheel_map
[ord(p
[0]) - ord('a')] = ord(p
[1]) - ord('a')
1175 self
.wheel_map
[ord(p
[1]) - ord('a')] = ord(p
[0]) - ord('a')
1176 return self
.wheel_map
1178 def validate_wheel_spec(self
, wheel_spec
):
1179 """Validates that a wheel specificaiton will turn into a valid wheel
1182 >>> pe.validate_wheel_spec([])
1183 Traceback (most recent call last):
1185 ValueError: Wheel specification has 0 pairs, requires 13
1186 >>> pe.validate_wheel_spec([('a', 'b', 'c')]*13)
1187 Traceback (most recent call last):
1189 ValueError: Not all mappings in wheel specificationhave two elements
1190 >>> pe.validate_wheel_spec([('a', 'b')]*13)
1191 Traceback (most recent call last):
1193 ValueError: Wheel specification does not contain 26 letters
1195 if len(wheel_spec
) != 13:
1196 raise ValueError("Wheel specification has {} pairs, requires 13".
1197 format(len(wheel_spec
)))
1198 for p
in wheel_spec
:
1200 raise ValueError("Not all mappings in wheel specification"
1201 "have two elements")
1202 if len(set([p
[0] for p
in wheel_spec
] +
1203 [p
[1] for p
in wheel_spec
])) != 26:
1204 raise ValueError("Wheel specification does not contain 26 letters")
1206 def encipher_letter(self
, letter
):
1207 """Enciphers a single letter, by advancing the wheel before looking up
1208 the letter on the wheel.
1210 >>> pe.set_position('f')
1212 >>> pe.encipher_letter('k')
1216 return self
.lookup(letter
)
1217 decipher_letter
= encipher_letter
1219 def lookup(self
, letter
):
1220 """Look up what a letter enciphers to, without turning the wheel.
1222 >>> pe.set_position('f')
1224 >>> cat([pe.lookup(l) for l in string.ascii_lowercase])
1225 'udhbfejcpgmokrliwntsayqzvx'
1229 if letter
in string
.ascii_lowercase
:
1231 (self
.wheel_map
[(ord(letter
) - ord('a') - self
.position
) % 26] +
1232 self
.position
) % 26 +
1238 """Advances the wheel one position.
1240 >>> pe.set_position('f')
1245 self
.position
= (self
.position
+ 1) % 26
1246 return self
.position
1248 def encipher(self
, message
, starting_position
=None):
1249 """Enciphers a whole message.
1251 >>> pe.set_position('f')
1253 >>> pe.encipher('helloworld')
1255 >>> pe.set_position('f')
1257 >>> pe.encipher('kjsglcjoqc')
1259 >>> pe.encipher('helloworld', starting_position = 'x')
1262 if starting_position
:
1263 self
.set_position(starting_position
)
1266 transformed
+= self
.encipher_letter(l
)
1270 def set_position(self
, position
):
1271 """Sets the position of the wheel, by specifying the letter the arrow
1274 >>> pe.set_position('a')
1276 >>> pe.set_position('m')
1278 >>> pe.set_position('z')
1281 self
.position
= ord(position
) - ord('a')
1282 return self
.position
1285 if __name__
== "__main__":
1287 doctest
.testmod(extraglobs
={'pe': PocketEnigma(1, 'a')})