Finished for a bit
[cipher-tools.git] / cipher.py
index 865a1b9808e8c7f9f439243f822bd6738835901f..59eb3a6223ccea3814256e6f05b5d0c30b000cfa 100644 (file)
--- a/cipher.py
+++ b/cipher.py
@@ -1,8 +1,9 @@
 import string
 import collections
 import logging
 import string
 import collections
 import logging
-from itertools import zip_longest, cycle
-
+import math
+from itertools import zip_longest, cycle, chain
+from language_models import *
 
 logger = logging.getLogger(__name__)
 logger.addHandler(logging.FileHandler('cipher.log'))
 
 logger = logging.getLogger(__name__)
 logger.addHandler(logging.FileHandler('cipher.log'))
@@ -11,32 +12,12 @@ logger.setLevel(logging.WARNING)
 #logger.setLevel(logging.DEBUG)
 
 
 #logger.setLevel(logging.DEBUG)
 
 
-modular_division_table = [[0]*26 for x in range(26)]
+modular_division_table = [[0]*26 for _ in range(26)]
 for a in range(26):
     for b in range(26):
         c = (a * b) % 26
         modular_division_table[b][c] = a
 
 for a in range(26):
     for b in range(26):
         c = (a * b) % 26
         modular_division_table[b][c] = a
 
-def letters(text):
-    """Remove all non-alphabetic characters from a text
-    >>> letters('The Quick')
-    'TheQuick'
-    >>> letters('The Quick BROWN fox jumped! over... the (9lazy) DOG')
-    'TheQuickBROWNfoxjumpedoverthelazyDOG'
-    """
-    return ''.join([c for c in text if c in string.ascii_letters])
-
-def sanitise(text):
-    """Remove all non-alphabetic characters and convert the text to lowercase
-    
-    >>> sanitise('The Quick')
-    'thequick'
-    >>> sanitise('The Quick BROWN fox jumped! over... the (9lazy) DOG')
-    'thequickbrownfoxjumpedoverthelazydog'
-    """
-    # sanitised = [c.lower() for c in text if c in string.ascii_letters]
-    # return ''.join(sanitised)
-    return letters(text).lower()
 
 def ngrams(text, n):
     """Returns all n-grams of a text
 
 def ngrams(text, n):
     """Returns all n-grams of a text
@@ -80,6 +61,22 @@ def combine_every_nth(split_text):
     return ''.join([''.join(l) 
                     for l in zip_longest(*split_text, fillvalue='')])
 
     return ''.join([''.join(l) 
                     for l in zip_longest(*split_text, fillvalue='')])
 
+def chunks(text, n, fillvalue=None):
+    """Split a text into chunks of n characters
+
+    >>> chunks('abcdefghi', 3)
+    ['abc', 'def', 'ghi']
+    >>> chunks('abcdefghi', 4)
+    ['abcd', 'efgh', 'i']
+    >>> chunks('abcdefghi', 4, fillvalue='!')
+    ['abcd', 'efgh', 'i!!!']
+    """
+    if fillvalue:
+        padding = fillvalue[0] * (n - len(text) % n)
+    else:
+        padding = ''
+    return [(text+padding)[i:i+n] for i in range(0, len(text), n)]
+
 def transpose(items, transposition):
     """Moves items around according to the given transposition
     
 def transpose(items, transposition):
     """Moves items around according to the given transposition
     
@@ -110,13 +107,10 @@ def untranspose(items, transposition):
        transposed[t] = items[p]
     return transposed
 
        transposed[t] = items[p]
     return transposed
 
-
-
 def deduplicate(text):
     return list(collections.OrderedDict.fromkeys(text))
 
 
 def deduplicate(text):
     return list(collections.OrderedDict.fromkeys(text))
 
 
-
 def caesar_encipher_letter(letter, shift):
     """Encipher a letter, given a shift amount
 
 def caesar_encipher_letter(letter, shift):
     """Encipher a letter, given a shift amount
 
