# HG changeset patch # User Brian Neal # Date 1369861110 18000 # Node ID 39b2db64fcf28b8a832b38b060963e436889a375 KeyWheel class and tests, first cut. diff -r 000000000000 -r 39b2db64fcf2 .hgignore --- /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 diff -r 000000000000 -r 39b2db64fcf2 LICENSE.txt --- /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. diff -r 000000000000 -r 39b2db64fcf2 m209/__init__.py --- /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 diff -r 000000000000 -r 39b2db64fcf2 m209/key_wheel.py --- /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)) diff -r 000000000000 -r 39b2db64fcf2 m209/tests/test_key_wheel.py --- /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())