Purpose of moduo operator in coded_correspondence

I understand everything up until the lines that use the modulo operator. I do not understand how letter_pointer = (letter_pointer + 1) % len(keyword) shift past the keyword. Same with translated_message += alphabet[letter_value & len(alphabet)]. Can someone explain to me what is happening please?

This modulo operation, or more significantly, remainder operation results from integer division.

D % d  ==  D - D // d

where D is the Dividend, and d is the divisor. // is floor division which between two integers is also an integer. D and d are both integers.

Now, also significantly, this will always give us an integer less than d, and not less than 0. From this we end up with a sequence, 0, 1, 2, .., d - 1, 0, 1, .., d - 1, ...

Given this is very predictable means we can apply in situations that overflow, such as the cipher. When we shift the ordinal of ‘Z’ by, say, 13, it no longer has an associated alpha character. By using the remainder, we can wrap that overflow to the beginning of the alpha group. The remainder becomes an offset.

>>> K = 13
>>> H = 65
>>> chr((ord('A') - H + K) % 26 + H)
>>> chr((ord('N') - H + K) % 26 + H)

It’s this mechanics that keeps everything within the alpha range.

>>> h = 97
>>> chr((ord('a') - h + K) % 26 + h)
>>> chr((ord('n') - h + K) % 26 + h)

From this we can derive a function that is the inverse of itself and can encode and decode on two passes.

def endecode(x, k = 13):
    if not isalpha(x): raise ValueError
    h = 65 if ord(x) < 97 else 97
    return chr((ord(x) - h + k) % 26 + h)

>>> endecode('A')
>>> endecode('N')
>>> endecode('z')
>>> endecode('m')

The above example would be deemed a helper function since it would be called on every letter in a msg. Because we have a constraint saying that the input MUST BE an alpha character, it means we do not have the capacity to deal with spaces or other non-alpha, meaning we cannot distinguish between words if this is the only tool we have.

Now if we look at a word, which does not contain spaces, our function is perfectly reliable.

>>> word = "cipher"
>>> msg = "".join([endecode(x) for x in word])
>>> msg
>>> word = "".join([endecode(x) for x in msg])
>>> word

That gives rise to another helper function…

def endeword(w):
    return "".join([endecode(x) for x in w])

>>> endeword('cipher')
>>> endeword('pvcure')

The final icing on the cake will be a function that takes a message, with spaces, and puts these two helpers through their paces. It will also be a two-pass (inverse) function.

And here it is…

def endemsg(m):
    return ' '.join([endeword(x) for x in m.split()])

>>> endemsg("Hello World")
'Uryyb Jbeyq'
>>> endemsg('Uryyb Jbeyq')
'Hello World'

Some touching up will be necessary, especially bringing the k (the key) parameter to the msg level. The logic is mostly there. What if the message requires a full stop? That would have to be engineered into the code. Other special characters may be able to ride that train.

'Xbz gbb irqqvat'
'Bytn Ze'

How about rolling all these functions into a class definition? Another avenue yet to be explored. If your interest is piqued, then segue into this study and see what is to be learned, of course shared with the rest of us, that is.

Moving along, a class has emerged…

class Msg:
    def  __init__(self, msg, key=13):
        self.msg = msg
        self.key = key
    def __repr__(self):
        return self.endemsg()
    def endecode(self, x):
        h = 65 if ord(x) < 97 else 97
        return chr((ord(x) - h + self.key) % 26 + h)
    def endeword(self, w):
        return "".join([self.endecode(x) for x in w])
    def endemsg(self, m):
        return ' '.join([self.endeword(x) for x in self.msg.split()])

msg = Msg("Hello World")
print (msg)

msg = Msg("Uryyb Jbeyq")
print (msg)