@@ -224,8 +218,9 @@ def affine_decipher_letter(letter, multiplier=1, adder=0, one_based=True):
             alphabet_start = ord('a')
         cipher_number = ord(letter) - alphabet_start
         if one_based: cipher_number += 1
             alphabet_start = ord('a')
         cipher_number = ord(letter) - alphabet_start
         if one_based: cipher_number += 1
-        plaintext_number = ( modular_division_table[multiplier]
-                                                   [(cipher_number - adder) % 26] )
+        plaintext_number = ( 
+            modular_division_table[multiplier]
+                                  [(cipher_number - adder) % 26] )
         if one_based: plaintext_number -= 1
         return chr(plaintext_number % 26 + alphabet_start) 
     else:
         if one_based: plaintext_number -= 1
         return chr(plaintext_number % 26 + alphabet_start) 
     else:
@@ -330,46 +325,29 @@ def keyword_decipher(message, keyword, wrap_alphabet=0):
     cipher_translation = ''.maketrans(cipher_alphabet, string.ascii_lowercase)
     return message.lower().translate(cipher_translation)
 
     cipher_translation = ''.maketrans(cipher_alphabet, string.ascii_lowercase)
     return message.lower().translate(cipher_translation)
 
-def scytale_encipher(message, rows):
-    """Enciphers using the scytale transposition cipher.
-    Message is padded with spaces to allow all rows to be the same length.
 
 
-    >>> scytale_encipher('thequickbrownfox', 3)
-    'tcnhkfeboqrxuo iw '
-    >>> scytale_encipher('thequickbrownfox', 4)
-    'tubnhirfecooqkwx'
-    >>> scytale_encipher('thequickbrownfox', 5)
-    'tubn hirf ecoo qkwx '
-    >>> scytale_encipher('thequickbrownfox', 6)
-    'tqcrnxhukof eibwo '
-    >>> scytale_encipher('thequickbrownfox', 7)
-    'tqcrnx hukof  eibwo  '
+def vigenere_encipher(message, keyword):
+    """Vigenere encipher
+
+    >>> vigenere_encipher('hello', 'abc')
+    'hfnlp'
     """
     """
-    if len(message) % rows != 0:
-        message += ' '*(rows - len(message) % rows)
-    row_length = round(len(message) / rows)
-    slices = [message[i:i+row_length] 
-              for i in range(0, len(message), row_length)]
-    return ''.join([''.join(r) for r in zip_longest(*slices, fillvalue='')])
+    shifts = [ord(l) - ord('a') for l in sanitise(keyword)]
+    pairs = zip(message, cycle(shifts))
+    return ''.join([caesar_encipher_letter(l, k) for l, k in pairs])
 
 
-def scytale_decipher(message, rows):
-    """Deciphers using the scytale transposition cipher.
-    Assumes the message is padded so that all rows are the same length.
-    
-    >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
-    'thequickbrownfox  '
-    >>> scytale_decipher('tubnhirfecooqkwx', 4)
-    'thequickbrownfox'
-    >>> scytale_decipher('tubn hirf ecoo qkwx ', 5)
-    'thequickbrownfox    '
-    >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
-    'thequickbrownfox  '
-    >>> scytale_decipher('tqcrnx hukof  eibwo  ', 7)
-    'thequickbrownfox     '
+def vigenere_decipher(message, keyword):
+    """Vigenere decipher
+
+    >>> vigenere_decipher('hfnlp', 'abc')
+    'hello'
     """
     """
-    cols = round(len(message) / rows)
-    columns = [message[i:i+rows] for i in range(0, cols * rows, rows)]
-    return ''.join([''.join(c) for c in zip_longest(*columns, fillvalue='')])
+    shifts = [ord(l) - ord('a') for l in sanitise(keyword)]
+    pairs = zip(message, cycle(shifts))
+    return ''.join([caesar_decipher_letter(l, k) for l, k in pairs])
+
+beaufort_encipher=vigenere_decipher
+beaufort_decipher=vigenere_encipher
 
 
 def transpositions_of(keyword):
 
 
 def transpositions_of(keyword):
