tdd-deciphered.com

created by @parsingphase

Chapter 9: Putting the rotors in place

In the last chapter, we got the rotors working. Now, we'll work on the framework they sit in, starting with the rotor slots

As we did with the Rotors themselves, we need to analyse the behaviour we need from the slots: - Any time a letter is sent to the right-hand rotor in slot 1, it will be turned over by one position before the electrical signal is sent. - If the rotor in a slot is in a position to engage the rotor shift mechanism (aka the pre-turnover position), and it turns over, it will cause the rotor to its left to turn over before the electrical signal is passed. - The position of the rotor in the slot must also be taken into account in routing the electrical signal.

From this, we can see that each slot will need to know: - What position it's in (ie, which slot number it is) - Whether its rotor is in the pre-turnover position - Whether the rotor to its right (if it has one) will cause it to turn over - What rotary position its rotor is in.

We can best model the behaviour of this system in two phases; the mechanical phase and the electrical phase. We can test these both separately and together, and we can also test the slots separately and as a set.

Once we can test both phases for the whole set of rotors, we'll have simulated the complete device except for the reflector and plugboard, both of which are comparatively simple elements.

The easiest place for us to start is with the electrical effects of the rotary position of a rotor in a slot, which is functionally the same as the position of the rotor's core within the ring. We can use the data and notation already seen above to designate what happens when we drop Rotor I, with its core set to an offset of 26/Z, into a slot at position "A" (the default position). We'll extend the diagrams we used before:

If we input a 'V', we get:

Diagram 4

ie input V, rotor offset A, ring offset Z, output A

If we rotate the wheel two steps (keeping the core position), we get:

Diagram 5

ie input V, rotor offset C, ring offset Z, output Z

This gives up two testable datasets; if we imagine we need to have a RotorSlot class into which we need to load a Rotor with a given Rotor Offset (distinct from the rotor’s Ring Offset), we might want to be able to run the following test, which we’ll put in a new file of tests/Phase/Enigma/RotorTest.php:

class RotorSlotTest extends \PHPUnit_Framework_TestCase {

/**
 * @dataProvider singleSlotRotorOneRingOffsetOneDataProvider
 * @param $slotInput
 * @param $rotorOffset
 * @param $ringOffset
 * @param $output
 */
public function testSingleSlotRotorOneRingOffsetOne($slotInput, $rotorOffset, $ringOffset, $output)
{
    $rotor = new Rotor();
    $coreMapping = [ // Rotor I
        'A'=>'E','B'=>'K','C'=>'M','D'=>'F','E'=>'L','F'=>'G','G'=>'D',
        'H'=>'Q','I'=>'V','J'=>'Z','K'=>'N','L'=>'T','M'=>'O','N'=>'W',
        'O'=>'Y','P'=>'H','Q'=>'X','R'=>'U','S'=>'S','T'=>'P','U'=>'A',
        'V'=>'I','W'=>'B','X'=>'R','Y'=>'C','Z'=>'J'
    ];

    $rotor->setRingOffset($ringOffset);
    $rotor->setCoreMapping($coreMapping);

    $slot = new RotorSlot();
    $slot->loadRotor($rotor);
    $slot->setRotorOffset($rotorOffset);

    $slotOutput = $slot->getOutputCharacterForInputCharacter($slotInput);

    $this->assertSame($output, $slotOutput);
}

public function singleSlotRotorOneRingOffsetOneDataProvider()
{
    return[
        //$slotInput, $rotorOffset, $ringOffset, $output
        ['V','A','Z','A'],
        ['V','C','Z','Z'],
    ];
}

}

We’ll move a little faster now, as you probably don’t need me to explain every tiny test-fail-fix iteration now… you can also try implementing the RotorSlot class that meets the above tests yourself, but here’s my version:

<?php

namespace Phase\Enigma;

class RotorSlot {

/**
 * @var Rotor
 */
protected $rotor;
/**
 * @var int
 */
protected $rotorOffset;

public function loadRotor(Rotor $rotor)
{
    $this->rotor = $rotor;
}

public function setRotorOffset($offset)
{
    if (preg_match('/^[A-Z]$/', $offset)) {
        $offset = $this->charToAlphabetPosition($offset);
    } else {
        if (!is_integer($offset) || ($offset < 1) || ($offset > 26)) {
            throw new \InvalidArgumentException("Offset must be integer in range 1..26");
        }
    }

    $this->rotorOffset = $offset;
}

public function getOutputCharacterForInputCharacter($inputCharacter)
{
    $inputCharacter = strtoupper($inputCharacter);
    $rotorInputCharacter = $this->getCharacterOffsetBy(
        $inputCharacter,
        $this->rotorOffset - 1
    );
    $rotorOutputCharacter = $this->rotor->getOutputCharacterForInputCharacter(
        $rotorInputCharacter
    );
    $outputCharacter = $this->getCharacterOffsetBy(
        $rotorOutputCharacter,
        0 - ($this->rotorOffset - 1)
    );

    return ($outputCharacter);
}

protected function getCharacterOffsetBy($character, $offset)
{
    $charAsInt =
        $this->charToAlphabetPosition($character);

    $newInteger = (26 + $charAsInt + $offset) % 26; // IMPORTANT! offsets work the other way from rings!

    if ($newInteger == 0) {
        $newInteger = 26;
    }

    $newCharacter =
        $this->alphabetPositionToCharacter($newInteger);

    return ($newCharacter);
}

protected function charToAlphabetPosition($char)
{
    $position = (ord($char) - 64);

    return $position;
}

protected function alphabetPositionToCharacter($position)
{
    $char = (chr($position + 64));

    return $char;
}

}

Most of this will be familiar from the equivalent code for the Rotor and its ring setting, and indeed we can copy most of that code. However, there’s one caveat - because we’re looking at the position of the thing that’s being moved as giving the offset, rather than (as for the ring) the position of what it’s being moved against, offsets work the other way.

We can refactor our code to make that a bit clearer (and reduce duplication) later, and when we do so we’ll have regression tests to protect us against any potential bugs.

We’ve dropped a lot of both theory and code in this chapter, so we’ll wrap up for now. The latest state of the code is tagged as Chapter9-1-RotorSlots