Cleaner way of dealing with punctuation in C1b
[cipher-training.git] / cipher.py
1 import string
2 import collections
3 import math
4 from enum import Enum
5 from itertools import zip_longest, cycle, chain, count
6 import numpy as np
7 from numpy import matrix
8 from numpy import linalg
9 from language_models import *
10
11
12 ## Utility functions
13 cat = ''.join
14 wcat = ' '.join
15
16 def pos(letter):
17 if letter in string.ascii_lowercase:
18 return ord(letter) - ord('a')
19 elif letter in string.ascii_uppercase:
20 return ord(letter) - ord('A')
21 else:
22 return ''
23
24 def unpos(number): return chr(number % 26 + ord('a'))
25
26
27 modular_division_table = [[0]*26 for _ in range(26)]
28 for a in range(26):
29 for b in range(26):
30 c = (a * b) % 26
31 modular_division_table[b][c] = a
32
33
34 def every_nth(text, n, fillvalue=''):
35 """Returns n strings, each of which consists of every nth character,
36 starting with the 0th, 1st, 2nd, ... (n-1)th character
37
38 >>> every_nth(string.ascii_lowercase, 5)
39 ['afkpuz', 'bglqv', 'chmrw', 'dinsx', 'ejoty']
40 >>> every_nth(string.ascii_lowercase, 1)
41 ['abcdefghijklmnopqrstuvwxyz']
42 >>> every_nth(string.ascii_lowercase, 26) # doctest: +NORMALIZE_WHITESPACE
43 ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
44 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
45 >>> every_nth(string.ascii_lowercase, 5, fillvalue='!')
46 ['afkpuz', 'bglqv!', 'chmrw!', 'dinsx!', 'ejoty!']
47 """
48 split_text = chunks(text, n, fillvalue)
49 return [cat(l) for l in zip_longest(*split_text, fillvalue=fillvalue)]
50
51 def combine_every_nth(split_text):
52 """Reforms a text split into every_nth strings
53
54 >>> combine_every_nth(every_nth(string.ascii_lowercase, 5))
55 'abcdefghijklmnopqrstuvwxyz'
56 >>> combine_every_nth(every_nth(string.ascii_lowercase, 1))
57 'abcdefghijklmnopqrstuvwxyz'
58 >>> combine_every_nth(every_nth(string.ascii_lowercase, 26))
59 'abcdefghijklmnopqrstuvwxyz'
60 """
61 return cat([cat(l)
62 for l in zip_longest(*split_text, fillvalue='')])
63
64 def chunks(text, n, fillvalue=None):
65 """Split a text into chunks of n characters
66
67 >>> chunks('abcdefghi', 3)
68 ['abc', 'def', 'ghi']
69 >>> chunks('abcdefghi', 4)
70 ['abcd', 'efgh', 'i']
71 >>> chunks('abcdefghi', 4, fillvalue='!')
72 ['abcd', 'efgh', 'i!!!']
73 """
74 if fillvalue:
75 padding = fillvalue[0] * (n - len(text) % n)
76 else:
77 padding = ''
78 return [(text+padding)[i:i+n] for i in range(0, len(text), n)]
79
80 def transpose(items, transposition):
81 """Moves items around according to the given transposition
82
83 >>> transpose(['a', 'b', 'c', 'd'], (0,1,2,3))
84 ['a', 'b', 'c', 'd']
85 >>> transpose(['a', 'b', 'c', 'd'], (3,1,2,0))
86 ['d', 'b', 'c', 'a']
87 >>> transpose([10,11,12,13,14,15], (3,2,4,1,5,0))
88 [13, 12, 14, 11, 15, 10]
89 """
90 transposed = [''] * len(transposition)
91 for p, t in enumerate(transposition):
92 transposed[p] = items[t]
93 return transposed
94
95 def untranspose(items, transposition):
96 """Undoes a transpose
97
98 >>> untranspose(['a', 'b', 'c', 'd'], [0,1,2,3])
99 ['a', 'b', 'c', 'd']
100 >>> untranspose(['d', 'b', 'c', 'a'], [3,1,2,0])
101 ['a', 'b', 'c', 'd']
102 >>> untranspose([13, 12, 14, 11, 15, 10], [3,2,4,1,5,0])
103 [10, 11, 12, 13, 14, 15]
104 """
105 transposed = [''] * len(transposition)
106 for p, t in enumerate(transposition):
107 transposed[t] = items[p]
108 return transposed
109
110 def deduplicate(text):
111 return list(collections.OrderedDict.fromkeys(text))
112
113
114 def caesar_encipher_letter(accented_letter, shift):
115 """Encipher a letter, given a shift amount
116
117 >>> caesar_encipher_letter('a', 1)
118 'b'
119 >>> caesar_encipher_letter('a', 2)
120 'c'
121 >>> caesar_encipher_letter('b', 2)
122 'd'
123 >>> caesar_encipher_letter('x', 2)
124 'z'
125 >>> caesar_encipher_letter('y', 2)
126 'a'
127 >>> caesar_encipher_letter('z', 2)
128 'b'
129 >>> caesar_encipher_letter('z', -1)
130 'y'
131 >>> caesar_encipher_letter('a', -1)
132 'z'
133 >>> caesar_encipher_letter('A', 1)
134 'B'
135 >>> caesar_encipher_letter('é', 1)
136 'f'
137 """
138 # letter = unaccent(accented_letter)
139 # if letter in string.ascii_letters:
140 # if letter in string.ascii_uppercase:
141 # alphabet_start = ord('A')
142 # else:
143 # alphabet_start = ord('a')
144 # return chr(((ord(letter) - alphabet_start + shift) % 26) +
145 # alphabet_start)
146 # else:
147 # return letter
148
149 letter = unaccent(accented_letter)
150 if letter in string.ascii_letters:
151 cipherletter = unpos(pos(letter) + shift)
152 if letter in string.ascii_uppercase:
153 return cipherletter.upper()
154 else:
155 return cipherletter
156 else:
157 return letter
158
159 def caesar_decipher_letter(letter, shift):
160 """Decipher a letter, given a shift amount
161
162 >>> caesar_decipher_letter('b', 1)
163 'a'
164 >>> caesar_decipher_letter('b', 2)
165 'z'
166 """
167 return caesar_encipher_letter(letter, -shift)
168
169 def caesar_encipher(message, shift):
170 """Encipher a message with the Caesar cipher of given shift
171
172 >>> caesar_encipher('abc', 1)
173 'bcd'
174 >>> caesar_encipher('abc', 2)
175 'cde'
176 >>> caesar_encipher('abcxyz', 2)
177 'cdezab'
178 >>> caesar_encipher('ab cx yz', 2)
179 'cd ez ab'
180 >>> caesar_encipher('Héllo World!', 2)
181 'Jgnnq Yqtnf!'
182 """
183 enciphered = [caesar_encipher_letter(l, shift) for l in message]
184 return cat(enciphered)
185
186 def caesar_decipher(message, shift):
187 """Decipher a message with the Caesar cipher of given shift
188
189 >>> caesar_decipher('bcd', 1)
190 'abc'
191 >>> caesar_decipher('cde', 2)
192 'abc'
193 >>> caesar_decipher('cd ez ab', 2)
194 'ab cx yz'
195 >>> caesar_decipher('Jgnnq Yqtnf!', 2)
196 'Hello World!'
197 """
198 return caesar_encipher(message, -shift)
199
200 def affine_encipher_letter(accented_letter, multiplier=1, adder=0, one_based=True):
201 """Encipher a letter, given a multiplier and adder
202
203 >>> cat(affine_encipher_letter(l, 3, 5, True) \
204 for l in string.ascii_letters)
205 'hknqtwzcfiloruxadgjmpsvybeHKNQTWZCFILORUXADGJMPSVYBE'
206 >>> cat(affine_encipher_letter(l, 3, 5, False) \
207 for l in string.ascii_letters)
208 'filoruxadgjmpsvybehknqtwzcFILORUXADGJMPSVYBEHKNQTWZC'
209 """
210 # letter = unaccent(accented_letter)
211 # if letter in string.ascii_letters:
212 # if letter in string.ascii_uppercase:
213 # alphabet_start = ord('A')
214 # else:
215 # alphabet_start = ord('a')
216 # letter_number = ord(letter) - alphabet_start
217 # if one_based: letter_number += 1
218 # cipher_number = (letter_number * multiplier + adder) % 26
219 # if one_based: cipher_number -= 1
220 # return chr(cipher_number % 26 + alphabet_start)
221 # else:
222 # return letter
223 letter = unaccent(accented_letter)
224 if letter in string.ascii_letters:
225 letter_number = pos(letter)
226 if one_based: letter_number += 1
227 cipher_number = (letter_number * multiplier + adder) % 26
228 if one_based: cipher_number -= 1
229 if letter in string.ascii_uppercase:
230 return unpos(cipher_number).upper()
231 else:
232 return unpos(cipher_number)
233 else:
234 return letter
235
236 def affine_decipher_letter(letter, multiplier=1, adder=0, one_based=True):
237 """Encipher a letter, given a multiplier and adder
238
239 >>> cat(affine_decipher_letter(l, 3, 5, True) \
240 for l in 'hknqtwzcfiloruxadgjmpsvybeHKNQTWZCFILORUXADGJMPSVYBE')
241 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
242 >>> cat(affine_decipher_letter(l, 3, 5, False) \
243 for l in 'filoruxadgjmpsvybehknqtwzcFILORUXADGJMPSVYBEHKNQTWZC')
244 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
245 """
246 # if letter in string.ascii_letters:
247 # if letter in string.ascii_uppercase:
248 # alphabet_start = ord('A')
249 # else:
250 # alphabet_start = ord('a')
251 # cipher_number = ord(letter) - alphabet_start
252 # if one_based: cipher_number += 1
253 # plaintext_number = (
254 # modular_division_table[multiplier]
255 # [(cipher_number - adder) % 26])
256 # if one_based: plaintext_number -= 1
257 # return chr(plaintext_number % 26 + alphabet_start)
258 # else:
259 # return letter
260 if letter in string.ascii_letters:
261 cipher_number = pos(letter)
262 if one_based: cipher_number += 1
263 plaintext_number = (
264 modular_division_table[multiplier]
265 [(cipher_number - adder) % 26])
266 if one_based: plaintext_number -= 1
267 if letter in string.ascii_uppercase:
268 return unpos(plaintext_number).upper()
269 else:
270 return unpos(plaintext_number)
271 else:
272 return letter
273
274 def affine_encipher(message, multiplier=1, adder=0, one_based=True):
275 """Encipher a message
276
277 >>> affine_encipher('hours passed during which jerico tried every ' \
278 'trick he could think of', 15, 22, True)
279 'lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh'
280 """
281 enciphered = [affine_encipher_letter(l, multiplier, adder, one_based)
282 for l in message]
283 return cat(enciphered)
284
285 def affine_decipher(message, multiplier=1, adder=0, one_based=True):
286 """Decipher a message
287
288 >>> affine_decipher('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg ' \
289 'jfaoe ls omytd jlaxe mh', 15, 22, True)
290 'hours passed during which jerico tried every trick he could think of'
291 """
292 enciphered = [affine_decipher_letter(l, multiplier, adder, one_based)
293 for l in message]
294 return cat(enciphered)
295
296
297 class KeywordWrapAlphabet(Enum):
298 from_a = 1
299 from_last = 2
300 from_largest = 3
301
302
303 def keyword_cipher_alphabet_of(keyword, wrap_alphabet=KeywordWrapAlphabet.from_a):
304 """Find the cipher alphabet given a keyword.
305 wrap_alphabet controls how the rest of the alphabet is added
306 after the keyword.
307
308 >>> keyword_cipher_alphabet_of('bayes')
309 'bayescdfghijklmnopqrtuvwxz'
310 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_a)
311 'bayescdfghijklmnopqrtuvwxz'
312 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_last)
313 'bayestuvwxzcdfghijklmnopqr'
314 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_largest)
315 'bayeszcdfghijklmnopqrtuvwx'
316 """
317 if wrap_alphabet == KeywordWrapAlphabet.from_a:
318 cipher_alphabet = cat(deduplicate(sanitise(keyword) +
319 string.ascii_lowercase))
320 else:
321 if wrap_alphabet == KeywordWrapAlphabet.from_last:
322 last_keyword_letter = deduplicate(sanitise(keyword))[-1]
323 else:
324 last_keyword_letter = sorted(sanitise(keyword))[-1]
325 last_keyword_position = string.ascii_lowercase.find(
326 last_keyword_letter) + 1
327 cipher_alphabet = cat(
328 deduplicate(sanitise(keyword) +
329 string.ascii_lowercase[last_keyword_position:] +
330 string.ascii_lowercase))
331 return cipher_alphabet
332
333
334 def keyword_encipher(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a):
335 """Enciphers a message with a keyword substitution cipher.
336 wrap_alphabet controls how the rest of the alphabet is added
337 after the keyword.
338 0 : from 'a'
339 1 : from the last letter in the sanitised keyword
340 2 : from the largest letter in the sanitised keyword
341
342 >>> keyword_encipher('test message', 'bayes')
343 'rsqr ksqqbds'
344 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_a)
345 'rsqr ksqqbds'
346 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_last)
347 'lskl dskkbus'
348 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_largest)
349 'qspq jsppbcs'
350 """
351 cipher_alphabet = keyword_cipher_alphabet_of(keyword, wrap_alphabet)
352 cipher_translation = ''.maketrans(string.ascii_lowercase, cipher_alphabet)
353 return unaccent(message).lower().translate(cipher_translation)
354
355 def keyword_decipher(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a):
356 """Deciphers a message with a keyword substitution cipher.
357 wrap_alphabet controls how the rest of the alphabet is added
358 after the keyword.
359 0 : from 'a'
360 1 : from the last letter in the sanitised keyword
361 2 : from the largest letter in the sanitised keyword
362
363 >>> keyword_decipher('rsqr ksqqbds', 'bayes')
364 'test message'
365 >>> keyword_decipher('rsqr ksqqbds', 'bayes', KeywordWrapAlphabet.from_a)
366 'test message'
367 >>> keyword_decipher('lskl dskkbus', 'bayes', KeywordWrapAlphabet.from_last)
368 'test message'
369 >>> keyword_decipher('qspq jsppbcs', 'bayes', KeywordWrapAlphabet.from_largest)
370 'test message'
371 """
372 cipher_alphabet = keyword_cipher_alphabet_of(keyword, wrap_alphabet)
373 cipher_translation = ''.maketrans(cipher_alphabet, string.ascii_lowercase)
374 return message.lower().translate(cipher_translation)
375
376
377 def vigenere_encipher(message, keyword):
378 """Vigenere encipher
379
380 >>> vigenere_encipher('hello', 'abc')
381 'hfnlp'
382 """
383 shifts = [ord(l) - ord('a') for l in sanitise(keyword)]
384 pairs = zip(message, cycle(shifts))
385 return cat([caesar_encipher_letter(l, k) for l, k in pairs])
386
387 def vigenere_decipher(message, keyword):
388 """Vigenere decipher
389
390 >>> vigenere_decipher('hfnlp', 'abc')
391 'hello'
392 """
393 shifts = [ord(l) - ord('a') for l in sanitise(keyword)]
394 pairs = zip(message, cycle(shifts))
395 return cat([caesar_decipher_letter(l, k) for l, k in pairs])
396
397 beaufort_encipher=vigenere_decipher
398 beaufort_decipher=vigenere_encipher
399
400
401 def transpositions_of(keyword):
402 """Finds the transpostions given by a keyword. For instance, the keyword
403 'clever' rearranges to 'celrv', so the first column (0) stays first, the
404 second column (1) moves to third, the third column (2) moves to second,
405 and so on.
406
407 If passed a tuple, assume it's already a transposition and just return it.
408
409 >>> transpositions_of('clever')
410 (0, 2, 1, 4, 3)
411 >>> transpositions_of('fred')
412 (3, 2, 0, 1)
413 >>> transpositions_of((3, 2, 0, 1))
414 (3, 2, 0, 1)
415 """
416 if isinstance(keyword, tuple):
417 return keyword
418 else:
419 key = deduplicate(keyword)
420 transpositions = tuple(key.index(l) for l in sorted(key))
421 return transpositions
422
423 def pad(message_len, group_len, fillvalue):
424 padding_length = group_len - message_len % group_len
425 if padding_length == group_len: padding_length = 0
426 padding = ''
427 for i in range(padding_length):
428 if callable(fillvalue):
429 padding += fillvalue()
430 else:
431 padding += fillvalue
432 return padding
433
434 def column_transposition_encipher(message, keyword, fillvalue=' ',
435 fillcolumnwise=False,
436 emptycolumnwise=False):
437 """Enciphers using the column transposition cipher.
438 Message is padded to allow all rows to be the same length.
439
440 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True)
441 'hlohr eltee '
442 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True, emptycolumnwise=True)
443 'hellothere '
444 >>> column_transposition_encipher('hellothere', 'abcdef')
445 'hellothere '
446 >>> column_transposition_encipher('hellothere', 'abcde')
447 'hellothere'
448 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
449 'hellothere'
450 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
451 'hlohreltee'
452 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
453 'htehlelroe'
454 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
455 'hellothere'
456 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=True)
457 'heotllrehe'
458 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=False)
459 'holrhetlee'
460 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=True)
461 'htleehoelr'
462 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=False)
463 'hleolteher'
464 >>> column_transposition_encipher('hellothere', 'cleverly')
465 'hleolthre e '
466 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
467 'hleolthre!e!'
468 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue=lambda: '*')
469 'hleolthre*e*'
470 """
471 transpositions = transpositions_of(keyword)
472 message += pad(len(message), len(transpositions), fillvalue)
473 if fillcolumnwise:
474 rows = every_nth(message, len(message) // len(transpositions))
475 else:
476 rows = chunks(message, len(transpositions))
477 transposed = [transpose(r, transpositions) for r in rows]
478 if emptycolumnwise:
479 return combine_every_nth(transposed)
480 else:
481 return cat(chain(*transposed))
482
483 def column_transposition_decipher(message, keyword, fillvalue=' ',
484 fillcolumnwise=False,
485 emptycolumnwise=False):
486 """Deciphers using the column transposition cipher.
487 Message is padded to allow all rows to be the same length.
488
489 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
490 'hellothere'
491 >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
492 'hellothere'
493 >>> column_transposition_decipher('htehlelroe', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
494 'hellothere'
495 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
496 'hellothere'
497 >>> column_transposition_decipher('heotllrehe', 'clever', fillcolumnwise=True, emptycolumnwise=True)
498 'hellothere'
499 >>> column_transposition_decipher('holrhetlee', 'clever', fillcolumnwise=True, emptycolumnwise=False)
500 'hellothere'
501 >>> column_transposition_decipher('htleehoelr', 'clever', fillcolumnwise=False, emptycolumnwise=True)
502 'hellothere'
503 >>> column_transposition_decipher('hleolteher', 'clever', fillcolumnwise=False, emptycolumnwise=False)
504 'hellothere'
505 """
506 transpositions = transpositions_of(keyword)
507 message += pad(len(message), len(transpositions), fillvalue)
508 if emptycolumnwise:
509 rows = every_nth(message, len(message) // len(transpositions))
510 else:
511 rows = chunks(message, len(transpositions))
512 untransposed = [untranspose(r, transpositions) for r in rows]
513 if fillcolumnwise:
514 return combine_every_nth(untransposed)
515 else:
516 return cat(chain(*untransposed))
517
518 def scytale_encipher(message, rows, fillvalue=' '):
519 """Enciphers using the scytale transposition cipher.
520 Message is padded with spaces to allow all rows to be the same length.
521
522 >>> scytale_encipher('thequickbrownfox', 3)
523 'tcnhkfeboqrxuo iw '
524 >>> scytale_encipher('thequickbrownfox', 4)
525 'tubnhirfecooqkwx'
526 >>> scytale_encipher('thequickbrownfox', 5)
527 'tubn hirf ecoo qkwx '
528 >>> scytale_encipher('thequickbrownfox', 6)
529 'tqcrnxhukof eibwo '
530 >>> scytale_encipher('thequickbrownfox', 7)
531 'tqcrnx hukof eibwo '
532 """
533 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
534 # return column_transposition_encipher(message, transpositions,
535 # fillvalue=fillvalue, fillcolumnwise=False, emptycolumnwise=True)
536 transpositions = [i for i in range(rows)]
537 return column_transposition_encipher(message, transpositions,
538 fillvalue=fillvalue, fillcolumnwise=True, emptycolumnwise=False)
539
540 def scytale_decipher(message, rows):
541 """Deciphers using the scytale transposition cipher.
542 Assumes the message is padded so that all rows are the same length.
543
544 >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
545 'thequickbrownfox '
546 >>> scytale_decipher('tubnhirfecooqkwx', 4)
547 'thequickbrownfox'
548 >>> scytale_decipher('tubn hirf ecoo qkwx ', 5)
549 'thequickbrownfox '
550 >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
551 'thequickbrownfox '
552 >>> scytale_decipher('tqcrnx hukof eibwo ', 7)
553 'thequickbrownfox '
554 """
555 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
556 # return column_transposition_decipher(message, transpositions,
557 # fillcolumnwise=False, emptycolumnwise=True)
558 transpositions = [i for i in range(rows)]
559 return column_transposition_decipher(message, transpositions,
560 fillcolumnwise=True, emptycolumnwise=False)
561
562
563 def railfence_encipher(message, height, fillvalue=''):
564 """Railfence cipher.
565 Works by splitting the text into sections, then reading across them to
566 generate the rows in the cipher. The rows are then combined to form the
567 ciphertext.
568
569 Example: the plaintext "hellotherefriends", with a height of four, written
570 out in the railfence as
571 h h i
572 etere*
573 lorfns
574 l e d
575 (with the * showing the one character to finish the last section).
576 Each 'section' is two columns, but unfolded. In the example, the first
577 section is 'hellot'.
578
579 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 2, fillvalue='!')
580 'hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!'
581 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 3, fillvalue='!')
582 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihr!!lhateihsnefttiaece!'
583 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 5, fillvalue='!')
584 'hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!'
585 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 10, fillvalue='!')
586 'hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!'
587 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 3)
588 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece'
589 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 5)
590 'hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp'
591 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 7)
592 'haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic'
593 """
594 sections = chunks(message, (height - 1) * 2, fillvalue=fillvalue)
595 n_sections = len(sections)
596 # Add the top row
597 rows = [cat([s[0] for s in sections])]
598 # process the middle rows of the grid
599 for r in range(1, height-1):
600 rows += [cat([s[r:r+1] + s[height*2-r-2:height*2-r-1] for s in sections])]
601 # process the bottom row
602 rows += [cat([s[height - 1:height] for s in sections])]
603 # rows += [wcat([s[height - 1] for s in sections])]
604 return cat(rows)
605
606 def railfence_decipher(message, height, fillvalue=''):
607 """Railfence decipher.
608 Works by reconstructing the grid used to generate the ciphertext, then
609 unfolding the sections so the text can be concatenated together.
610
611 Example: given the ciphertext 'hhieterelorfnsled' and a height of 4, first
612 work out that the second row has a character missing, find the rows of the
613 grid, then split the section into its two columns.
614
615 'hhieterelorfnsled' is split into
616 h h i
617 etere
618 lorfns
619 l e d
620 (spaces added for clarity), which is stored in 'rows'. This is then split
621 into 'down_rows' and 'up_rows':
622
623 down_rows:
624 hhi
625 eee
626 lrn
627 led
628
629 up_rows:
630 tr
631 ofs
632
633 These are then zipped together (after the up_rows are reversed) to recover
634 the plaintext.
635
636 Most of the procedure is about finding the correct lengths for each row then
637 splitting the ciphertext into those rows.
638
639 >>> railfence_decipher('hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!', 2).strip('!')
640 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
641 >>> railfence_decipher('horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihr!!lhateihsnefttiaece!', 3).strip('!')
642 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
643 >>> railfence_decipher('hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!', 5).strip('!')
644 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
645 >>> railfence_decipher('hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!', 10).strip('!')
646 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
647 >>> railfence_decipher('horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece', 3)
648 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
649 >>> railfence_decipher('hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp', 5)
650 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
651 >>> railfence_decipher('haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic', 7)
652 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
653 """
654 # find the number and size of the sections, including how many characters
655 # are missing for a full grid
656 n_sections = math.ceil(len(message) / ((height - 1) * 2))
657 padding_to_add = n_sections * (height - 1) * 2 - len(message)
658 # row_lengths are for the both up rows and down rows
659 row_lengths = [n_sections] * (height - 1) * 2
660 for i in range((height - 1) * 2 - 1, (height - 1) * 2 - (padding_to_add + 1), -1):
661 row_lengths[i] -= 1
662 # folded_rows are the combined row lengths in the middle of the railfence
663 folded_row_lengths = [row_lengths[0]]
664 for i in range(1, height-1):
665 folded_row_lengths += [row_lengths[i] + row_lengths[-i]]
666 folded_row_lengths += [row_lengths[height - 1]]
667 # find the rows that form the railfence grid
668 rows = []
669 row_start = 0
670 for i in folded_row_lengths:
671 rows += [message[row_start:row_start + i]]
672 row_start += i
673 # split the rows into the 'down_rows' (those that form the first column of
674 # a section) and the 'up_rows' (those that ofrm the second column of a
675 # section).
676 down_rows = [rows[0]]
677 up_rows = []
678 for i in range(1, height-1):
679 down_rows += [cat([c for n, c in enumerate(rows[i]) if n % 2 == 0])]
680 up_rows += [cat([c for n, c in enumerate(rows[i]) if n % 2 == 1])]
681 down_rows += [rows[-1]]
682 up_rows.reverse()
683 return cat(c for r in zip_longest(*(down_rows + up_rows), fillvalue='') for c in r)
684
685 def make_cadenus_keycolumn(doubled_letters = 'vw', start='a', reverse=False):
686 """Makes the key column for a Cadenus cipher (the column down between the
687 rows of letters)
688
689 >>> make_cadenus_keycolumn()['a']
690 0
691 >>> make_cadenus_keycolumn()['b']
692 1
693 >>> make_cadenus_keycolumn()['c']
694 2
695 >>> make_cadenus_keycolumn()['v']
696 21
697 >>> make_cadenus_keycolumn()['w']
698 21
699 >>> make_cadenus_keycolumn()['z']
700 24
701 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['a']
702 1
703 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['b']
704 0
705 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['c']
706 24
707 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['i']
708 18
709 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['j']
710 18
711 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['v']
712 6
713 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['z']
714 2
715 """
716 index_to_remove = string.ascii_lowercase.find(doubled_letters[0])
717 short_alphabet = string.ascii_lowercase[:index_to_remove] + string.ascii_lowercase[index_to_remove+1:]
718 if reverse:
719 short_alphabet = cat(reversed(short_alphabet))
720 start_pos = short_alphabet.find(start)
721 rotated_alphabet = short_alphabet[start_pos:] + short_alphabet[:start_pos]
722 keycolumn = {l: i for i, l in enumerate(rotated_alphabet)}
723 keycolumn[doubled_letters[0]] = keycolumn[doubled_letters[1]]
724 return keycolumn
725
726 def cadenus_encipher(message, keyword, keycolumn, fillvalue='a'):
727 """Encipher with the Cadenus cipher
728
729 >>> cadenus_encipher(sanitise('Whoever has made a voyage up the Hudson ' \
730 'must remember the Kaatskill mountains. ' \
731 'They are a dismembered branch of the great'), \
732 'wink', \
733 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
734 'antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaasuvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned'
735 >>> cadenus_encipher(sanitise('a severe limitation on the usefulness of ' \
736 'the cadenus is that every message must be ' \
737 'a multiple of twenty-five letters long'), \
738 'easy', \
739 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
740 'systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtofarenuseieeieltarlmentieetogevesitfaisltngeeuvowul'
741 """
742 rows = chunks(message, len(message) // 25, fillvalue=fillvalue)
743 columns = zip(*rows)
744 rotated_columns = [col[start:] + col[:start] for start, col in zip([keycolumn[l] for l in keyword], columns)]
745 rotated_rows = zip(*rotated_columns)
746 transpositions = transpositions_of(keyword)
747 transposed = [transpose(r, transpositions) for r in rotated_rows]
748 return cat(chain(*transposed))
749
750 def cadenus_decipher(message, keyword, keycolumn, fillvalue='a'):
751 """
752 >>> cadenus_decipher('antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaa' \
753 'suvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned', \
754 'wink', \
755 make_cadenus_keycolumn(reverse=True))
756 'whoeverhasmadeavoyageupthehudsonmustrememberthekaatskillmountainstheyareadismemberedbranchofthegreat'
757 >>> cadenus_decipher('systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtof' \
758 'arenuseieeieltarlmentieetogevesitfaisltngeeuvowul', \
759 'easy', \
760 make_cadenus_keycolumn(reverse=True))
761 'aseverelimitationontheusefulnessofthecadenusisthateverymessagemustbeamultipleoftwentyfiveletterslong'
762 """
763 rows = chunks(message, len(message) // 25, fillvalue=fillvalue)
764 transpositions = transpositions_of(keyword)
765 untransposed_rows = [untranspose(r, transpositions) for r in rows]
766 columns = zip(*untransposed_rows)
767 rotated_columns = [col[-start:] + col[:-start] for start, col in zip([keycolumn[l] for l in keyword], columns)]
768 rotated_rows = zip(*rotated_columns)
769 # return rotated_columns
770 return cat(chain(*rotated_rows))
771
772
773 def hill_encipher(matrix, message_letters, fillvalue='a'):
774 """Hill cipher
775
776 >>> hill_encipher(np.matrix([[7,8], [11,11]]), 'hellothere')
777 'drjiqzdrvx'
778 >>> hill_encipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
779 'hello there')
780 'tfjflpznvyac'
781 """
782 n = len(matrix)
783 sanitised_message = sanitise(message_letters)
784 if len(sanitised_message) % n != 0:
785 padding = fillvalue[0] * (n - len(sanitised_message) % n)
786 else:
787 padding = ''
788 message = [ord(c) - ord('a') for c in sanitised_message + padding]
789 message_chunks = [message[i:i+n] for i in range(0, len(message), n)]
790 # message_chunks = chunks(message, len(matrix), fillvalue=None)
791 enciphered_chunks = [((matrix * np.matrix(c).T).T).tolist()[0]
792 for c in message_chunks]
793 return cat([chr(int(round(l)) % 26 + ord('a'))
794 for l in sum(enciphered_chunks, [])])
795
796 def hill_decipher(matrix, message, fillvalue='a'):
797 """Hill cipher
798
799 >>> hill_decipher(np.matrix([[7,8], [11,11]]), 'drjiqzdrvx')
800 'hellothere'
801 >>> hill_decipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
802 'tfjflpznvyac')
803 'hellothereaa'
804 """
805 adjoint = linalg.det(matrix)*linalg.inv(matrix)
806 inverse_determinant = modular_division_table[int(round(linalg.det(matrix))) % 26][1]
807 inverse_matrix = (inverse_determinant * adjoint) % 26
808 return hill_encipher(inverse_matrix, message, fillvalue)
809
810
811 # Where each piece of text ends up in the AMSCO transpositon cipher.
812 # 'index' shows where the slice appears in the plaintext, with the slice
813 # from 'start' to 'end'
814 AmscoSlice = collections.namedtuple('AmscoSlice', ['index', 'start', 'end'])
815
816 class AmscoFillStyle(Enum):
817 continuous = 1
818 same_each_row = 2
819 reverse_each_row = 3
820
821 def amsco_transposition_positions(message, keyword,
822 fillpattern=(1, 2),
823 fillstyle=AmscoFillStyle.continuous,
824 fillcolumnwise=False,
825 emptycolumnwise=True):
826 """Creates the grid for the AMSCO transposition cipher. Each element in the
827 grid shows the index of that slice and the start and end positions of the
828 plaintext that go to make it up.
829
830 >>> amsco_transposition_positions(string.ascii_lowercase, 'freddy', \
831 fillpattern=(1, 2)) # doctest: +NORMALIZE_WHITESPACE
832 [[AmscoSlice(index=3, start=4, end=6),
833 AmscoSlice(index=2, start=3, end=4),
834 AmscoSlice(index=0, start=0, end=1),
835 AmscoSlice(index=1, start=1, end=3),
836 AmscoSlice(index=4, start=6, end=7)],
837 [AmscoSlice(index=8, start=12, end=13),
838 AmscoSlice(index=7, start=10, end=12),
839 AmscoSlice(index=5, start=7, end=9),
840 AmscoSlice(index=6, start=9, end=10),
841 AmscoSlice(index=9, start=13, end=15)],
842 [AmscoSlice(index=13, start=19, end=21),
843 AmscoSlice(index=12, start=18, end=19),
844 AmscoSlice(index=10, start=15, end=16),
845 AmscoSlice(index=11, start=16, end=18),
846 AmscoSlice(index=14, start=21, end=22)],
847 [AmscoSlice(index=18, start=27, end=28),
848 AmscoSlice(index=17, start=25, end=27),
849 AmscoSlice(index=15, start=22, end=24),
850 AmscoSlice(index=16, start=24, end=25),
851 AmscoSlice(index=19, start=28, end=30)]]
852 """
853 transpositions = transpositions_of(keyword)
854 fill_iterator = cycle(fillpattern)
855 indices = count()
856 message_length = len(message)
857
858 current_position = 0
859 grid = []
860 current_fillpattern = fillpattern
861 while current_position < message_length:
862 row = []
863 if fillstyle == AmscoFillStyle.same_each_row:
864 fill_iterator = cycle(fillpattern)
865 if fillstyle == AmscoFillStyle.reverse_each_row:
866 fill_iterator = cycle(current_fillpattern)
867 for _ in range(len(transpositions)):
868 index = next(indices)
869 gap = next(fill_iterator)
870 row += [AmscoSlice(index, current_position, current_position + gap)]
871 current_position += gap
872 grid += [row]
873 if fillstyle == AmscoFillStyle.reverse_each_row:
874 current_fillpattern = list(reversed(current_fillpattern))
875 return [transpose(r, transpositions) for r in grid]
876
877 def amsco_transposition_encipher(message, keyword,
878 fillpattern=(1,2), fillstyle=AmscoFillStyle.reverse_each_row):
879 """AMSCO transposition encipher.
880
881 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(1, 2))
882 'hoteelhler'
883 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(2, 1))
884 'hetelhelor'
885 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(1, 2))
886 'hotelerelh'
887 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(2, 1))
888 'hetelorlhe'
889 >>> amsco_transposition_encipher('hereissometexttoencipher', 'encode')
890 'etecstthhomoerereenisxip'
891 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2))
892 'hetcsoeisterereipexthomn'
893 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
894 'hecsoisttererteipexhomen'
895 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(2, 1))
896 'heecisoosttrrtepeixhemen'
897 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2))
898 'hxtomephescieretoeisnter'
899 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
900 'hxomeiphscerettoisenteer'
901 """
902 grid = amsco_transposition_positions(message, keyword,
903 fillpattern=fillpattern, fillstyle=fillstyle)
904 ct_as_grid = [[message[s.start:s.end] for s in r] for r in grid]
905 return combine_every_nth(ct_as_grid)
906
907
908 def amsco_transposition_decipher(message, keyword,
909 fillpattern=(1,2), fillstyle=AmscoFillStyle.reverse_each_row):
910 """AMSCO transposition decipher
911
912 >>> amsco_transposition_decipher('hoteelhler', 'abc', fillpattern=(1, 2))
913 'hellothere'
914 >>> amsco_transposition_decipher('hetelhelor', 'abc', fillpattern=(2, 1))
915 'hellothere'
916 >>> amsco_transposition_decipher('hotelerelh', 'acb', fillpattern=(1, 2))
917 'hellothere'
918 >>> amsco_transposition_decipher('hetelorlhe', 'acb', fillpattern=(2, 1))
919 'hellothere'
920 >>> amsco_transposition_decipher('etecstthhomoerereenisxip', 'encode')
921 'hereissometexttoencipher'
922 >>> amsco_transposition_decipher('hetcsoeisterereipexthomn', 'cipher', fillpattern=(1, 2))
923 'hereissometexttoencipher'
924 >>> amsco_transposition_decipher('hecsoisttererteipexhomen', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
925 'hereissometexttoencipher'
926 >>> amsco_transposition_decipher('heecisoosttrrtepeixhemen', 'cipher', fillpattern=(2, 1))
927 'hereissometexttoencipher'
928 >>> amsco_transposition_decipher('hxtomephescieretoeisnter', 'cipher', fillpattern=(1, 3, 2))
929 'hereissometexttoencipher'
930 >>> amsco_transposition_decipher('hxomeiphscerettoisenteer', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
931 'hereissometexttoencipher'
932 """
933
934 grid = amsco_transposition_positions(message, keyword,
935 fillpattern=fillpattern, fillstyle=fillstyle)
936 transposed_sections = [s for c in [l for l in zip(*grid)] for s in c]
937 plaintext_list = [''] * len(transposed_sections)
938 current_pos = 0
939 for slice in transposed_sections:
940 plaintext_list[slice.index] = message[current_pos:current_pos-slice.start+slice.end][:len(message[slice.start:slice.end])]
941 current_pos += len(message[slice.start:slice.end])
942 return cat(plaintext_list)
943
944
945 class PocketEnigma(object):
946 """A pocket enigma machine
947 The wheel is internally represented as a 26-element list self.wheel_map,
948 where wheel_map[i] == j shows that the position i places on from the arrow
949 maps to the position j places on.
950 """
951 def __init__(self, wheel=1, position='a'):
952 """initialise the pocket enigma, including which wheel to use and the
953 starting position of the wheel.
954
955 The wheel is either 1 or 2 (the predefined wheels) or a list of letter
956 pairs.
957
958 The position is the letter pointed to by the arrow on the wheel.
959
960 >>> pe.wheel_map
961 [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]
962 >>> pe.position
963 0
964 """
965 self.wheel1 = [('a', 'z'), ('b', 'e'), ('c', 'x'), ('d', 'k'),
966 ('f', 'h'), ('g', 'j'), ('i', 'm'), ('l', 'r'), ('n', 'o'),
967 ('p', 'v'), ('q', 't'), ('s', 'u'), ('w', 'y')]
968 self.wheel2 = [('a', 'c'), ('b', 'd'), ('e', 'w'), ('f', 'i'),
969 ('g', 'p'), ('h', 'm'), ('j', 'k'), ('l', 'n'), ('o', 'q'),
970 ('r', 'z'), ('s', 'u'), ('t', 'v'), ('x', 'y')]
971 if wheel == 1:
972 self.make_wheel_map(self.wheel1)
973 elif wheel == 2:
974 self.make_wheel_map(self.wheel2)
975 else:
976 self.validate_wheel_spec(wheel)
977 self.make_wheel_map(wheel)
978 if position in string.ascii_lowercase:
979 self.position = ord(position) - ord('a')
980 else:
981 self.position = position
982
983 def make_wheel_map(self, wheel_spec):
984 """Expands a wheel specification from a list of letter-letter pairs
985 into a full wheel_map.
986
987 >>> pe.make_wheel_map(pe.wheel2)
988 [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]
989 """
990 self.validate_wheel_spec(wheel_spec)
991 self.wheel_map = [0] * 26
992 for p in wheel_spec:
993 self.wheel_map[ord(p[0]) - ord('a')] = ord(p[1]) - ord('a')
994 self.wheel_map[ord(p[1]) - ord('a')] = ord(p[0]) - ord('a')
995 return self.wheel_map
996
997 def validate_wheel_spec(self, wheel_spec):
998 """Validates that a wheel specificaiton will turn into a valid wheel
999 map.
1000
1001 >>> pe.validate_wheel_spec([])
1002 Traceback (most recent call last):
1003 ...
1004 ValueError: Wheel specification has 0 pairs, requires 13
1005 >>> pe.validate_wheel_spec([('a', 'b', 'c')]*13)
1006 Traceback (most recent call last):
1007 ...
1008 ValueError: Not all mappings in wheel specificationhave two elements
1009 >>> pe.validate_wheel_spec([('a', 'b')]*13)
1010 Traceback (most recent call last):
1011 ...
1012 ValueError: Wheel specification does not contain 26 letters
1013 """
1014 if len(wheel_spec) != 13:
1015 raise ValueError("Wheel specification has {} pairs, requires 13".
1016 format(len(wheel_spec)))
1017 for p in wheel_spec:
1018 if len(p) != 2:
1019 raise ValueError("Not all mappings in wheel specification"
1020 "have two elements")
1021 if len(set([p[0] for p in wheel_spec] +
1022 [p[1] for p in wheel_spec])) != 26:
1023 raise ValueError("Wheel specification does not contain 26 letters")
1024
1025 def encipher_letter(self, letter):
1026 """Enciphers a single letter, by advancing the wheel before looking up
1027 the letter on the wheel.
1028
1029 >>> pe.set_position('f')
1030 5
1031 >>> pe.encipher_letter('k')
1032 'h'
1033 """
1034 self.advance()
1035 return self.lookup(letter)
1036 decipher_letter = encipher_letter
1037
1038 def lookup(self, letter):
1039 """Look up what a letter enciphers to, without turning the wheel.
1040
1041 >>> pe.set_position('f')
1042 5
1043 >>> cat([pe.lookup(l) for l in string.ascii_lowercase])
1044 'udhbfejcpgmokrliwntsayqzvx'
1045 >>> pe.lookup('A')
1046 ''
1047 """
1048 if letter in string.ascii_lowercase:
1049 return chr(
1050 (self.wheel_map[(ord(letter) - ord('a') - self.position) % 26] +
1051 self.position) % 26 +
1052 ord('a'))
1053 else:
1054 return ''
1055
1056 def advance(self):
1057 """Advances the wheel one position.
1058
1059 >>> pe.set_position('f')
1060 5
1061 >>> pe.advance()
1062 6
1063 """
1064 self.position = (self.position + 1) % 26
1065 return self.position
1066
1067 def encipher(self, message, starting_position=None):
1068 """Enciphers a whole message.
1069
1070 >>> pe.set_position('f')
1071 5
1072 >>> pe.encipher('helloworld')
1073 'kjsglcjoqc'
1074 >>> pe.set_position('f')
1075 5
1076 >>> pe.encipher('kjsglcjoqc')
1077 'helloworld'
1078 >>> pe.encipher('helloworld', starting_position = 'x')
1079 'egrekthnnf'
1080 """
1081 if starting_position:
1082 self.set_position(starting_position)
1083 transformed = ''
1084 for l in message:
1085 transformed += self.encipher_letter(l)
1086 return transformed
1087 decipher = encipher
1088
1089 def set_position(self, position):
1090 """Sets the position of the wheel, by specifying the letter the arrow
1091 points to.
1092
1093 >>> pe.set_position('a')
1094 0
1095 >>> pe.set_position('m')
1096 12
1097 >>> pe.set_position('z')
1098 25
1099 """
1100 self.position = ord(position) - ord('a')
1101 return self.position
1102
1103
1104 if __name__ == "__main__":
1105 import doctest
1106 doctest.testmod(extraglobs={'pe': PocketEnigma(1, 'a')})