@@ -394,70 +372,138 @@ def transpositions_of(keyword):
         transpositions = tuple(key.index(l) for l in sorted(key))
         return transpositions
 
         transpositions = tuple(key.index(l) for l in sorted(key))
         return transpositions
 
-def column_transposition_encipher(message, keyword, fillvalue=' '):
+def pad(message_len, group_len, fillvalue):
+    padding_length = group_len - message_len % group_len
+    if padding_length == group_len: padding_length = 0
+    padding = ''
+    for i in range(padding_length):
+        if callable(fillvalue):
+            padding += fillvalue()
+        else:
+            padding += fillvalue
+    return padding
+
+def column_transposition_encipher(message, keyword, fillvalue=' ', 
+      fillcolumnwise=False,
+      emptycolumnwise=False):
     """Enciphers using the column transposition cipher.
     Message is padded to allow all rows to be the same length.
 
     """Enciphers using the column transposition cipher.
     Message is padded to allow all rows to be the same length.
 
-    >>> column_transposition_encipher('hellothere', 'clever')
+    >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True)
+    'hlohr eltee '
+    >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True, emptycolumnwise=True)
+    'hellothere  '
+    >>> column_transposition_encipher('hellothere', 'abcdef')
+    'hellothere  '
+    >>> column_transposition_encipher('hellothere', 'abcde')
+    'hellothere'
+    >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
+    'hellothere'
+    >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
+    'hlohreltee'
+    >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
+    'htehlelroe'
+    >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
+    'hellothere'
+    >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=True)
+    'heotllrehe'
+    >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=False)
+    'holrhetlee'
+    >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=True)
+    'htleehoelr'
+    >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=False)
     'hleolteher'
     'hleolteher'
+    >>> column_transposition_encipher('hellothere', 'cleverly')
+    'hleolthre e '
     >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
     'hleolthre!e!'
     >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
     'hleolthre!e!'
+    >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue=lambda: '*')
+    'hleolthre*e*'
     """
     """
