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 transpositions_of(keyword
):
403 """Finds the transpostions given by a keyword. For instance, the keyword
404 'clever' rearranges to 'celrv', so the first column (0) stays first, the
405 second column (1) moves to third, the third column (2) moves to second,
408 If passed a tuple, assume it's already a transposition and just return it.
410 >>> transpositions_of('clever')
412 >>> transpositions_of('fred')
414 >>> transpositions_of((3, 2, 0, 1))
417 if isinstance(keyword
, tuple):
420 key
= deduplicate(keyword
)
421 transpositions
= tuple(key
.index(l
) for l
in sorted(key
))
422 return transpositions
424 def pad(message_len
, group_len
, fillvalue
):
425 padding_length
= group_len
- message_len
% group_len
426 if padding_length
== group_len
: padding_length
= 0
428 for i
in range(padding_length
):
429 if callable(fillvalue
):
430 padding
+= fillvalue()
435 def column_transposition_encipher(message
, keyword
, fillvalue
=' ',
436 fillcolumnwise
=False,
437 emptycolumnwise
=False):
438 """Enciphers using the column transposition cipher.
439 Message is padded to allow all rows to be the same length.
441 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True)
443 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True, emptycolumnwise=True)
445 >>> column_transposition_encipher('hellothere', 'abcdef')
447 >>> column_transposition_encipher('hellothere', 'abcde')
449 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
451 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
453 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
455 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
457 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=True)
459 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=False)
461 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=True)
463 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=False)
465 >>> column_transposition_encipher('hellothere', 'cleverly')
467 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
469 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue=lambda: '*')
472 transpositions
= transpositions_of(keyword
)
473 message
+= pad(len(message
), len(transpositions
), fillvalue
)
475 rows
= every_nth(message
, len(message
) // len(transpositions
))
477 rows
= chunks(message
, len(transpositions
))
478 transposed
= [transpose(r
, transpositions
) for r
in rows
]
480 return combine_every_nth(transposed
)
482 return cat(chain(*transposed
))
484 def column_transposition_decipher(message
, keyword
, fillvalue
=' ',
485 fillcolumnwise
=False,
486 emptycolumnwise
=False):
487 """Deciphers using the column transposition cipher.
488 Message is padded to allow all rows to be the same length.
490 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
492 >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
494 >>> column_transposition_decipher('htehlelroe', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
496 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
498 >>> column_transposition_decipher('heotllrehe', 'clever', fillcolumnwise=True, emptycolumnwise=True)
500 >>> column_transposition_decipher('holrhetlee', 'clever', fillcolumnwise=True, emptycolumnwise=False)
502 >>> column_transposition_decipher('htleehoelr', 'clever', fillcolumnwise=False, emptycolumnwise=True)
504 >>> column_transposition_decipher('hleolteher', 'clever', fillcolumnwise=False, emptycolumnwise=False)
507 transpositions
= transpositions_of(keyword
)
508 message
+= pad(len(message
), len(transpositions
), fillvalue
)
510 rows
= every_nth(message
, len(message
) // len(transpositions
))
512 rows
= chunks(message
, len(transpositions
))
513 untransposed
= [untranspose(r
, transpositions
) for r
in rows
]
515 return combine_every_nth(untransposed
)
517 return cat(chain(*untransposed
))
519 def scytale_encipher(message
, rows
, fillvalue
=' '):
520 """Enciphers using the scytale transposition cipher.
521 Message is padded with spaces to allow all rows to be the same length.
523 >>> scytale_encipher('thequickbrownfox', 3)
525 >>> scytale_encipher('thequickbrownfox', 4)
527 >>> scytale_encipher('thequickbrownfox', 5)
528 'tubn hirf ecoo qkwx '
529 >>> scytale_encipher('thequickbrownfox', 6)
531 >>> scytale_encipher('thequickbrownfox', 7)
532 'tqcrnx hukof eibwo '
534 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
535 # return column_transposition_encipher(message, transpositions,
536 # fillvalue=fillvalue, fillcolumnwise=False, emptycolumnwise=True)
537 transpositions
= [i
for i
in range(rows
)]
538 return column_transposition_encipher(message
, transpositions
,
539 fillvalue
=fillvalue
, fillcolumnwise
=True, emptycolumnwise
=False)
541 def scytale_decipher(message
, rows
):
542 """Deciphers using the scytale transposition cipher.
543 Assumes the message is padded so that all rows are the same length.
545 >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
547 >>> scytale_decipher('tubnhirfecooqkwx', 4)
549 >>> scytale_decipher('tubn hirf ecoo qkwx ', 5)
551 >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
553 >>> scytale_decipher('tqcrnx hukof eibwo ', 7)
556 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
557 # return column_transposition_decipher(message, transpositions,
558 # fillcolumnwise=False, emptycolumnwise=True)
559 transpositions
= [i
for i
in range(rows
)]
560 return column_transposition_decipher(message
, transpositions
,
561 fillcolumnwise
=True, emptycolumnwise
=False)
564 def railfence_encipher(message
, height
, fillvalue
=''):
566 Works by splitting the text into sections, then reading across them to
567 generate the rows in the cipher. The rows are then combined to form the
570 Example: the plaintext "hellotherefriends", with a height of four, written
571 out in the railfence as
576 (with the * showing the one character to finish the last section).
577 Each 'section' is two columns, but unfolded. In the example, the first
580 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 2, fillvalue='!')
581 'hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!'
582 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 3, fillvalue='!')
583 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihr!!lhateihsnefttiaece!'
584 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 5, fillvalue='!')
585 'hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!'
586 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 10, fillvalue='!')
587 'hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!'
588 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 3)
589 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece'
590 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 5)
591 'hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp'
592 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 7)
593 'haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic'
595 sections
= chunks(message
, (height
- 1) * 2, fillvalue
=fillvalue
)
596 n_sections
= len(sections
)
598 rows
= [cat([s
[0] for s
in sections
])]
599 # process the middle rows of the grid
600 for r
in range(1, height
-1):
601 rows
+= [cat([s
[r
:r
+1] + s
[height
*2-r
-2:height
*2-r
-1] for s
in sections
])]
602 # process the bottom row
603 rows
+= [cat([s
[height
- 1:height
] for s
in sections
])]
604 # rows += [wcat([s[height - 1] for s in sections])]
607 def railfence_decipher(message
, height
, fillvalue
=''):
608 """Railfence decipher.
609 Works by reconstructing the grid used to generate the ciphertext, then
610 unfolding the sections so the text can be concatenated together.
612 Example: given the ciphertext 'hhieterelorfnsled' and a height of 4, first
613 work out that the second row has a character missing, find the rows of the
614 grid, then split the section into its two columns.
616 'hhieterelorfnsled' is split into
621 (spaces added for clarity), which is stored in 'rows'. This is then split
622 into 'down_rows' and 'up_rows':
634 These are then zipped together (after the up_rows are reversed) to recover
637 Most of the procedure is about finding the correct lengths for each row then
638 splitting the ciphertext into those rows.
640 >>> railfence_decipher('hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!', 2).strip('!')
641 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
642 >>> railfence_decipher('horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihr!!lhateihsnefttiaece!', 3).strip('!')
643 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
644 >>> railfence_decipher('hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!', 5).strip('!')
645 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
646 >>> railfence_decipher('hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!', 10).strip('!')
647 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
648 >>> railfence_decipher('horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece', 3)
649 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
650 >>> railfence_decipher('hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp', 5)
651 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
652 >>> railfence_decipher('haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic', 7)
653 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
655 # find the number and size of the sections, including how many characters
656 # are missing for a full grid
657 n_sections
= math
.ceil(len(message
) / ((height
- 1) * 2))
658 padding_to_add
= n_sections
* (height
- 1) * 2 - len(message
)
659 # row_lengths are for the both up rows and down rows
660 row_lengths
= [n_sections
] * (height
- 1) * 2
661 for i
in range((height
- 1) * 2 - 1, (height
- 1) * 2 - (padding_to_add
+ 1), -1):
663 # folded_rows are the combined row lengths in the middle of the railfence
664 folded_row_lengths
= [row_lengths
[0]]
665 for i
in range(1, height
-1):
666 folded_row_lengths
+= [row_lengths
[i
] + row_lengths
[-i
]]
667 folded_row_lengths
+= [row_lengths
[height
- 1]]
668 # find the rows that form the railfence grid
671 for i
in folded_row_lengths
:
672 rows
+= [message
[row_start
:row_start
+ i
]]
674 # split the rows into the 'down_rows' (those that form the first column of
675 # a section) and the 'up_rows' (those that ofrm the second column of a
677 down_rows
= [rows
[0]]
679 for i
in range(1, height
-1):
680 down_rows
+= [cat([c
for n
, c
in enumerate(rows
[i
]) if n
% 2 == 0])]
681 up_rows
+= [cat([c
for n
, c
in enumerate(rows
[i
]) if n
% 2 == 1])]
682 down_rows
+= [rows
[-1]]
684 return cat(c
for r
in zip_longest(*(down_rows
+ up_rows
), fillvalue
='') for c
in r
)
686 def make_cadenus_keycolumn(doubled_letters
= 'vw', start
='a', reverse
=False):
687 """Makes the key column for a Cadenus cipher (the column down between the
690 >>> make_cadenus_keycolumn()['a']
692 >>> make_cadenus_keycolumn()['b']
694 >>> make_cadenus_keycolumn()['c']
696 >>> make_cadenus_keycolumn()['v']
698 >>> make_cadenus_keycolumn()['w']
700 >>> make_cadenus_keycolumn()['z']
702 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['a']
704 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['b']
706 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['c']
708 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['i']
710 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['j']
712 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['v']
714 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['z']
717 index_to_remove
= string
.ascii_lowercase
.find(doubled_letters
[0])
718 short_alphabet
= string
.ascii_lowercase
[:index_to_remove
] + string
.ascii_lowercase
[index_to_remove
+1:]
720 short_alphabet
= cat(reversed(short_alphabet
))
721 start_pos
= short_alphabet
.find(start
)
722 rotated_alphabet
= short_alphabet
[start_pos
:] + short_alphabet
[:start_pos
]
723 keycolumn
= {l
: i
for i
, l
in enumerate(rotated_alphabet
)}
724 keycolumn
[doubled_letters
[0]] = keycolumn
[doubled_letters
[1]]
727 def cadenus_encipher(message
, keyword
, keycolumn
, fillvalue
='a'):
728 """Encipher with the Cadenus cipher
730 >>> cadenus_encipher(sanitise('Whoever has made a voyage up the Hudson ' \
731 'must remember the Kaatskill mountains. ' \
732 'They are a dismembered branch of the great'), \
734 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
735 'antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaasuvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned'
736 >>> cadenus_encipher(sanitise('a severe limitation on the usefulness of ' \
737 'the cadenus is that every message must be ' \
738 'a multiple of twenty-five letters long'), \
740 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
741 'systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtofarenuseieeieltarlmentieetogevesitfaisltngeeuvowul'
743 rows
= chunks(message
, len(message
) // 25, fillvalue
=fillvalue
)
745 rotated_columns
= [col
[start
:] + col
[:start
] for start
, col
in zip([keycolumn
[l
] for l
in keyword
], columns
)]
746 rotated_rows
= zip(*rotated_columns
)
747 transpositions
= transpositions_of(keyword
)
748 transposed
= [transpose(r
, transpositions
) for r
in rotated_rows
]
749 return cat(chain(*transposed
))
751 def cadenus_decipher(message
, keyword
, keycolumn
, fillvalue
='a'):
753 >>> cadenus_decipher('antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaa' \
754 'suvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned', \
756 make_cadenus_keycolumn(reverse=True))
757 'whoeverhasmadeavoyageupthehudsonmustrememberthekaatskillmountainstheyareadismemberedbranchofthegreat'
758 >>> cadenus_decipher('systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtof' \
759 'arenuseieeieltarlmentieetogevesitfaisltngeeuvowul', \
761 make_cadenus_keycolumn(reverse=True))
762 'aseverelimitationontheusefulnessofthecadenusisthateverymessagemustbeamultipleoftwentyfiveletterslong'
764 rows
= chunks(message
, len(message
) // 25, fillvalue
=fillvalue
)
765 transpositions
= transpositions_of(keyword
)
766 untransposed_rows
= [untranspose(r
, transpositions
) for r
in rows
]
767 columns
= zip(*untransposed_rows
)
768 rotated_columns
= [col
[-start
:] + col
[:-start
] for start
, col
in zip([keycolumn
[l
] for l
in keyword
], columns
)]
769 rotated_rows
= zip(*rotated_columns
)
770 # return rotated_columns
771 return cat(chain(*rotated_rows
))
774 def hill_encipher(matrix
, message_letters
, fillvalue
='a'):
777 >>> hill_encipher(np.matrix([[7,8], [11,11]]), 'hellothere')
779 >>> hill_encipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
784 sanitised_message
= sanitise(message_letters
)
785 if len(sanitised_message
) % n
!= 0:
786 padding
= fillvalue
[0] * (n
- len(sanitised_message
) % n
)
789 message
= [ord(c
) - ord('a') for c
in sanitised_message
+ padding
]
790 message_chunks
= [message
[i
:i
+n
] for i
in range(0, len(message
), n
)]
791 # message_chunks = chunks(message, len(matrix), fillvalue=None)
792 enciphered_chunks
= [((matrix
* np
.matrix(c
).T
).T
).tolist()[0]
793 for c
in message_chunks
]
794 return cat([chr(int(round(l
)) % 26 + ord('a'))
795 for l
in sum(enciphered_chunks
, [])])
797 def hill_decipher(matrix
, message
, fillvalue
='a'):
800 >>> hill_decipher(np.matrix([[7,8], [11,11]]), 'drjiqzdrvx')
802 >>> hill_decipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
806 adjoint
= linalg
.det(matrix
)*linalg
.inv(matrix
)
807 inverse_determinant
= modular_division_table
[int(round(linalg
.det(matrix
))) % 26][1]
808 inverse_matrix
= (inverse_determinant
* adjoint
) % 26
809 return hill_encipher(inverse_matrix
, message
, fillvalue
)
812 # Where each piece of text ends up in the AMSCO transpositon cipher.
813 # 'index' shows where the slice appears in the plaintext, with the slice
814 # from 'start' to 'end'
815 AmscoSlice
= collections
.namedtuple('AmscoSlice', ['index', 'start', 'end'])
817 class AmscoFillStyle(Enum
):
822 def amsco_transposition_positions(message
, keyword
,
824 fillstyle
=AmscoFillStyle
.continuous
,
825 fillcolumnwise
=False,
826 emptycolumnwise
=True):
827 """Creates the grid for the AMSCO transposition cipher. Each element in the
828 grid shows the index of that slice and the start and end positions of the
829 plaintext that go to make it up.
831 >>> amsco_transposition_positions(string.ascii_lowercase, 'freddy', \
832 fillpattern=(1, 2)) # doctest: +NORMALIZE_WHITESPACE
833 [[AmscoSlice(index=3, start=4, end=6),
834 AmscoSlice(index=2, start=3, end=4),
835 AmscoSlice(index=0, start=0, end=1),
836 AmscoSlice(index=1, start=1, end=3),
837 AmscoSlice(index=4, start=6, end=7)],
838 [AmscoSlice(index=8, start=12, end=13),
839 AmscoSlice(index=7, start=10, end=12),
840 AmscoSlice(index=5, start=7, end=9),
841 AmscoSlice(index=6, start=9, end=10),
842 AmscoSlice(index=9, start=13, end=15)],
843 [AmscoSlice(index=13, start=19, end=21),
844 AmscoSlice(index=12, start=18, end=19),
845 AmscoSlice(index=10, start=15, end=16),
846 AmscoSlice(index=11, start=16, end=18),
847 AmscoSlice(index=14, start=21, end=22)],
848 [AmscoSlice(index=18, start=27, end=28),
849 AmscoSlice(index=17, start=25, end=27),
850 AmscoSlice(index=15, start=22, end=24),
851 AmscoSlice(index=16, start=24, end=25),
852 AmscoSlice(index=19, start=28, end=30)]]
854 transpositions
= transpositions_of(keyword
)
855 fill_iterator
= cycle(fillpattern
)
857 message_length
= len(message
)
861 current_fillpattern
= fillpattern
862 while current_position
< message_length
:
864 if fillstyle
== AmscoFillStyle
.same_each_row
:
865 fill_iterator
= cycle(fillpattern
)
866 if fillstyle
== AmscoFillStyle
.reverse_each_row
:
867 fill_iterator
= cycle(current_fillpattern
)
868 for _
in range(len(transpositions
)):
869 index
= next(indices
)
870 gap
= next(fill_iterator
)
871 row
+= [AmscoSlice(index
, current_position
, current_position
+ gap
)]
872 current_position
+= gap
874 if fillstyle
== AmscoFillStyle
.reverse_each_row
:
875 current_fillpattern
= list(reversed(current_fillpattern
))
876 return [transpose(r
, transpositions
) for r
in grid
]
878 def amsco_transposition_encipher(message
, keyword
,
879 fillpattern
=(1,2), fillstyle
=AmscoFillStyle
.reverse_each_row
):
880 """AMSCO transposition encipher.
882 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(1, 2))
884 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(2, 1))
886 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(1, 2))
888 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(2, 1))
890 >>> amsco_transposition_encipher('hereissometexttoencipher', 'encode')
891 'etecstthhomoerereenisxip'
892 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2))
893 'hetcsoeisterereipexthomn'
894 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
895 'hecsoisttererteipexhomen'
896 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(2, 1))
897 'heecisoosttrrtepeixhemen'
898 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2))
899 'hxtomephescieretoeisnter'
900 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
901 'hxomeiphscerettoisenteer'
903 grid
= amsco_transposition_positions(message
, keyword
,
904 fillpattern
=fillpattern
, fillstyle
=fillstyle
)
905 ct_as_grid
= [[message
[s
.start
:s
.end
] for s
in r
] for r
in grid
]
906 return combine_every_nth(ct_as_grid
)
909 def amsco_transposition_decipher(message
, keyword
,
910 fillpattern
=(1,2), fillstyle
=AmscoFillStyle
.reverse_each_row
):
911 """AMSCO transposition decipher
913 >>> amsco_transposition_decipher('hoteelhler', 'abc', fillpattern=(1, 2))
915 >>> amsco_transposition_decipher('hetelhelor', 'abc', fillpattern=(2, 1))
917 >>> amsco_transposition_decipher('hotelerelh', 'acb', fillpattern=(1, 2))
919 >>> amsco_transposition_decipher('hetelorlhe', 'acb', fillpattern=(2, 1))
921 >>> amsco_transposition_decipher('etecstthhomoerereenisxip', 'encode')
922 'hereissometexttoencipher'
923 >>> amsco_transposition_decipher('hetcsoeisterereipexthomn', 'cipher', fillpattern=(1, 2))
924 'hereissometexttoencipher'
925 >>> amsco_transposition_decipher('hecsoisttererteipexhomen', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
926 'hereissometexttoencipher'
927 >>> amsco_transposition_decipher('heecisoosttrrtepeixhemen', 'cipher', fillpattern=(2, 1))
928 'hereissometexttoencipher'
929 >>> amsco_transposition_decipher('hxtomephescieretoeisnter', 'cipher', fillpattern=(1, 3, 2))
930 'hereissometexttoencipher'
931 >>> amsco_transposition_decipher('hxomeiphscerettoisenteer', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
932 'hereissometexttoencipher'
935 grid
= amsco_transposition_positions(message
, keyword
,
936 fillpattern
=fillpattern
, fillstyle
=fillstyle
)
937 transposed_sections
= [s
for c
in [l
for l
in zip(*grid
)] for s
in c
]
938 plaintext_list
= [''] * len(transposed_sections
)
940 for slice in transposed_sections
:
941 plaintext_list
[slice.index
] = message
[current_pos
:current_pos
-slice.start
+slice.end
][:len(message
[slice.start
:slice.end
])]
942 current_pos
+= len(message
[slice.start
:slice.end
])
943 return cat(plaintext_list
)
946 def bifid_grid(keyword
, wrap_alphabet
, letter_mapping
):
947 """Create the grids for a Bifid cipher
949 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
950 if letter_mapping
is None:
951 letter_mapping
= {'j': 'i'}
952 translation
= ''.maketrans(letter_mapping
)
953 cipher_alphabet
= cat(collections
.OrderedDict
.fromkeys(cipher_alphabet
.translate(translation
)))
954 f_grid
= {k
: ((i
// 5) + 1, (i
% 5) + 1)
955 for i
, k
in enumerate(cipher_alphabet
)}
956 r_grid
= {((i
// 5) + 1, (i
% 5) + 1): k
957 for i
, k
in enumerate(cipher_alphabet
)}
958 return translation
, f_grid
, r_grid
960 def bifid_encipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
,
961 letter_mapping
=None, period
=None, fillvalue
=None):
964 >>> bifid_encipher("indiajelly", 'iguana')
966 >>> bifid_encipher("indiacurry", 'iguana', period=4)
968 >>> bifid_encipher("indiacurry", 'iguana', period=4, fillvalue='x')
971 translation
, f_grid
, r_grid
= bifid_grid(keyword
, wrap_alphabet
, letter_mapping
)
973 t_message
= message
.translate(translation
)
974 pairs0
= [f_grid
[l
] for l
in sanitise(t_message
)]
976 chunked_pairs
= [pairs0
[i
:i
+period
] for i
in range(0, len(pairs0
), period
)]
977 if len(chunked_pairs
[-1]) < period
and fillvalue
:
978 chunked_pairs
[-1] += [f_grid
[fillvalue
]] * (period
- len(chunked_pairs
[-1]))
980 chunked_pairs
= [pairs0
]
983 for c
in chunked_pairs
:
984 items
= sum(list(list(i
) for i
in zip(*c
)), [])
985 p
= [(items
[i
], items
[i
+1]) for i
in range(0, len(items
), 2)]
988 return cat(r_grid
[p
] for p
in pairs1
)
991 def bifid_decipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
,
992 letter_mapping
=None, period
=None, fillvalue
=None):
993 """Decipher with bifid cipher
995 >>> bifid_decipher('ibidonhprm', 'iguana')
997 >>> bifid_decipher("ibnhgaqltm", 'iguana', period=4)
999 >>> bifid_decipher("ibnhgaqltzml", 'iguana', period=4)
1002 translation
, f_grid
, r_grid
= bifid_grid(keyword
, wrap_alphabet
, letter_mapping
)
1004 t_message
= message
.translate(translation
)
1005 pairs0
= [f_grid
[l
] for l
in sanitise(t_message
)]
1007 chunked_pairs
= [pairs0
[i
:i
+period
] for i
in range(0, len(pairs0
), period
)]
1008 if len(chunked_pairs
[-1]) < period
and fillvalue
:
1009 chunked_pairs
[-1] += [f_grid
[fillvalue
]] * (period
- len(chunked_pairs
[-1]))
1011 chunked_pairs
= [pairs0
]
1014 for c
in chunked_pairs
:
1015 items
= [j
for i
in c
for j
in i
]
1017 p
= [(items
[i
], items
[i
+gap
]) for i
in range(gap
)]
1020 return cat(r_grid
[p
] for p
in pairs1
)
1022 class PocketEnigma(object):
1023 """A pocket enigma machine
1024 The wheel is internally represented as a 26-element list self.wheel_map,
1025 where wheel_map[i] == j shows that the position i places on from the arrow
1026 maps to the position j places on.
1028 def __init__(self
, wheel
=1, position
='a'):
1029 """initialise the pocket enigma, including which wheel to use and the
1030 starting position of the wheel.
1032 The wheel is either 1 or 2 (the predefined wheels) or a list of letter
1035 The position is the letter pointed to by the arrow on the wheel.
1038 [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]
1042 self
.wheel1
= [('a', 'z'), ('b', 'e'), ('c', 'x'), ('d', 'k'),
1043 ('f', 'h'), ('g', 'j'), ('i', 'm'), ('l', 'r'), ('n', 'o'),
1044 ('p', 'v'), ('q', 't'), ('s', 'u'), ('w', 'y')]
1045 self
.wheel2
= [('a', 'c'), ('b', 'd'), ('e', 'w'), ('f', 'i'),
1046 ('g', 'p'), ('h', 'm'), ('j', 'k'), ('l', 'n'), ('o', 'q'),
1047 ('r', 'z'), ('s', 'u'), ('t', 'v'), ('x', 'y')]
1049 self
.make_wheel_map(self
.wheel1
)
1051 self
.make_wheel_map(self
.wheel2
)
1053 self
.validate_wheel_spec(wheel
)
1054 self
.make_wheel_map(wheel
)
1055 if position
in string
.ascii_lowercase
:
1056 self
.position
= ord(position
) - ord('a')
1058 self
.position
= position
1060 def make_wheel_map(self
, wheel_spec
):
1061 """Expands a wheel specification from a list of letter-letter pairs
1062 into a full wheel_map.
1064 >>> pe.make_wheel_map(pe.wheel2)
1065 [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]
1067 self
.validate_wheel_spec(wheel_spec
)
1068 self
.wheel_map
= [0] * 26
1069 for p
in wheel_spec
:
1070 self
.wheel_map
[ord(p
[0]) - ord('a')] = ord(p
[1]) - ord('a')
1071 self
.wheel_map
[ord(p
[1]) - ord('a')] = ord(p
[0]) - ord('a')
1072 return self
.wheel_map
1074 def validate_wheel_spec(self
, wheel_spec
):
1075 """Validates that a wheel specificaiton will turn into a valid wheel
1078 >>> pe.validate_wheel_spec([])
1079 Traceback (most recent call last):
1081 ValueError: Wheel specification has 0 pairs, requires 13
1082 >>> pe.validate_wheel_spec([('a', 'b', 'c')]*13)
1083 Traceback (most recent call last):
1085 ValueError: Not all mappings in wheel specificationhave two elements
1086 >>> pe.validate_wheel_spec([('a', 'b')]*13)
1087 Traceback (most recent call last):
1089 ValueError: Wheel specification does not contain 26 letters
1091 if len(wheel_spec
) != 13:
1092 raise ValueError("Wheel specification has {} pairs, requires 13".
1093 format(len(wheel_spec
)))
1094 for p
in wheel_spec
:
1096 raise ValueError("Not all mappings in wheel specification"
1097 "have two elements")
1098 if len(set([p
[0] for p
in wheel_spec
] +
1099 [p
[1] for p
in wheel_spec
])) != 26:
1100 raise ValueError("Wheel specification does not contain 26 letters")
1102 def encipher_letter(self
, letter
):
1103 """Enciphers a single letter, by advancing the wheel before looking up
1104 the letter on the wheel.
1106 >>> pe.set_position('f')
1108 >>> pe.encipher_letter('k')
1112 return self
.lookup(letter
)
1113 decipher_letter
= encipher_letter
1115 def lookup(self
, letter
):
1116 """Look up what a letter enciphers to, without turning the wheel.
1118 >>> pe.set_position('f')
1120 >>> cat([pe.lookup(l) for l in string.ascii_lowercase])
1121 'udhbfejcpgmokrliwntsayqzvx'
1125 if letter
in string
.ascii_lowercase
:
1127 (self
.wheel_map
[(ord(letter
) - ord('a') - self
.position
) % 26] +
1128 self
.position
) % 26 +
1134 """Advances the wheel one position.
1136 >>> pe.set_position('f')
1141 self
.position
= (self
.position
+ 1) % 26
1142 return self
.position
1144 def encipher(self
, message
, starting_position
=None):
1145 """Enciphers a whole message.
1147 >>> pe.set_position('f')
1149 >>> pe.encipher('helloworld')
1151 >>> pe.set_position('f')
1153 >>> pe.encipher('kjsglcjoqc')
1155 >>> pe.encipher('helloworld', starting_position = 'x')
1158 if starting_position
:
1159 self
.set_position(starting_position
)
1162 transformed
+= self
.encipher_letter(l
)
1166 def set_position(self
, position
):
1167 """Sets the position of the wheel, by specifying the letter the arrow
1170 >>> pe.set_position('a')
1172 >>> pe.set_position('m')
1174 >>> pe.set_position('z')
1177 self
.position
= ord(position
) - ord('a')
1178 return self
.position
1181 if __name__
== "__main__":
1183 doctest
.testmod(extraglobs
={'pe': PocketEnigma(1, 'a')})