Many tests done, still more to come.
authorNeil Smith <neil.git@njae.me.uk>
Tue, 10 Nov 2020 16:13:57 +0000 (16:13 +0000)
committerNeil Smith <neil.git@njae.me.uk>
Tue, 10 Nov 2020 16:13:57 +0000 (16:13 +0000)
20 files changed:
LICENCE
szyfrow/amsco.py
szyfrow/autokey.py
szyfrow/bifid.py
szyfrow/cadenus.py
szyfrow/column_transposition.py
szyfrow/keyword_cipher.py
szyfrow/railfence.py
szyfrow/support/language_models.py
szyfrow/support/plot_frequency_histogram.py [deleted file]
szyfrow/support/utilities.py
tests/test_amsco.py [new file with mode: 0644]
tests/test_autokey.py [new file with mode: 0644]
tests/test_bifid.py [new file with mode: 0644]
tests/test_cadenus.py [new file with mode: 0644]
tests/test_column_transposition.py [new file with mode: 0644]
tests/test_keyword_cipher.py [new file with mode: 0644]
tests/test_languge_models.py [new file with mode: 0644]
tests/test_railfence.py [new file with mode: 0644]
tests/test_utilities.py [new file with mode: 0644]

diff --git a/LICENCE b/LICENCE
index 9ec50e7e98d12abe56a02321c376b93525707233..416de5f6358c3f5400c6226a9456fa0bd74b5874 100644 (file)
--- a/LICENCE
+++ b/LICENCE
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.
\ No newline at end of file
index 48ab30627b7c46f472b526a2fcbd4cfbe3c05b6c..850861bb5bf1c4988c5b77bbcb9e4f48e9f2269f 100644 (file)
@@ -4,7 +4,7 @@ import itertools
 
 from szyfrow.support.utilities import *
 from szyfrow.support.language_models import *
-from szyfrow.column_transposition import transpositions, transpositions_of
+from szyfrow.column_transposition import transpositions, transpositions_of
 
 # Where each piece of text ends up in the AMSCO transpositon cipher.
 # 'index' shows where the slice appears in the plaintext, with the slice
@@ -16,7 +16,7 @@ class AmscoFillStyle(Enum):
     same_each_row = 2
     reverse_each_row = 3
 
