Fixed railfence ciphers, done challenges 2014 4 and 5
[cipher-tools.git] / cipher.py
index f29151120b411ea2c051b1059c47edc1a5ff230f..6cce72543224f02468e8a7609b674d158d238127 100644 (file)
--- a/cipher.py
+++ b/cipher.py
@@ -451,7 +451,7 @@ def column_transposition_decipher(message, keyword, fillvalue=' ',
     'hellothere'
     """
     transpositions = transpositions_of(keyword)
-    message += pad(len(message), len(transpositions), '*')
+    message += pad(len(message), len(transpositions), fillvalue)
     if emptycolumnwise:
         rows = every_nth(message, len(message) // len(transpositions))
     else:
@@ -507,8 +507,21 @@ def scytale_decipher(message, rows):
         fillcolumnwise=True, emptycolumnwise=False)
 
 
-def railfence_encipher(message, height, fillvalue=' '):
-    """Railfence cipher
+def railfence_encipher(message, height, fillvalue=''):
+    """Railfence cipher.
+    Works by splitting the text into sections, then reading across them to
+    generate the rows in the cipher. The rows are then combined to form the
+    ciphertext.
+
+    Example: the plaintext "hellotherefriends", with a height of four, written 
+    out in the railfence as 
+       h h i
+       etere*
+       lorfns
+       l e d
+    (with the * showing the one character to finish the last section). 
+    Each 'section' is two columns, but unfolded. In the example, the first
+    section is 'hellot'.
 
     >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 2, fillvalue='!')
     'hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!'
@@ -518,19 +531,57 @@ def railfence_encipher(message, height, fillvalue=' '):
     'hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!'
     >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 10, fillvalue='!')
     'hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!'
+    >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 3)
+    'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece'
+    >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 5)
+    'hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp'
+    >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 7)
+    'haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic'
     """
     sections = chunks(message, (height - 1) * 2, fillvalue=fillvalue)
+    n_sections = len(sections)
     # Add the top row
-    rows = [s[0] for s in sections]
+    rows = [''.join([s[0] for s in sections])]
     # process the middle rows of the grid
-    for r in range(1, height - 1):
-        rows += [s[r] + s[-r] for s in sections]
+    for r in range(1, height-1):
+        rows += [''.join([s[r:r+1] + s[height*2-r-2:height*2-r-1] for s in sections])]
     # process the bottom row
-    rows += [s[height - 1] for s in sections]
+    rows += [''.join([s[height - 1:height] for s in sections])]
+    # rows += [' '.join([s[height - 1] for s in sections])]
     return ''.join(rows)
 
-def railfence_decipher(message, height):
-    """Railfence decipher. Assumes the message is already the correct length.
+def railfence_decipher(message, height, fillvalue=''):
+    """Railfence decipher. 
+    Works by reconstructing the grid used to generate the ciphertext, then
+    unfolding the sections so the text can be concatenated together.
+
+    Example: given the ciphertext 'hhieterelorfnsled' and a height of 4, first
+    work out that the second row has a character missing, find the rows of the
+    grid, then split the section into its two columns.
+
+    'hhieterelorfnsled' is split into
+        h h i
+        etere
+        lorfns
+        l e d
+    (spaces added for clarity), which is stored in 'rows'. This is then split
+    into 'down_rows' and 'up_rows':
+
+    down_rows:
+       hhi
+       eee
+       lrn
+       led
+
+    up_rows:
+       tr
+       ofs
+
+    These are then zipped together (after the up_rows are reversed) to recover 
+    the plaintext.
+
+    Most of the procedure is about finding the correct lengths for each row then
+    splitting the ciphertext into those rows.
 
     >>> railfence_decipher('hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!', 2).strip('!')
     'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
@@ -540,17 +591,43 @@ def railfence_decipher(message, height):
     'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
     >>> railfence_decipher('hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!', 10).strip('!')
     'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
+    >>> railfence_decipher('horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece', 3)
+    'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
+    >>> railfence_decipher('hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp', 5)
+    'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
+    >>> railfence_decipher('haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic', 7)
+    'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
     """
-    n_secs = len(message) // ((height - 1) * 2)
-    downrows = [message[:n_secs]]
-    uprows = []
-    for r in range(height-2):
-        midrow = message[(2 * r + 1) * n_secs:(2 * r + 1) * n_secs + n_secs * 2]
-        downrows += [''.join([midrow[i] for i in range(0, len(midrow), 2)])]
-        uprows = [''.join([midrow[i] for i in range(1, len(midrow), 2)])] + uprows
-    downrows += [message[-n_secs:]]
-    rows = downrows + uprows
-    return ''.join(letter for section in zip(*rows) for letter in section)
+    # find the number and size of the sections, including how many characters
+    #   are missing for a full grid
+    n_sections = math.ceil(len(message) / ((height - 1) * 2))
+    padding_to_add = n_sections * (height - 1) * 2 - len(message)
+    # row_lengths are for the both up rows and down rows
+    row_lengths = [n_sections] * (height - 1) * 2
+    for i in range((height - 1) * 2 - 1, (height - 1) * 2 - (padding_to_add + 1), -1):
+        row_lengths[i] -= 1
+    # folded_rows are the combined row lengths in the middle of the railfence
+    folded_row_lengths = [row_lengths[0]]
+    for i in range(1, height-1):
+        folded_row_lengths += [row_lengths[i] + row_lengths[-i]]
+    folded_row_lengths += [row_lengths[height - 1]]
+    # find the rows that form the railfence grid
+    rows = []
+    row_start = 0
+    for i in folded_row_lengths:
+        rows += [message[row_start:row_start + i]]
+        row_start += i
+    # split the rows into the 'down_rows' (those that form the first column of
+    #   a section) and the 'up_rows' (those that ofrm the second column of a 
+    #   section).
+    down_rows = [rows[0]]
+    up_rows = []
+    for i in range(1, height-1):
+        down_rows += [''.join([c for n, c in enumerate(rows[i]) if n % 2 == 0])]
+        up_rows += [''.join([c for n, c in enumerate(rows[i]) if n % 2 == 1])]
+    down_rows += [rows[-1]]
+    up_rows.reverse()
+    return ''.join(c for r in zip_longest(*(down_rows + up_rows), fillvalue='') for c in r)
 
 
 class PocketEnigma(object):