changeset 8:9e44ad188f4a

Started work on the Purple97 machine class.
author Brian Neal <bgneal@gmail.com>
date Wed, 27 Nov 2013 18:31:48 -0600
parents 9e43a627ba5d
children 46e1c1e6b564
files purple/machine.py purple/tests/test_machine.py
diffstat 2 files changed, 185 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/purple/machine.py	Wed Nov 27 18:31:48 2013 -0600
@@ -0,0 +1,119 @@
+# Copyright (C) 2013 by Brian Neal.
+# This file is part of purple, the PURPLE (Cipher Machine 97) simulation.
+# purple is released under the MIT License (see LICENSE.txt).
+
+"""This module contains the Purple97 class, the top level class in the PURPLE
+simulation.
+
+"""
+from collections import Counter
+import string
+
+import purple.switch as switch
+
+
+class Purple97Error(Exception):
+    """Exception class for all Purple97 errors"""
+
+
+class Purple97:
+    """This class simulates the top-level behavior of the PURPLE cipher
+    machine.
+
+    """
+    def __init__(self, switches_pos=None, fast_switch=1, middle_switch=2,
+            alphabet=None):
+        """Build a PURPLE (Cipher Machine 97) instance. Initial settings can be
+        optionally supplied.
+
+        switches_pos: If not None, must be a 4-element list or tuple of integer
+        starting switch positions. Each position must be in the range of 0-24,
+        inclusive. If None, a list of all 0's is assumed. The first element in
+        the list is the starting position for the sixes switch. The second
+        through fourth elements are the starting positions for the three
+        twenties switches, 1 through 3.
+
+        fast_switch: this integer parameter names which twenties switch (1-3) is
+        assuming the role of the fast switch.
+
+        middle_switch: this integer parameter names which twenties switch (1-3)
+        has been designated as the middle switch.
+
+        Passing in the same value for both the fast and medium switches will
+        raise a Purple97Error exception. The slow switch is assumed to be the
+        remaining twenties switch that was not named.
+
+        alphabet: this parameter must be a 26-letter sequence that represents
+        the daily alphabet setting. It describes how the typewriters are wired
+        to the plugboard. The first six characters are the mapping for the sixes
+        switch, and the remaining 20 are for the input wiring for the first
+        stage of the twenties switches. If None is supplied, a straight through
+        mapping is assumed; i.e. "AEIOUYBCDFGHJKLMNPQRSTVWXZ".
+
+        The alphabet parameter will accept either upper or lowercase letters.
+        All 26 distinct letters must be present or else a Purple97Error
+        exception will be raised.
+
+        """
+        # If no switch positions are supplied, default to all 0's
+        if switches_pos is None:
+            switches_pos = (0, 0, 0, 0)
+
+        # Validate switch positions
+        try:
+            n = len(switches_pos)
+        except TypeError:
+            raise Purple97Error("switches_pos must be a sequence")
+        if n != 4:
+            raise Purple97Error("switches_pos must have length of 4")
+
+        # Create switches with correct starting positions
+        self.sixes = switch.create_switch(switch.SIXES, switches_pos[0])
+        self.twenties = [
+            switch.create_switch(switch.TWENTIES_1, switches_pos[1]),
+            switch.create_switch(switch.TWENTIES_2, switches_pos[2]),
+            switch.create_switch(switch.TWENTIES_3, switches_pos[3]),
+        ]
+
+        # Validate fast & middle switch parameters
+        if not (1 <= fast_switch <= 3):
+            raise Purple97Error("fast_switch out of range (1-3)")
+        if not (1 <= middle_switch <= 3):
+            raise Purple97Error("middle_switch out of range (1-3)")
+        if fast_switch == middle_switch:
+            raise Purple97Error("fast & middle switches cannot be the same")
+
+        # Store references to the fast, middle, and slow switches
+        self.fast_switch = self.twenties[fast_switch - 1]
+        self.middle_switch = self.twenties[middle_switch - 1]
+
+        # Pick the remaining switch as the slow switch
+        switches = [1, 2, 3]
+        switches.remove(fast_switch)
+        switches.remove(middle_switch)
+        self.slow_switch = self.twenties[switches[0] - 1]
+
+        # Validate the alphabet
+        if alphabet is None:
+            alphabet = 'AEIOUYBCDFGHJKLMNPQRSTVWXZ'
+
+        alphabet = alphabet.upper()
+        if len(alphabet) != 26:
+            raise Purple97Error("invalid alphabet length")
+
+        # Count valid letters
+        ctr = Counter(string.ascii_uppercase)
+        for c in alphabet:
+            if c in ctr:
+                ctr[c] += 1
+
+        # At this point if alphabet is legal, all keys in ctr must have a value
+        # of 2. If any are 1, then alphabet was missing some letters. If any are
+        # greater than 2, we had duplicate letters.
+
+        if not all(v == 2 for v in ctr.values()):
+            raise Purple97Error("invalid alphabet")
+
+        self.alphabet = alphabet
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/purple/tests/test_machine.py	Wed Nov 27 18:31:48 2013 -0600
@@ -0,0 +1,66 @@
+# Copyright (C) 2013 by Brian Neal.
+# This file is part of purple, the PURPLE (Cipher Machine 97) simulation.
+# purple is released under the MIT License (see LICENSE.txt).
+
+import string
+import unittest
+
+from purple.machine import Purple97, Purple97Error
+from purple.switch import SteppingSwitchError
+
+
+class Purple97TestCase(unittest.TestCase):
+
+    def test_construction(self):
+
+        Purple97()
+        Purple97([0, 1, 2, 3])
+        Purple97([0, 1, 2, 3], 1)
+        Purple97([0, 1, 2, 3], 2, 1)
+        Purple97((0, 1, 2, 3), 2, 1, string.ascii_uppercase)
+        Purple97((0, 1, 2, 3), 2, 1, string.ascii_lowercase)
+        Purple97(alphabet=string.ascii_lowercase)
+        Purple97(fast_switch=3, middle_switch=1)
+
+    def test_construct_bad_positions(self):
+
+        self.assertRaises(Purple97Error, Purple97, [])
+        self.assertRaises(Purple97Error, Purple97, [1])
+        self.assertRaises(Purple97Error, Purple97, [1, 1, 1, 1, 1])
+        self.assertRaises(SteppingSwitchError, Purple97, [1, 1, 1, 100])
+
+    def test_construct_bad_switches(self):
+
+        self.assertRaises(Purple97Error, Purple97, fast_switch=0)
+        self.assertRaises(Purple97Error, Purple97, fast_switch=4)
+        self.assertRaises(Purple97Error, Purple97, fast_switch=-1)
+
+        self.assertRaises(Purple97Error, Purple97, middle_switch=0)
+        self.assertRaises(Purple97Error, Purple97, middle_switch=4)
+        self.assertRaises(Purple97Error, Purple97, middle_switch=-1)
+
+        self.assertRaises(Purple97Error, Purple97, fast_switch=2)
+        self.assertRaises(Purple97Error, Purple97, fast_switch=1, middle_switch=1)
+        self.assertRaises(Purple97Error, Purple97, fast_switch=2, middle_switch=2)
+        self.assertRaises(Purple97Error, Purple97, fast_switch=3, middle_switch=3)
+
+        self.assertRaises(Purple97Error, Purple97, fast_switch=0, middle_switch=1)
+        self.assertRaises(Purple97Error, Purple97, fast_switch=0, middle_switch=0)
+        self.assertRaises(Purple97Error, Purple97, fast_switch=0, middle_switch=4)
+
+    def test_construct_bad_alphabet(self):
+
+        alpha = ''
+        self.assertRaises(Purple97Error, Purple97, alpha)
+        alpha = '1'
+        self.assertRaises(Purple97Error, Purple97, alpha)
+        alpha = '!' * 26
+        self.assertRaises(Purple97Error, Purple97, alpha)
+        alpha = string.ascii_uppercase[3:]
+        self.assertRaises(Purple97Error, Purple97, alpha)
+        alpha = string.ascii_uppercase + string.ascii_uppercase[4:10]
+        self.assertRaises(Purple97Error, Purple97, alpha)
+        alpha = string.ascii_uppercase[:13] + string.ascii_uppercase[:13]
+        self.assertRaises(Purple97Error, Purple97, alpha)
+        alpha = 'M' * 26
+        self.assertRaises(Purple97Error, Purple97, alpha)