-def amsco_transposition_positions(message, keyword, 
+def amsco_positions(message, keyword, 
       fillpattern=(1, 2),
       fillstyle=AmscoFillStyle.continuous,
       fillcolumnwise=False,
@@ -25,7 +25,7 @@ def amsco_transposition_positions(message, keyword,
     grid shows the index of that slice and the start and end positions of the
     plaintext that go to make it up.
 
-    >>> amsco_transposition_positions(string.ascii_lowercase, 'freddy', \
+    >>> amsco_positions(string.ascii_lowercase, 'freddy', \
         fillpattern=(1, 2)) # doctest:  +NORMALIZE_WHITESPACE
     [[AmscoSlice(index=3, start=4, end=6),
      AmscoSlice(index=2, start=3, end=4),
@@ -72,64 +72,64 @@ def amsco_transposition_positions(message, keyword,
             current_fillpattern = list(reversed(current_fillpattern))
     return [transpose(r, transpositions) for r in grid]
 
-def amsco_transposition_encipher(message, keyword, 
+def amsco_encipher(message, keyword, 
     fillpattern=(1,2), fillstyle=AmscoFillStyle.reverse_each_row):
     """AMSCO transposition encipher.
 
-    >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(1, 2))
+    >>> amsco_encipher('hellothere', 'abc', fillpattern=(1, 2))
     'hoteelhler'
-    >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(2, 1))
+    >>> amsco_encipher('hellothere', 'abc', fillpattern=(2, 1))
     'hetelhelor'
-    >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(1, 2))
+    >>> amsco_encipher('hellothere', 'acb', fillpattern=(1, 2))
     'hotelerelh'
-    >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(2, 1))
+    >>> amsco_encipher('hellothere', 'acb', fillpattern=(2, 1))
     'hetelorlhe'
-    >>> amsco_transposition_encipher('hereissometexttoencipher', 'encode')
+    >>> amsco_encipher('hereissometexttoencipher', 'encode')
     'etecstthhomoerereenisxip'
-    >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2))
+    >>> amsco_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2))
     'hetcsoeisterereipexthomn'
-    >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
+    >>> amsco_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
     'hecsoisttererteipexhomen'
-    >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(2, 1))
+    >>> amsco_encipher('hereissometexttoencipher', 'cipher', fillpattern=(2, 1))
     'heecisoosttrrtepeixhemen'
-    >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2))
+    >>> amsco_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2))
     'hxtomephescieretoeisnter'
-    >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
+    >>> amsco_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
     'hxomeiphscerettoisenteer'
     """
-    grid = amsco_transposition_positions(message, keyword, 
+    grid = amsco_positions(message, keyword, 
         fillpattern=fillpattern, fillstyle=fillstyle)
     ct_as_grid = [[message[s.start:s.end] for s in r] for r in grid]
     return combine_every_nth(ct_as_grid)
 
 
-def amsco_transposition_decipher(message, keyword, 
+def amsco_decipher(message, keyword, 
     fillpattern=(1,2), fillstyle=AmscoFillStyle.reverse_each_row):
     """AMSCO transposition decipher
 
-    >>> amsco_transposition_decipher('hoteelhler', 'abc', fillpattern=(1, 2))
+    >>> amsco_decipher('hoteelhler', 'abc', fillpattern=(1, 2))
     'hellothere'
-    >>> amsco_transposition_decipher('hetelhelor', 'abc', fillpattern=(2, 1))
+    >>> amsco_decipher('hetelhelor', 'abc', fillpattern=(2, 1))
     'hellothere'
-    >>> amsco_transposition_decipher('hotelerelh', 'acb', fillpattern=(1, 2))
+    >>> amsco_decipher('hotelerelh', 'acb', fillpattern=(1, 2))
     'hellothere'
-    >>> amsco_transposition_decipher('hetelorlhe', 'acb', fillpattern=(2, 1))
+    >>> amsco_decipher('hetelorlhe', 'acb', fillpattern=(2, 1))
     'hellothere'
-    >>> amsco_transposition_decipher('etecstthhomoerereenisxip', 'encode')
+    >>> amsco_decipher('etecstthhomoerereenisxip', 'encode')
     'hereissometexttoencipher'
-    >>> amsco_transposition_decipher('hetcsoeisterereipexthomn', 'cipher', fillpattern=(1, 2))
+    >>> amsco_decipher('hetcsoeisterereipexthomn', 'cipher', fillpattern=(1, 2))
     'hereissometexttoencipher'
-    >>> amsco_transposition_decipher('hecsoisttererteipexhomen', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
+    >>> amsco_decipher('hecsoisttererteipexhomen', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
     'hereissometexttoencipher'
-    >>> amsco_transposition_decipher('heecisoosttrrtepeixhemen', 'cipher', fillpattern=(2, 1))
+    >>> amsco_decipher('heecisoosttrrtepeixhemen', 'cipher', fillpattern=(2, 1))
     'hereissometexttoencipher'
-    >>> amsco_transposition_decipher('hxtomephescieretoeisnter', 'cipher', fillpattern=(1, 3, 2))
+    >>> amsco_decipher('hxtomephescieretoeisnter', 'cipher', fillpattern=(1, 3, 2))
     'hereissometexttoencipher'
-    >>> amsco_transposition_decipher('hxomeiphscerettoisenteer', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
+    >>> amsco_decipher('hxomeiphscerettoisenteer', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
     'hereissometexttoencipher'
     """
 
-    grid = amsco_transposition_positions(message, keyword, 
+    grid = amsco_positions(message, keyword, 
         fillpattern=fillpattern, fillstyle=fillstyle)
     transposed_sections = [s for c in [l for l in zip(*grid)] for s in c]
     plaintext_list = [''] * len(transposed_sections)
@@ -149,7 +149,7 @@ def amsco_break(message, translist=transpositions, patterns = [(1, 2), (2, 1)],
     """Breaks an AMSCO transposition cipher using a dictionary and
     n-gram frequency analysis
 
-    >>> amsco_break(amsco_transposition_encipher(sanitise( \
+    >>> amsco_break(amsco_encipher(sanitise( \
             "It is a truth universally acknowledged, that a single man in \
              possession of a good fortune, must be in want of a wife. However \
              little known the feelings or views of such a man may be on his \
@@ -162,7 +162,7 @@ def amsco_break(message, translist=transpositions, patterns = [(1, 2), (2, 1)],
                    (6, 1, 0, 4, 5, 3, 2): ['keyword']}, \
         patterns=[(1, 2)]) # doctest: +ELLIPSIS
     (((2, 0, 5, 3, 1, 4, 6), (1, 2), <AmscoFillStyle.continuous: 1>), -709.4646722...)
-    >>> amsco_break(amsco_transposition_encipher(sanitise( \
+    >>> amsco_break(amsco_encipher(sanitise( \
             "It is a truth universally acknowledged, that a single man in \
              possession of a good fortune, must be in want of a wife. However \
              little known the feelings or views of such a man may be on his \
@@ -188,7 +188,7 @@ def amsco_break(message, translist=transpositions, patterns = [(1, 2), (2, 1)],
 
 def amsco_break_worker(message, transposition,
         pattern, fillstyle, fitness):
-    plaintext = amsco_transposition_decipher(message, transposition,
+    plaintext = amsco_decipher(message, transposition,
         fillpattern=pattern, fillstyle=fillstyle)
     fit = fitness(sanitise(plaintext))
     return (transposition, pattern, fillstyle), fit
index 36ffa856b49ad45d987af125c5bcde468aeae8da..11bfa6bdea506fc51e78e6ccfee2a3a46cefae75 100644 (file)
@@ -61,10 +61,8 @@ def autokey_sa_break( message
 
 
 def autokey_sa_break_worker(message, key, 
-                                     t0, max_iterations, fitness):
-   
+                                     t0, max_iterations, fitness):   
     temperature = t0
-
     dt = t0 / (0.9 * max_iterations)
     
     plaintext = autokey_decipher(message, key)
index ed8462febbea43927ca3c775f8b98b372e26536f..967bff4838dbfc5772b132de6eacb043b7c8021a 100644 (file)
@@ -28,14 +28,24 @@ def bifid_encipher(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a,
     >>> bifid_encipher("indiacurry", 'iguana', period=4, fillvalue='x')
     'ibnhgaqltzml'
     """
+
+    if period:
+        if not fillvalue:
+            raise ValueError("fillvalue must be given if period is given")
+        else:
+            p_message = message + pad(len(message), period, fillvalue)
+    else:
+        p_message = message
+
     translation, f_grid, r_grid = bifid_grid(keyword, wrap_alphabet, letter_mapping)
     
-    t_message = message.translate(translation)
+    t_message = p_message.translate(translation)
     pairs0 = [f_grid[l] for l in sanitise(t_message)]
     if period:
-        chunked_pairs = [pairs0[i:i+period] for i in range(0, len(pairs0), period)]
-        if len(chunked_pairs[-1]) < period and fillvalue:
-            chunked_pairs[-1] += [f_grid[fillvalue]] * (period - len(chunked_pairs[-1]))
+        # chunked_pairs = [pairs0[i:i+period] for i in range(0, len(pairs0), period)]
+        # if len(chunked_pairs[-1]) < period and fillvalue:
+        #     chunked_pairs[-1] += [f_grid[fillvalue]] * (period - len(chunked_pairs[-1]))
+        chunked_pairs = chunks(pairs0, period, fillvalue=None)
     else:
         chunked_pairs = [pairs0]
     
@@ -59,14 +69,23 @@ def bifid_decipher(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a,
     >>> bifid_decipher("ibnhgaqltzml", 'iguana', period=4)
     'indiacurryxx'
     """
+    if period:
+        if not fillvalue:
+            raise ValueError("fillvalue must be given if period is given")
+        else:
+            p_message = message + pad(len(message), period, fillvalue)
+    else:
+        p_message = message
+
     translation, f_grid, r_grid = bifid_grid(keyword, wrap_alphabet, letter_mapping)
     
     t_message = message.translate(translation)
     pairs0 = [f_grid[l] for l in sanitise(t_message)]
     if period:
-        chunked_pairs = [pairs0[i:i+period] for i in range(0, len(pairs0), period)]
-        if len(chunked_pairs[-1]) < period and fillvalue:
-            chunked_pairs[-1] += [f_grid[fillvalue]] * (period - len(chunked_pairs[-1]))
+        # chunked_pairs = [pairs0[i:i+period] for i in range(0, len(pairs0), period)]
+        # if len(chunked_pairs[-1]) < period and fillvalue:
+        #     chunked_pairs[-1] += [f_grid[fillvalue]] * (period - len(chunked_pairs[-1]))
+        chunked_pairs = chunks(pairs0, period, fillvalue=None)
     else:
         chunked_pairs = [pairs0]
         
@@ -80,7 +99,7 @@ def bifid_decipher(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a,
     return cat(r_grid[p] for p in pairs1) 
 
 
-def bifid_break_mp(message, wordlist=keywords, fitness=Pletters, max_period=10,
+def bifid_break(message, wordlist=keywords, fitness=Pletters, max_period=10,
                      number_of_solutions=1, chunksize=500):
     """Breaks a keyword substitution cipher using a dictionary and
     frequency analysis
@@ -110,7 +129,8 @@ def bifid_break_mp(message, wordlist=keywords, fitness=Pletters, max_period=10,
             return sorted(breaks, key=lambda k: k[1], reverse=True)[:number_of_solutions]
 
 def bifid_break_worker(message, keyword, wrap_alphabet, period, fitness):
-    plaintext = bifid_decipher(message, keyword, wrap_alphabet, period=period)
+    plaintext = bifid_decipher(message, keyword, wrap_alphabet, 
+        period=period, fillvalue='e')
     fit = fitness(plaintext)
     return (keyword, wrap_alphabet, period), fit
 
index 0f33ac517070aa6eb892751b2c61bf24ac63f5b9..b56500c6de1f01df5d602664b04731cb5008429d 100644 (file)
@@ -97,7 +97,7 @@ def cadenus_break(message, words=keywords,
     doubled_letters='vw', fitness=Pbigrams):
     c = make_cadenus_keycolumn(reverse=True)
     valid_words = [w for w in words 
-        if max(transpositions_of(w)) <= len(c)]
+        if len(transpositions_of(w)) == len(message) // 25]
     with multiprocessing.Pool() as pool:
         results = pool.starmap(cadenus_break_worker, 
                 [(message, w, 
@@ -107,16 +107,17 @@ def cadenus_break(message, words=keywords,
                 for w in words 
                 for s in string.ascii_lowercase 
                 for r in [True, False]
-                if max(transpositions_of(w)) <= len(
-                    make_cadenus_keycolumn(
-                        doubled_letters=doubled_letters, start=s, reverse=r))
+                if max(transpositions_of(w)) <= len(
+                    make_cadenus_keycolumn(
+                        doubled_letters=doubled_letters, start=s, reverse=r))
                 ])
     # return list(results)
     return max(results, key=lambda k: k[1])
 
 def cadenus_break_worker(message, keyword, keycolumn, fitness):
-    message_chunks = chunks(message, 175)
-    plaintext = ''.join(cadenus_decipher(c, keyword, keycolumn) for c in message_chunks)
+    # message_chunks = chunks(message, 175)
+    # plaintext = ''.join(cadenus_decipher(c, keyword, keycolumn) for c in message_chunks)
+    plaintext = cadenus_decipher(message, keyword, keycolumn)
     fit = fitness(plaintext)
     return (keyword, keycolumn), fit
 
index d81903e9bf3cf7f8b93bb7a1e6a5caaad69272e7..6d6fe687810925f990b6b1527fac29860cf0b6a5 100644 (file)
@@ -4,44 +4,51 @@ from itertools import chain
 from szyfrow.support.utilities import *
 from szyfrow.support.language_models import *
 
-def transpositions_of(keyword):
-    """Finds the transpostions given by a keyword. For instance, the keyword
-    'clever' rearranges to 'celrv', so the first column (0) stays first, the
-    second column (1) moves to third, the third column (2) moves to second, 
-    and so on.
+def transpositions_of(keyword):
+    """Finds the transpostions given by a keyword. For instance, the keyword
+    'clever' rearranges to 'celrv', so the first column (0) stays first, the
+    second column (1) moves to third, the third column (2) moves to second, 
+    and so on.
 
-    If passed a tuple, assume it's already a transposition and just return it.
+#     If passed a tuple, assume it's already a transposition and just return it.
+
+#     >>> transpositions_of('clever')
+#     (0, 2, 1, 4, 3)
+#     >>> transpositions_of('fred')
+#     (3, 2, 0, 1)
+#     >>> transpositions_of((3, 2, 0, 1))
+#     (3, 2, 0, 1)
+#     """
+#     if isinstance(keyword, tuple):
+#         return keyword
+#     else:
+#         key = deduplicate(keyword)
+#         transpositions = tuple(key.index(l) for l in sorted(key))
+#         return transpositions
 
-    >>> transpositions_of('clever')
-    (0, 2, 1, 4, 3)
-    >>> transpositions_of('fred')
-    (3, 2, 0, 1)
-    >>> transpositions_of((3, 2, 0, 1))
-    (3, 2, 0, 1)
-    """
-    if isinstance(keyword, tuple):
-        return keyword
-    else:
-        key = deduplicate(keyword)
-        transpositions = tuple(key.index(l) for l in sorted(key))
-        return transpositions
 
+# transpositions = collections.defaultdict(list)
+# for word in keywords:
+#     transpositions[transpositions_of(word)] += [word]
 
-transpositions = collections.defaultdict(list)
-for word in keywords:
-    transpositions[transpositions_of(word)] += [word]
 
+# def pad(message_len, group_len, fillvalue):
+#     """Return the padding needed to extend a message to a multiple of group_len
+#     in length.
 
-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
+#     fillvalue can be a function or a literal value. If a function, it is called
+#     once for each padded character. Use this will fillvalue=random_english_letter
+#     to pad a message with random letters.
+#     """
+#     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,
@@ -145,7 +152,7 @@ def scytale_encipher(message, rows, fillvalue=' '):
     # transpositions = [i for i in range(math.ceil(len(message) / rows))]
     # return column_transposition_encipher(message, transpositions, 
     #     fillvalue=fillvalue, fillcolumnwise=False, emptycolumnwise=True)
-    transpositions = [i for i in range(rows)]
+    transpositions = (i for i in range(rows))
     return column_transposition_encipher(message, transpositions, 
         fillvalue=fillvalue, fillcolumnwise=True, emptycolumnwise=False)
 
@@ -177,6 +184,9 @@ def column_transposition_break_mp(message, translist=transpositions,
     """Breaks a column transposition cipher using a dictionary and
     n-gram frequency analysis
 
+    >>> len(keywords)
+    20
+
     >>> column_transposition_break_mp(column_transposition_encipher(sanitise( \
             "It is a truth universally acknowledged, that a single man in \
              possession of a good fortune, must be in want of a wife. However \
index b895b3f626663a666b178d161a438cde96c065fd..446c6c42076d73f15739cd2346a6181b8da4d4ca 100644 (file)
@@ -148,7 +148,7 @@ def monoalphabetic_break_hillclimbing(message,
                               cipher_alphabet=None, 
                               swap_index_finder=None,
                               fitness=Pletters, chunksize=1):
-    return simulated_annealing_break(message, 
+    return monoalphabetic_sa_break(message, 
                               workers=1, 
                               initial_temperature=0,
                               max_iterations=max_iterations,
@@ -165,7 +165,7 @@ def monoalphabetic_break_hillclimbing_mp(message,
                               cipher_alphabet=None, 
                               swap_index_finder=None,
                               fitness=Pletters, chunksize=1):
-    return simulated_annealing_break(message, 
+    return monoalphabetic_sa_break(message, 
                               workers=workers, 
                               initial_temperature=0,
                               max_iterations=max_iterations,
@@ -181,7 +181,7 @@ def gaussian_swap_index(a):
 def uniform_swap_index(a):
     return random.randrange(26)
 
-def simulated_annealing_break(message, workers=10, 
+def monoalphabetic_sa_break(message, workers=10, 
                               initial_temperature=200,
                               max_iterations=20000,
                               plain_alphabet=None, 
@@ -215,12 +215,12 @@ def simulated_annealing_break(message, workers=10,
                             initial_temperature, max_iterations, fitness,
                             i))
     with multiprocessing.Pool() as pool:
-        breaks = pool.starmap(simulated_annealing_break_worker,
+        breaks = pool.starmap(monoalphabetic_sa_break_worker,
                               worker_args, chunksize)
     return max(breaks, key=lambda k: k[1])
 
 
-def simulated_annealing_break_worker(message, plain_alphabet, cipher_alphabet, 
+def monoalphabetic_sa_break_worker(message, plain_alphabet, cipher_alphabet, 
                                      swap_index_finder,
                                      t0, max_iterations, fitness,
                                      logID):
index e81b066809623dae8db28cd89d1e90132b122e83..ef3d266f5739ee129d125b5d1d239f57847185aa 100644 (file)
@@ -128,19 +128,6 @@ def railfence_decipher(message, height, fillvalue=''):
     return cat(c for r in zip_longest(*(down_rows + up_rows), fillvalue='') for c in r)
 
 
-def railfence_break(message, max_key_length=20,
-                     fitness=Pletters, chunksize=500):
-    """Breaks a railfence cipher using a matrix of given rank and letter frequencies
-
-    
-    """
-    
-    sanitised_message = sanitise(message)
-    results = starmap(worker, [(sanitised_message, i, fitness)
-                               for i in range(2, max_key_length+1)])
-    return max(results, key=lambda k: k[1])
-
-
 def railfence_break(message, max_key_length=20,
                      fitness=Pbigrams, chunksize=500):
     """Breaks a railfence cipher using a range of lengths and
index 6898eb4688031c1e21a31cbef64b5a66c3400b83..95dbeb277b8c828f62cc6e8ac83938e683491ed0 100644 (file)
@@ -4,13 +4,10 @@ import collections
 import itertools
 from math import log10
 import os 
-
 import importlib.resources as pkg_resources
 
 import szyfrow.support.norms
-from szyfrow.support.utilities import sanitise
-
-
+from szyfrow.support.utilities import sanitise, deduplicate
 from szyfrow import language_model_files
 
 
@@ -36,6 +33,33 @@ with pkg_resources.open_text(language_model_files, 'words.txt') as f:
     keywords = [line.rstrip() for line in f]
 
 
+def transpositions_of(keyword):
+    """Finds the transpostions given by a keyword. For instance, the keyword
+    'clever' rearranges to 'celrv', so the first column (0) stays first, the
+    second column (1) moves to third, the third column (2) moves to second, 
+    and so on.
+
+    If passed a tuple, assume it's already a transposition and just return it.
+
+    >>> transpositions_of('clever')
+    (0, 2, 1, 4, 3)
+    >>> transpositions_of('fred')
+    (3, 2, 0, 1)
+    >>> transpositions_of((3, 2, 0, 1))
+    (3, 2, 0, 1)
+    """
+    if isinstance(keyword, tuple):
+        return keyword
+    else:
+        key = deduplicate(keyword)
+        transpositions = tuple(key.index(l) for l in sorted(key))
+        return transpositions
+
+transpositions = collections.defaultdict(list)
+for word in keywords:
+    transpositions[transpositions_of(word)] += [word]
+
+
 def weighted_choice(d):
     """Generate random item from a dictionary of item counts
     """
diff --git a/szyfrow/support/plot_frequency_histogram.py b/szyfrow/support/plot_frequency_histogram.py
deleted file mode 100644 (file)
index d4a9297..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-import matplotlib.pyplot as plt
-
-def plot_frequency_histogram(freqs, sort_key=None):
-    x = range(len(freqs))
-    y = [freqs[l] for l in sorted(freqs, key=sort_key)]
-    f = plt.figure()
-    ax = f.add_axes([0.1, 0.1, 0.9, 0.9])
-    ax.bar(x, y, align='center')
-    ax.set_xticks(x)
-    ax.set_xticklabels(sorted(freqs, key=sort_key))
-    f.show()
-
-if __name__ == "__main__":
-    import doctest
-    doctest.testmod()
index 7319621c1fbfdfa7cf650912d288401189482057..7a61ae709a55d2115118991bc78f860caf1ad66b 100644 (file)
@@ -25,6 +25,24 @@ def unpos(number):
     """Return the letter in the given position in the alphabet (mod 26)"""
     return chr(number % 26 + ord('a'))
 
+def pad(message_len, group_len, fillvalue):
+    """Return the padding needed to extend a message to a multiple of group_len
+    in length.
+
+    fillvalue can be a function or a literal value. If a function, it is called
+    once for each padded character. Use this with fillvalue=random_english_letter
+    to pad a message with random letters.
+    """
+    padding_length = group_len - message_len % group_len
+    if padding_length == group_len: padding_length = 0
+    padding = ''
+    if callable(fillvalue):
+        for i in range(padding_length):
+            padding += fillvalue()
+    else:
+        padding += fillvalue * padding_length
+    return padding
+
 def every_nth(text, n, fillvalue=''):
     """Returns n strings, each of which consists of every nth character, 
     starting with the 0th, 1st, 2nd, ... (n-1)th character
@@ -66,10 +84,12 @@ def chunks(text, n, fillvalue=None):
     ['abcd', 'efgh', 'i!!!']
     """
     if fillvalue:
-        padding = fillvalue[0] * (n - len(text) % n)
+        # padding = fillvalue[0] * (n - len(text) % n)
+        padding = pad(len(text), n, fillvalue)
+        padded_text = text + padding
     else:
-        padding = ''
-    return [(text+padding)[i:i+n] for i in range(0, len(text), n)]
+        padded_text = text
+    return [(padded_text)[i:i+n] for i in range(0, len(text), n)]
 
 def transpose(items, transposition):
     """Moves items around according to the given transposition
diff --git a/tests/test_amsco.py b/tests/test_amsco.py
new file mode 100644 (file)
index 0000000..a09f701
--- /dev/null
@@ -0,0 +1,98 @@
+import pytest
+import string 
+
+from szyfrow.amsco import *
+from szyfrow.support.utilities import *
+from szyfrow.support.language_models import transpositions_of
+
+def test_positions():
+    grid = amsco_positions(string.ascii_lowercase, 'freddy', fillpattern=(1, 2))
+    assert grid == [[AmscoSlice(index=3, start=4, end=6),
+     AmscoSlice(index=2, start=3, end=4),
+     AmscoSlice(index=0, start=0, end=1),
+     AmscoSlice(index=1, start=1, end=3),
+     AmscoSlice(index=4, start=6, end=7)],
+    [AmscoSlice(index=8, start=12, end=13),
+     AmscoSlice(index=7, start=10, end=12),
+     AmscoSlice(index=5, start=7, end=9),
+     AmscoSlice(index=6, start=9, end=10),
+     AmscoSlice(index=9, start=13, end=15)],
+    [AmscoSlice(index=13, start=19, end=21),
+     AmscoSlice(index=12, start=18, end=19),
+     AmscoSlice(index=10, start=15, end=16),
+     AmscoSlice(index=11, start=16, end=18),
+     AmscoSlice(index=14, start=21, end=22)],
+    [AmscoSlice(index=18, start=27, end=28),
+     AmscoSlice(index=17, start=25, end=27),
+     AmscoSlice(index=15, start=22, end=24),
+     AmscoSlice(index=16, start=24, end=25),
+     AmscoSlice(index=19, start=28, end=30)]]
+
+def test_encipher_message():
+    ciphertext = amsco_encipher('hellothere', 'abc', fillpattern=(1, 2))
+    assert ciphertext == 'hoteelhler'
+
+    ciphertext = amsco_encipher('hellothere', 'abc', fillpattern=(2, 1))
+    assert ciphertext == 'hetelhelor'
+
+    ciphertext = amsco_encipher('hellothere', 'acb', fillpattern=(1, 2))
+    assert ciphertext == 'hotelerelh'
+
+    ciphertext = amsco_encipher('hellothere', 'acb', fillpattern=(2, 1))
+    assert ciphertext == 'hetelorlhe'
+
+    ciphertext = amsco_encipher('hereissometexttoencipher', 'encode')
+    assert ciphertext == 'etecstthhomoerereenisxip'
+
+    ciphertext = amsco_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2))
+    assert ciphertext == 'hetcsoeisterereipexthomn'
+
+    ciphertext = amsco_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
+    assert ciphertext == 'hecsoisttererteipexhomen'
+
+    ciphertext = amsco_encipher('hereissometexttoencipher', 'cipher', fillpattern=(2, 1))
+    assert ciphertext == 'heecisoosttrrtepeixhemen'
+
+    ciphertext = amsco_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2))
+    assert ciphertext == 'hxtomephescieretoeisnter'
+
+    ciphertext = amsco_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
+    assert ciphertext == 'hxomeiphscerettoisenteer'
+
+
+def test_decipher_message():
+    plaintext = 'hereissometexttoencipher'
+    for key in ['bayes', 'samplekey']:
+        for fillpattern in [(1, 2), (2, 1)]:
+            for fillstyle in AmscoFillStyle:
+                enciphered = amsco_encipher(plaintext, key, 
+                    fillpattern=fillpattern, fillstyle=fillstyle)
+                deciphered = amsco_decipher(enciphered, key, 
+                    fillpattern=fillpattern, fillstyle=fillstyle)
+                assert deciphered == plaintext
+
+
+def test_amsco_break():
+    plaintext = sanitise('''It is a truth universally acknowledged, that a single man in
+             possession of a good fortune, must be in want of a wife. However
+             little known the feelings or views of such a man may be on his 
+             first entering a neighbourhood, this truth is so well fixed in
+             the minds of the surrounding families, that he is considered the
+             rightful property of some one or other of their daughters.''')
+    expected_key = 'encipher'
+    expected_fillpattern = (1, 2)
+    expected_fillsytle = AmscoFillStyle.continuous
+    expected_score = Pbigrams(plaintext)
+    ciphertext = amsco_encipher(plaintext, expected_key, 
+        fillpattern=expected_fillpattern, fillstyle=expected_fillsytle)
+    used_translist = collections.defaultdict(list)
+    for word in 'encipher fourteen keyword'.split():
+        used_translist[transpositions_of(word)] += [word]
+
+    (key, fillpattern, fillstyle), score = amsco_break(ciphertext, 
+        translist=used_translist)
+
+    assert key == transpositions_of(expected_key)
+    assert fillpattern == expected_fillpattern
+    assert fillstyle == expected_fillsytle
+    assert score == pytest.approx(expected_score)
diff --git a/tests/test_autokey.py b/tests/test_autokey.py
new file mode 100644 (file)
index 0000000..c57c08e
--- /dev/null
@@ -0,0 +1,34 @@
+import pytest
+import string 
+
+from szyfrow.autokey import *
+from szyfrow.support.utilities import *
+
+
+def test_encipher_message():
+    enciphered = autokey_encipher('meetatthefountain', 'kilt')
+    expected = 'wmpmmxxaeyhbryoca'
+    assert enciphered == expected
+
+def test_decipher_message():
+    deciphered = autokey_decipher('wmpmmxxaeyhbryoca', 'kilt')
+    expected = 'meetatthefountain'
+    assert deciphered == expected
+
+# def test_break():
+#     plaintext = '''hours passed during which jerico tried every trick he 
+#         could think of to prompt some fresh inspiration. he arranged the 
+#         cryptograms chronologically then he arranged them by length. then 
+#         he sorted them by frequency. he doodled on the pile of paper. he 
+#         prowled around the hut, oblivious now to who was looking at him 
+#         and who wasnt.'''
+#     # expected_key = 'samplekey'
+#     expected_key = 'abc'
+#     ciphertext = autokey_encipher(sanitise(plaintext), expected_key)
+#     expected_score = Ptrigrams(sanitise(plaintext))
+#     actual_key, actual_score = autokey_sa_break(ciphertext, 
+#         min_keylength=len(expected_key), max_keylength=len(expected_key),
+#         workers=10, max_iterations=1000, fitness=Ptrigrams
+#         )
+#     assert expected_key == actual_key
+#     assert expected_score == pytest.approx(actual_score)
diff --git a/tests/test_bifid.py b/tests/test_bifid.py
new file mode 100644 (file)
index 0000000..1077411
--- /dev/null
@@ -0,0 +1,57 @@
+import pytest
+import string 
+
+from szyfrow.bifid import *
+from szyfrow.support.utilities import *
+
+def test_grid():
+    trans, f_grid, r_grid = bifid_grid('bayes', KeywordWrapAlphabet.from_a, None)
+    assert len(f_grid) == 25
+    assert len(r_grid) == 25
+    for l in f_grid:
+        assert l == r_grid[f_grid[l]]
+    assert r_grid[1, 1] == 'b'
+    assert r_grid[2, 1] == 'c'
+
+
+def test_encipher_message():
+    assert bifid_encipher("indiajelly", 'iguana') == 'ibidonhprm'
+    assert bifid_encipher("indiacurry", 'iguana', period=4, fillvalue='x') == 'ibnhgaqltzml'
+    with pytest.raises(ValueError):
+        bifid_encipher("indiacurry", 'iguana', period=4)
+
+
+def test_decipher_message():
+    plaintext = 'hereissometexttoencipher'
+    for key in ['bayes', 'samplekey']:
+        enciphered = bifid_encipher(plaintext, key)
+        deciphered = bifid_decipher(enciphered, key)
+        assert deciphered == plaintext
+
+    for key in ['bayes', 'samplekey']:
+        enciphered = bifid_encipher(plaintext, key, period=5, fillvalue='a')
+        deciphered = bifid_decipher(enciphered, key, period=5, fillvalue='a')
+        assert deciphered == plaintext + pad(len(plaintext), 5, 'a') 
+
+
+def test_bifid_break():
+    plaintext = sanitise('''It is a truth universally acknowledged, that a single man in
+             possession of a good fortune, must be in want of a wife. However
+             little known the feelings or views of such a man may be on his 
+             first entering a neighbourhood, this truth is so well fixed in
+             the minds of the surrounding families, that he is considered the
+             rightful property of some one or other of their daughters.''')
+    expected_key = 'encipher'
+    expected_wrap = KeywordWrapAlphabet.from_a
+    expected_period = 0
+    expected_score = Pletters(plaintext)
+    ciphertext = bifid_encipher(plaintext, expected_key, 
+        wrap_alphabet=expected_wrap)
+    
+    (key, wrap, period), score = bifid_break(ciphertext, 
+        wordlist='encipher fourteen keyword'.split())
+
+    assert key == expected_key
+    assert wrap == expected_wrap
+    assert period == expected_period
+    assert score == pytest.approx(expected_score)
diff --git a/tests/test_cadenus.py b/tests/test_cadenus.py
new file mode 100644 (file)
index 0000000..c7b48ec
--- /dev/null
@@ -0,0 +1,83 @@
+import pytest
+import string 
+
+from szyfrow.cadenus import *
+from szyfrow.support.utilities import *
+
+def test_keycolumn():
+    assert make_cadenus_keycolumn()['a'] == 0
+    assert make_cadenus_keycolumn()['b'] == 1
+    assert make_cadenus_keycolumn()['c'] == 2
+    assert make_cadenus_keycolumn()['v'] == 21
+    assert make_cadenus_keycolumn()['w'] == 21
+    assert make_cadenus_keycolumn()['z'] == 24
+    assert make_cadenus_keycolumn(doubled_letters='ij', start='b', 
+        reverse=True)['a'] == 1
+    assert make_cadenus_keycolumn(doubled_letters='ij', start='b', 
+        reverse=True)['b'] == 0
+    assert make_cadenus_keycolumn(doubled_letters='ij', start='b', 
+        reverse=True)['c'] == 24
+    assert make_cadenus_keycolumn(doubled_letters='ij', start='b', 
+        reverse=True)['i'] == 18
+    assert make_cadenus_keycolumn(doubled_letters='ij', start='b', 
+        reverse=True)['j'] == 18
+    assert make_cadenus_keycolumn(doubled_letters='ij', start='b', 
+        reverse=True)['v'] == 6
+    assert make_cadenus_keycolumn(doubled_letters='ij', start='b', 
+        reverse=True)['z'] == 2
+
+
+def test_encipher_message():
+    plaintext = sanitise('''Whoever has made a voyage up the Hudson must 
+        remember the Kaatskill mountains. They are a dismembered branch of 
+        the great''')
+    keycol = make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True)
+    ciphertext = cadenus_encipher(plaintext, 'wink', keycol)
+    expected = 'antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaasuvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned'
+    assert ciphertext == expected
+
+    plaintext = sanitise('''a severe limitation on the usefulness of the 
+        cadenus is that every message must be a multiple of twenty-five 
+        letters long''')
+    ciphertext = cadenus_encipher(plaintext, 'easy', keycol)
+    expected = 'systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtofarenuseieeieltarlmentieetogevesitfaisltngeeuvowul'
+    assert ciphertext == expected
+
+
+def test_decipher_message():
+    plaintext = sanitise('''Whoever has made a voyage up the Hudson must 
+        remember the Kaatskill mountains. They are a dismembered branch of 
+        the great''')
+    keycol = make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True)
+    for key in ['wink', 'easy']: 
+        enciphered = cadenus_encipher(plaintext, key, keycol)
+        deciphered = cadenus_decipher(enciphered, key, keycol)
+        assert deciphered == plaintext
+
+
+def test_cadenus_break():
+    plaintext = sanitise('''It is a truth universally acknowledged, that a single man in
+             possession of a good fortune, must be in want of a wife. However
+             little known the feelings or views of such a man may be on his 
+             first entering a neighbourhood, this truth is so well fixed in
+             the minds of the surrounding families, that he is considered the
+             rightful property of some one or other of their daughters.''')
+    expected_key = 'swashbuckling'
+    expected_score = Ptrigrams(plaintext)
+    expected_keycol = make_cadenus_keycolumn(doubled_letters='vw', start='a', 
+        reverse=True)
+
+    ciphertext = cadenus_encipher(plaintext, expected_key, expected_keycol)
+    
+    # dictionary = ['clearinghouse', 'computerising', 'counterclaims', 
+    #     'housewarmings',  'intravenously', 'liquefactions', 'somersaulting', 
+    #     'sportsmanlike', 'swashbuckling']
+
+    dictionary = ['swashbuckling']
+
+    (key, keycol), score = cadenus_break(ciphertext, words=dictionary, 
+        fitness=Ptrigrams)
+
+    assert key == expected_key
+    assert keycol == expected_keycol
+    assert score == pytest.approx(expected_score)
diff --git a/tests/test_column_transposition.py b/tests/test_column_transposition.py
new file mode 100644 (file)
index 0000000..31b3417
--- /dev/null
@@ -0,0 +1,105 @@
+import pytest
+import string 
+
+from szyfrow.column_transposition import *
+from szyfrow.support.utilities import *
+
+def test_encipher_message():
+    ciphertext =  column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True)
+    assert ciphertext == 'hlohr eltee '
+    ciphertext =  column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True, emptycolumnwise=True)
+    assert ciphertext == 'hellothere  '
+    ciphertext =  column_transposition_encipher('hellothere', 'abcdef')
+    assert ciphertext == 'hellothere  '
+    ciphertext =  column_transposition_encipher('hellothere', 'abcde')
+    assert ciphertext == 'hellothere'
+    ciphertext =  column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
+    assert ciphertext == 'hellothere'
+    ciphertext =  column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
+    assert ciphertext == 'hlohreltee'
+    ciphertext =  column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
+    assert ciphertext == 'htehlelroe'
+    ciphertext =  column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
+    assert ciphertext == 'hellothere'
+    ciphertext =  column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=True)
+    assert ciphertext == 'heotllrehe'
+    ciphertext =  column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=False)
+    assert ciphertext == 'holrhetlee'
+    ciphertext =  column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=True)
+    assert ciphertext == 'htleehoelr'
+    ciphertext =  column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=False)
+    assert ciphertext == 'hleolteher'
+    ciphertext =  column_transposition_encipher('hellothere', 'cleverly')
+    assert ciphertext == 'hleolthre e '
+    ciphertext =  column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
+    assert ciphertext == 'hleolthre!e!'
+    ciphertext =  column_transposition_encipher('hellothere', 'cleverly', fillvalue=lambda: '*')
+    assert ciphertext == 'hleolthre*e*'
+
+def test_decipher_message():
+    plaintext = 'hereissometexttoencipher'
+    for key in ['bayes', 'samplekey']:
+        for fillcolumnwise in [True, False]:
+            for emptycolumnwise in [True, False]:
+                enciphered = column_transposition_encipher(plaintext, key, 
+                    fillcolumnwise=fillcolumnwise, emptycolumnwise=emptycolumnwise)
+                deciphered = column_transposition_decipher(enciphered, key, 
+                    fillcolumnwise=fillcolumnwise, emptycolumnwise=emptycolumnwise)
+                assert deciphered.strip() == plaintext
+
+
+def test_encipher_scytale():
+    assert scytale_encipher('thequickbrownfox', 3) == 'tcnhkfeboqrxuo iw '
+    assert scytale_encipher('thequickbrownfox', 4) == 'tubnhirfecooqkwx'
+    assert scytale_encipher('thequickbrownfox', 5) == 'tubn hirf ecoo qkwx '
+    assert scytale_encipher('thequickbrownfox', 6) == 'tqcrnxhukof eibwo '
+    assert scytale_encipher('thequickbrownfox', 7) == 'tqcrnx hukof  eibwo  '
+
+def test_decipher_scytale():
+    plaintext = 'hereissometexttoencipher'
+    for key in range(3, 8):
+        enciphered = scytale_encipher(plaintext, key)
+        deciphered = scytale_decipher(enciphered, key)
+        assert deciphered.strip() == plaintext
+
+def test_column_transposition_break():
+    plaintext = sanitise('''It is a truth universally acknowledged, that a single man in
+             possession of a good fortune, must be in want of a wife. However
+             little known the feelings or views of such a man may be on his 
+             first entering a neighbourhood, this truth is so well fixed in
+             the minds of the surrounding families, that he is considered the
+             rightful property of some one or other of their daughters.''')
+    expected_key = 'encipher'
+    expected_fill = False
+    expected_empty = True
+    expected_score = Pbigrams(plaintext)
+    ciphertext = column_transposition_encipher(plaintext, expected_key, 
+        fillcolumnwise=expected_fill, emptycolumnwise=expected_empty)
+    used_translist = collections.defaultdict(list)
+    for word in 'encipher fourteen keyword'.split():
+        used_translist[transpositions_of(word)] += [word]
+
+    (key, fill, empty), score = column_transposition_break_mp(ciphertext, 
+        translist=used_translist)
+
+    assert key == transpositions_of(expected_key)
+    assert fill == expected_fill
+    assert empty == expected_empty
+    assert score == pytest.approx(expected_score)
+
+def test_scytale_break():
+    plaintext = sanitise('''It is a truth universally acknowledged, that a single man in
+             possession of a good fortune, must be in want of a wife. However
+             little known the feelings or views of such a man may be on his 
+             first entering a neighbourhood, this truth is so well fixed in
+             the minds of the surrounding families, that he is considered the
+             rightful property of some one or other of their daughters.''')
+    expected_key = 5
+    expected_score = Pbigrams(plaintext)
+    ciphertext = scytale_encipher(plaintext, expected_key)
+
+    key, score = scytale_break_mp(ciphertext)
+
+    assert key == expected_key
+    assert score == pytest.approx(expected_score)
+    
\ No newline at end of file
diff --git a/tests/test_keyword_cipher.py b/tests/test_keyword_cipher.py
new file mode 100644 (file)
index 0000000..5959902
--- /dev/null
@@ -0,0 +1,121 @@
+import pytest
+import string 
+
+from szyfrow.keyword_cipher import *
+from szyfrow.support.utilities import *
+
+def test_cipher_alphabet():
+    assert keyword_cipher_alphabet_of('bayes') == 'bayescdfghijklmnopqrtuvwxz'
+    assert keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_a) == 'bayescdfghijklmnopqrtuvwxz'
+    assert keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_last) == 'bayestuvwxzcdfghijklmnopqr'
+    assert keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_largest) == 'bayeszcdfghijklmnopqrtuvwx'
+
+
+def test_encipher_message():
+    enciphered = keyword_encipher('test message', 'bayes') 
+    expected = 'rsqr ksqqbds'
+    assert enciphered == expected
+
+    enciphered = keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_a) 
+    expected = 'rsqr ksqqbds'
+    assert enciphered == expected
+
+    enciphered = keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_last) 
+    expected = 'lskl dskkbus'
+    assert enciphered == expected
+
+    enciphered = keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_largest) 
+    expected = 'qspq jsppbcs'
+    assert enciphered == expected
+
+
+def test_decipher_message():
+    plaintext = 'test message'
+    for key in ['bayes', 'samplekey']:
+        for wrap in KeywordWrapAlphabet:
+            enciphered = keyword_encipher(plaintext, key, wrap)
+            deciphered = keyword_decipher(enciphered, key, wrap)
+            assert deciphered == plaintext
+
+
+def test_keyword_break():
+    plaintext = 'this is a test message for the keyword breaking routine'
+    expected_key = 'elephant'
+    expected_wrap = KeywordWrapAlphabet.from_last
+    ciphertext = keyword_encipher(plaintext, expected_key, expected_wrap)
+    (key, wrap), score = keyword_break(ciphertext, 
+        wordlist=['cat', 'elephant', 'kangaroo'])
+    assert key == expected_key
+    assert wrap == expected_wrap
+    assert score == pytest.approx(Pletters(sanitise(plaintext)))
+
+def test_keyword_break_mp():
+    plaintext = 'this is a test message for the keyword breaking routine'
+    expected_key = 'elephant'
+    expected_wrap = KeywordWrapAlphabet.from_last
+    ciphertext = keyword_encipher(plaintext, expected_key, expected_wrap)
+    (key, wrap), score = keyword_break_mp(ciphertext, 
+        wordlist=['cat', 'elephant', 'kangaroo'])
+    assert key == expected_key
+    assert wrap == expected_wrap
+    assert score == pytest.approx(Pletters(sanitise(plaintext)))
+
+
+# def test_simulated_annealing_break():
+#     # random.seed(0)
+#     plaintext = '''You will rejoice to hear that no disaster has accompanied the
+#     commencement of an enterprise which you have regarded with such evil
+#     forebodings.  I arrived here yesterday, and my first task is to assure
+#     my dear sister of my welfare and increasing confidence in the success
+#     of my undertaking.
+
+#     I am already far north of London, and as I walk in the streets of
+#     Petersburgh, I feel a cold northern breeze play upon my cheeks, which
+#     braces my nerves and fills me with delight.  Do you understand this
+#     feeling?  This breeze, which has travelled from the regions towards
+#     which I am advancing, gives me a foretaste of those icy climes.
+#     Inspirited by this wind of promise, my daydreams become more fervent
+#     and vivid.  
+
+#      I try in vain to be persuaded that the pole is the seat of
+#     frost and desolation; it ever presents itself to my imagination as the
+#     region of beauty and delight.  There, Margaret, the sun is for ever
+#     visible, its broad disk just skirting the horizon and diffusing a
+#     perpetual splendour.  There—for with your leave, my sister, I will put
+#     some trust in preceding navigators—there snow and frost are banished;
+#     and, sailing over a calm sea, we may be wafted to a land surpassing in
+#     wonders and in beauty every region hitherto discovered on the habitable
+#     globe.  
+
+#     Its productions and features may be without example, as the
+#     phenomena of the heavenly bodies undoubtedly are in those undiscovered
+#     solitudes.  What may not be expected in a country of eternal light?  I
+#     may there discover the wondrous power which attracts the needle and may
+#     regulate a thousand celestial observations that require only this
+#     voyage to render their seeming eccentricities consistent for ever.  I
+#     shall satiate my ardent curiosity with the sight of a part of the world
+#     never before visited, and may tread a land never before imprinted by
+#     the foot of man. These are my enticements, and they are sufficient to
+#     conquer all fear of danger or death and to induce me to commence this
+#     laborious voyage with the joy a child feels when he embarks in a little
+#     boat, with his holiday mates, on an expedition of discovery up his
+#     native river. 
+
+#     But supposing all these conjectures to be false, you cannot contest the
+#     inestimable benefit which I shall confer on all mankind, to the last
+#     generation, by discovering a passage near the pole to those countries, to
+#     reach which at present so many months are requisite; or by ascertaining the
+#     secret of the magnet, which, if at all possible, can only be effected by an
+#     undertaking such as mine.'''
+
+#     expected_key = keyword_cipher_alphabet_of('abced')
+#     ciphertext = keyword_encipher(sanitise(plaintext), expected_key)
+#     expected_score = Ptrigrams(sanitise(plaintext))
+#     actual_key, actual_score = monoalphabetic_sa_break(ciphertext, 
+#         plain_alphabet=string.ascii_lowercase,
+#         cipher_alphabet=string.ascii_lowercase,
+#         workers=10, max_iterations=1000, 
+#         fitness=Ptrigrams
+#         )
+#     assert expected_key == actual_key
+#     assert expected_score == pytest.approx(actual_score)
diff --git a/tests/test_languge_models.py b/tests/test_languge_models.py
new file mode 100644 (file)
index 0000000..0e00104
--- /dev/null
@@ -0,0 +1,18 @@
+import pytest
+import string 
+
+from szyfrow.support.language_models import *
+
+def test_transpositions_of():
+    assert transpositions_of('clever') == (0, 2, 1, 4, 3)
+    assert transpositions_of('fred') == (3, 2, 0, 1)
+    assert transpositions_of((3, 2, 0, 1)) == (3, 2, 0, 1)
+
+def test_ngrams():
+    assert ngrams(sanitise('the quick brown fox'), 2) == ['th', 'he', 'eq', 
+        'qu', 'ui', 'ic', 'ck', 'kb', 'br', 'ro', 'ow', 'wn', 
+        'nf', 'fo', 'ox']
+    assert ngrams(sanitise('the quick brown fox'), 4) == ['theq', 'hequ', 
+        'equi', 'quic', 'uick', 'ickb', 'ckbr', 'kbro', 'brow', 
+        'rown', 'ownf', 'wnfo', 'nfox']
+    
\ No newline at end of file
diff --git a/tests/test_railfence.py b/tests/test_railfence.py
new file mode 100644 (file)
index 0000000..5e43f46
--- /dev/null
@@ -0,0 +1,55 @@
+import pytest
+import string 
+
+from szyfrow.railfence import *
+from szyfrow.support.utilities import *
+
+
+def test_encipher_message():
+    plaintext = 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
+    ciphertext = railfence_encipher(plaintext, 2, fillvalue='!')
+    expected = 'hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!'
+    assert ciphertext == expected
+    ciphertext = railfence_encipher(plaintext, 3, fillvalue='!')
+    expected = 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihr!!lhateihsnefttiaece!'
+    assert ciphertext == expected
+    ciphertext = railfence_encipher(plaintext, 5, fillvalue='!')
+    expected = 'hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!'
+    assert ciphertext == expected
+    ciphertext = railfence_encipher(plaintext, 10, fillvalue='!')
+    expected = 'hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!'
+    assert ciphertext == expected
+    ciphertext = railfence_encipher(plaintext, 3)
+    expected = 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece'
+    assert ciphertext == expected
+    ciphertext = railfence_encipher(plaintext, 5)
+    expected = 'hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp'
+    assert ciphertext == expected
+    ciphertext = railfence_encipher(plaintext, 7)
+    expected = 'haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic'
+    assert ciphertext == expected
+
+
+def test_decipher_message():
+    plaintext = 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
+    for key in range(2, 11):
+        enciphered = railfence_encipher(plaintext, key)
+        deciphered = railfence_decipher(enciphered, key)
+        assert deciphered == plaintext
+
+
+def test_railfence_break():
+    plaintext = sanitise('''It is a truth universally acknowledged, that a single man in
+             possession of a good fortune, must be in want of a wife. However
+             little known the feelings or views of such a man may be on his 
+             first entering a neighbourhood, this truth is so well fixed in
+             the minds of the surrounding families, that he is considered the
+             rightful property of some one or other of their daughters.''')
+    expected_key = 7
+    expected_score = Ptrigrams(plaintext)
+    ciphertext = railfence_encipher(plaintext, expected_key)
+    
+    key, score = railfence_break(ciphertext, fitness=Ptrigrams)
+
+    assert key == expected_key
+    assert score == pytest.approx(expected_score)
diff --git a/tests/test_utilities.py b/tests/test_utilities.py
new file mode 100644 (file)
index 0000000..9b84283
--- /dev/null
@@ -0,0 +1,74 @@
+import pytest
+import string 
+
+from szyfrow.support.utilities import *
+
+def test_pos():
+       for n, l in enumerate(string.ascii_lowercase):
+               assert n == pos(l)
+       for n, l in enumerate(string.ascii_uppercase):
+               assert n == pos(l)
+       with pytest.raises(ValueError):
+               pos('9')
+
+def test_unpos():
+       for n, l in enumerate(string.ascii_lowercase):
+               assert l == unpos(n)
+               assert l == unpos(n + 26)
+       for n, l in enumerate(string.ascii_uppercase):
+               assert l.lower() == unpos(n)
+       with pytest.raises(ValueError):
+               pos('9')
+
+def test_pad():
+    assert pad(10, 3, '*') == '**'
+    assert pad(10, 5, '*') == ''
+    assert pad(10, 3, lambda: '!') == '!!'
+
+def test_every_nth():
+       assert every_nth(string.ascii_lowercase, 5) == ['afkpuz', 'bglqv', 'chmrw', 'dinsx', 'ejoty']
+       assert every_nth(string.ascii_lowercase, 1) == ['abcdefghijklmnopqrstuvwxyz']
+       assert every_nth(string.ascii_lowercase, 26) == ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
+       assert every_nth(string.ascii_lowercase, 5, fillvalue='!') == ['afkpuz', 'bglqv!', 'chmrw!', 'dinsx!', 'ejoty!']
+
+def test_combine_every_nth():
+    for i in range(1, 27):
+        assert combine_every_nth(every_nth(string.ascii_lowercase, 5)) == string.ascii_lowercase
+
+
+def test_chunks_on_text():
+    assert chunks('abcdefghi', 3) == ['abc', 'def', 'ghi']
+    assert chunks('abcdefghi', 4) == ['abcd', 'efgh', 'i']
+    assert chunks('abcdefghi', 4, fillvalue='!') == ['abcd', 'efgh', 'i!!!']
+
+def test_chunks_on_nontext():
+    ns = [1,2,3,4,5,6,7,8,9]
+    assert chunks(ns, 3) == [[1,2,3],[4,5,6],[7,8,9]]
+
+def test_transpose():
+    assert transpose(['a', 'b', 'c', 'd'], (0,1,2,3)) == ['a', 'b', 'c', 'd']
+    assert transpose(['a', 'b', 'c', 'd'], (3,1,2,0)) == ['d', 'b', 'c', 'a']
+    assert transpose([10,11,12,13,14,15], (3,2,4,1,5,0)) == [13, 12, 14, 11, 15, 10]
+
+def test_untranspose():
+    assert untranspose(['a', 'b', 'c', 'd'], [0,1,2,3]) == ['a', 'b', 'c', 'd']
+    assert untranspose(['d', 'b', 'c', 'a'], [3,1,2,0]) == ['a', 'b', 'c', 'd']
+    assert untranspose([13, 12, 14, 11, 15, 10], [3,2,4,1,5,0]) == [10, 11, 12, 13, 14, 15]
+
+def test_letters():
+    assert letters('The Quick') == 'TheQuick'
+    assert letters('The Quick BROWN fox jumped! over... the (9lazy) DOG') == 'TheQuickBROWNfoxjumpedoverthelazyDOG'
+
+def test_unaccent():
+    assert unaccent('hello') == 'hello'
+    assert unaccent('HELLO') == 'HELLO'
+    assert unaccent('héllo') == 'hello'
+    assert unaccent('héllö') == 'hello'
+    assert unaccent('HÉLLÖ') == 'HELLO'
+
+def test_sanitise():
+    assert sanitise('The Quick') == 'thequick'
+    assert sanitise('The Quick BROWN fox jumped! over... the (9lazy) DOG') == 'thequickbrownfoxjumpedoverthelazydog'
+    assert sanitise('HÉLLÖ') == 'hello'
+
+    
\ No newline at end of file