-    return column_transposition_worker(message, keyword, encipher=True, 
-                                       fillvalue=fillvalue)
+    transpositions = transpositions_of(keyword)
+    message += pad(len(message), len(transpositions), fillvalue)
+    if fillcolumnwise:
+        rows = every_nth(message, len(message) // len(transpositions))
+    else:
+        rows = chunks(message, len(transpositions))
+    transposed = [transpose(r, transpositions) for r in rows]
+    if emptycolumnwise:
+        return combine_every_nth(transposed)
+    else:
+        return ''.join(chain(*transposed))
 
 
-def column_transposition_decipher(message, keyword, fillvalue=' '):
+def column_transposition_decipher(message, keyword, fillvalue=' ', 
+      fillcolumnwise=False,
+      emptycolumnwise=False):
     """Deciphers using the column transposition cipher.
     Message is padded to allow all rows to be the same length.
 
     """Deciphers using the column transposition cipher.
     Message is padded to allow all rows to be the same length.
 
-    >>> column_transposition_decipher('hleolteher', 'clever')
+    >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
     'hellothere'
     'hellothere'
-    >>> column_transposition_decipher('hleolthre!e!', 'cleverly', fillvalue='?')
-    'hellothere!!'
-    """
-    return column_transposition_worker(message, keyword, encipher=False, 
-                                       fillvalue=fillvalue)
-
-def column_transposition_worker(message, keyword, 
-                                encipher=True, fillvalue=' '):
-    """Does the actual work of the column transposition cipher.
-    Message is padded with spaces to allow all rows to be the same length.
-
-    >>> column_transposition_worker('hellothere', 'clever')
-    'hleolteher'
-    >>> column_transposition_worker('hellothere', 'clever', encipher=True)
-    'hleolteher'
-    >>> column_transposition_worker('hleolteher', 'clever', encipher=False)
+    >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
+    'hellothere'
+    >>> column_transposition_decipher('htehlelroe', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
+    'hellothere'
+    >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
+    'hellothere'
+    >>> column_transposition_decipher('heotllrehe', 'clever', fillcolumnwise=True, emptycolumnwise=True)
+    'hellothere'
+    >>> column_transposition_decipher('holrhetlee', 'clever', fillcolumnwise=True, emptycolumnwise=False)
+    'hellothere'
+    >>> column_transposition_decipher('htleehoelr', 'clever', fillcolumnwise=False, emptycolumnwise=True)
+    'hellothere'
+    >>> column_transposition_decipher('hleolteher', 'clever', fillcolumnwise=False, emptycolumnwise=False)
     'hellothere'
     """
     transpositions = transpositions_of(keyword)
     'hellothere'
     """
     transpositions = transpositions_of(keyword)
-    columns = every_nth(message, len(transpositions), fillvalue=fillvalue)
-    if encipher:
-        transposed_columns = transpose(columns, transpositions)
+    message += pad(len(message), len(transpositions), '*')
+    if emptycolumnwise:
+        rows = every_nth(message, len(message) // len(transpositions))
     else:
     else:
-        transposed_columns = untranspose(columns, transpositions)
-    return combine_every_nth(transposed_columns)
+        rows = chunks(message, len(transpositions))
+    untransposed = [untranspose(r, transpositions) for r in rows]
+    if fillcolumnwise:
+        return combine_every_nth(untransposed)
+    else:
+        return ''.join(chain(*untransposed))
 
 
-def vigenere_encipher(message, keyword):
-    """Vigenere encipher
+def scytale_encipher(message, rows, fillvalue=' '):
+    """Enciphers using the scytale transposition cipher.
+    Message is padded with spaces to allow all rows to be the same length.
 
 
-    >>> vigenere_encipher('hello', 'abc')
-    'hfnlp'
+    >>> scytale_encipher('thequickbrownfox', 3)
+    'tcnhkfeboqrxuo iw '
+    >>> scytale_encipher('thequickbrownfox', 4)
+    'tubnhirfecooqkwx'
+    >>> scytale_encipher('thequickbrownfox', 5)
+    'tubnhirfecooqkwx'
+    >>> scytale_encipher('thequickbrownfox', 6)
+    'tqcrnxhukof eibwo '
+    >>> scytale_encipher('thequickbrownfox', 7)
+    'tqcrnxhukof eibwo '
     """
     """
-    shifts = [ord(l) - ord('a') for l in sanitise(keyword)]
-    pairs = zip(message, cycle(shifts))
-    return ''.join([caesar_encipher_letter(l, k) for l, k in pairs])
-
-def vigenere_decipher(message, keyword):
-    """Vigenere decipher
+    transpositions = [i for i in range(math.ceil(len(message) / rows))]
+    return column_transposition_encipher(message, transpositions, 
+        fillcolumnwise=False, emptycolumnwise=True)
 
 
-    >>> vigenere_decipher('hfnlp', 'abc')
-    'hello'
+def scytale_decipher(message, rows):
+    """Deciphers using the scytale transposition cipher.
+    Assumes the message is padded so that all rows are the same length.
+    
+    >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
+    'thequickbrownfox  '
+    >>> scytale_decipher('tubnhirfecooqkwx', 4)
+    'thequickbrownfox'
+    >>> scytale_decipher('tubnhirfecooqkwx', 5)
+    'thequickbrownfox'
+    >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
+    'thequickbrownfox  '
+    >>> scytale_decipher('tqcrnxhukof eibwo ', 7)
+    'thequickbrownfox  '
     """
     """
-    shifts = [ord(l) - ord('a') for l in sanitise(keyword)]
-    pairs = zip(message, cycle(shifts))
-    return ''.join([caesar_decipher_letter(l, k) for l, k in pairs])
-
+    transpositions = [i for i in range(math.ceil(len(message) / rows))]
+    return column_transposition_decipher(message, transpositions, 
+        fillcolumnwise=False, emptycolumnwise=True)
 
 
 if __name__ == "__main__":
 
 
 if __name__ == "__main__":