changeset 0:39b2db64fcf2

KeyWheel class and tests, first cut.
author Brian Neal <bgneal@gmail.com>
date Wed, 29 May 2013 15:58:30 -0500
parents
children 8b51044f9c94
files .hgignore LICENSE.txt m209/__init__.py m209/key_wheel.py m209/main.py m209/tests/__init__.py m209/tests/test_key_wheel.py
diffstat 5 files changed, 246 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Wed May 29 15:58:30 2013 -0500
@@ -0,0 +1,5 @@
+syntax: glob
+*.pyc
+*.swp
+m209/docs/build
+dist
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LICENSE.txt	Wed May 29 15:58:30 2013 -0500
@@ -0,0 +1,18 @@
+Copyright (c) 2013 by Brian Neal
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 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.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/m209/__init__.py	Wed May 29 15:58:30 2013 -0500
@@ -0,0 +1,9 @@
+# Copyright (C) 2013 by Brian Neal.
+# This file is part of m209, the M-209 simulation.
+# m209 is released under the MIT License (see LICENSE.txt).
+
+__version__ = '0.1'
+
+class M209Error(Exception):
+    """Base Exception class for all M209 errors"""
+    pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/m209/key_wheel.py	Wed May 29 15:58:30 2013 -0500
@@ -0,0 +1,107 @@
+# Copyright (C) 2013 by Brian Neal.
+# This file is part of m209, the M-209 simulation.
+# m209 is released under the MIT License (see LICENSE.txt).
+
+"""This module contains the key wheel related classes for the M209
+simulation.
+
+"""
+from . import M209Error
+
+class KeyWheelError(M209Error):
+    """Exception class for all key wheel errors"""
+    pass
+
+
+class KeyWheel:
+    """Simulates a key wheel in a M209 converter"""
+
+    def __init__(self, letters, guide_letter, effective_pins=None):
+        """Initialize a KeyWheel instance:
+
+        letters - an iterable of letters which appear on the wheel face
+
+        guide_letter - must be a letter that appears within the letters
+        parameter. It indicates which letter affects the guide arm when the
+        first letter in letters is displayed to the operator.
+
+        effective_pins - see the description of set_pins(), below.
+
+        """
+        self.letters = list(letters)
+        self.num_pins = len(self.letters)
+
+        self.letter_offsets = {letter : n for n, letter in enumerate(self.letters)}
+
+        if self.num_pins < 1:
+            raise KeyWheelError("Too few key wheel letters")
+
+        # pin effectivity list:
+        if effective_pins:
+            self.set_pins(effective_pins)
+        else:
+            self.reset_pins()
+
+        try:
+            self.guide_offset = self.letter_offsets[guide_letter]
+        except KeyError:
+            raise KeyWheelError("Invalid guide_letter")
+
+        # rotational position; 0 means first letter shown to operator
+        self.pos = 0
+
+    def reset_pins(self):
+        """Reset all pins to the ineffective state."""
+        self.pins = [False] * self.num_pins
+
+    def set_pins(self, effective_pins):
+        """Sets which pins are effective.
+
+        effective_pins - must be an iterable of letters whose pins are slide to
+        the "effective" position (to the right). Letters not appearing in this
+        sequence are considered to be in the "ineffective" position (to the
+        left). If None or empty, all pins are set to be ineffective.
+
+        """
+        self.reset_pins()
+        for letter in effective_pins:
+            try:
+                n = self.letter_offsets[letter]
+            except KeyError:
+                raise KeyWheelError("Invalid pin: {}".format(letter))
+            self.pins[n] = True
+
+    def rotate(self, steps=1):
+        """Rotate the key wheel the given number of steps."""
+        self.pos = (self.pos + steps) % self.num_pins
+
+    def display(self):
+        """Returns the letter shown to the operator based on the current key
+        wheel position.
+
+        """
+        return self.letters[self.pos]
+
+    def guide_letter(self):
+        """Returns the letter of the pin that is in position to effect the guide
+        arm. To check to see if this pin will effect the guide arm, call
+        is_effective().
+
+        """
+        n = (self.pos + self.guide_offset) % self.num_pins
+        return self.letters[n]
+
+    def is_effective(self):
+        """Returns True if the key wheel, in the current position, has a pin in
+        the effective position, and False otherwise.
+
+        """
+        n = (self.pos + self.guide_offset) % self.num_pins
+        return self.pins[n]
+
+    def set_pos(self, c):
+        """Sets the position of the key wheel to the letter c."""
+        try:
+            self.pos = self.letter_offsets[c]
+        except KeyError:
+            raise KeyWheelError("Invalid position {}".format(c))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/m209/tests/test_key_wheel.py	Wed May 29 15:58:30 2013 -0500
@@ -0,0 +1,107 @@
+# Copyright (C) 2013 by Brian Neal.
+# This file is part of m209, the M-209 simulation.
+# m209 is released under the MIT License (see LICENSE.txt).
+
+"""test_key_wheel.py - Unit tests for the KeyWheel class for the M-209
+simulation.
+
+"""
+import random
+import string
+import unittest
+
+from ..key_wheel import KeyWheel, KeyWheelError
+
+
+class KeyWheelTestCase(unittest.TestCase):
+    """Basic tests to verify KeyWheel functionality"""
+
+    def test_bad_init(self):
+        self.assertRaises(KeyWheelError, KeyWheel, [], 'X')
+        self.assertRaises(KeyWheelError, KeyWheel, '', 'X')
+        self.assertRaises(KeyWheelError, KeyWheel, 'A', 'X')
+        self.assertRaises(KeyWheelError, KeyWheel, 'ABCDEFG', 'X')
+        self.assertRaises(KeyWheelError, KeyWheel, 'ABCDEFG', 'G', 'X')
+        self.assertRaises(KeyWheelError, KeyWheel, 'ABCDEFG', 'G', 'AX')
+        self.assertRaises(KeyWheelError, KeyWheel, 'ABCDEFG', 'G', 'AXG')
+
+    def test_reset_pins(self):
+        letters = string.ascii_uppercase
+        kw = KeyWheel(letters, 'F', 'ACEGHLMQSXY')
+        kw.reset_pins()
+        for c in letters:
+            self.assertFalse(kw.is_effective())
+            kw.rotate()
+
+        kw = KeyWheel(letters, 'P', letters)
+        kw.reset_pins()
+        for c in letters:
+            self.assertFalse(kw.is_effective())
+            kw.rotate()
+        for c in letters:
+            self.assertFalse(kw.is_effective())
+            kw.rotate()
+
+    def test_set_pins(self):
+        letters = string.ascii_uppercase
+        kw = KeyWheel(letters, 'P', letters)
+        for c in letters:
+            self.assertTrue(kw.is_effective())
+            kw.rotate()
+
+        kw = KeyWheel(letters, 'P')
+        kw.set_pins(letters)
+        for c in letters:
+            self.assertTrue(kw.is_effective())
+            kw.rotate()
+
+        kw = KeyWheel(letters, 'P')
+        kw.set_pins([])
+        for c in letters:
+            self.assertFalse(kw.is_effective())
+            kw.rotate()
+
+        kw = KeyWheel(letters, 'P')
+        for n in range(0, len(letters) + 1):
+            pins = random.sample(letters, n)
+            kw.set_pins(pins)
+            for c in letters:
+                if kw.guide_letter() in pins:
+                    self.assertTrue(kw.is_effective())
+                else:
+                    self.assertFalse(kw.is_effective())
+                kw.rotate()
+
+    def test_set_pins_bad(self):
+        letters = 'ABCDEFG'
+        kw = KeyWheel(letters, 'F')
+        self.assertRaises(KeyWheelError, kw.set_pins, 'X')
+        self.assertRaises(KeyWheelError, kw.set_pins, 'AX')
+        self.assertRaises(KeyWheelError, kw.set_pins, 'XA')
+        self.assertRaises(KeyWheelError, kw.set_pins, 'AXFG')
+
+    def test_rotate_display(self):
+        letters = string.ascii_uppercase
+        kw = KeyWheel(letters, 'P')
+        for n in range(5):
+            for c in letters:
+                self.assertTrue(c == kw.display())
+                kw.rotate()
+
+        for n in range(5):
+            for c in letters[::2]:
+                self.assertTrue(c == kw.display())
+                kw.rotate(2)
+
+        for n in range(5):
+            for c in letters[1::2]:
+                kw.rotate(1)
+                self.assertTrue(c == kw.display())
+                kw.rotate(1)
+
+    def test_set_pos(self):
+        letters = string.ascii_uppercase
+        kw = KeyWheel(letters, 'P')
+        for c in letters:
+            kw.set_pos(c)
+            self.assertEqual(c, kw.display())