Michel85
Michel85

Reputation: 317

ROT(n) encoder and decoder, but decoder not working

I know there are a lot of ways to write a ROT(n) function. But I don't want to have some table with chars.

So, I've tried to write a simple ROT(n) with decoder, as practice project. The encode function works fine. But the decoder keeps changing the 'a' into a 'z'.

Could somebody please explain to me what I'm doing wrong?

The (Python3) code below changes everything into lowercase ignoring any special chars.

import random
import string

shift = random.randint(1, 20)


# Encoder:
def encode(string):
    coded_string = []
    string = string.lower()
    for c in string:
        if ord(c) >= 97 and ord(c) <= 122:
            c = (ord(c) + shift) % 122
            if c <= 97:
                c += 97
            coded_string.append(chr(c))
            continue
        coded_string.append(c)
    return ''.join(coded_string)


# Decoder:
def decode(string):
    decoded_string = []
    for c in string:
        if ord(c) >= 97 and ord(c) <= 122:
            if ord(c) - shift <= 97:
                c = (ord(c) % 97) + (122 - shift)
                decoded_string.append(chr(c))
                continue
            c = ord(c) - shift
            decoded_string.append(chr(c))
            continue
        decoded_string.append(c)
    return ''.join(decoded_string)


# Test Function:
def tryout(text):
    test = decode(encode(text))
    try:
        assert test == text, 'Iznogoedh!'
    except AssertionError as AE:
        print(AE, '\t', test)
    else:
        print('Yes, good:', '\t', test)


# Random text generator:
def genRandomWord(n):
    random_word = ''
    for i in range(n):
        random_word += random.choice(string.ascii_lowercase)
    return random_word


# Some tests:
print(f'Shift: {shift}')
tryout('pokemon')
tryout("chip 'n dale rescue rangers")
tryout('Ziggy the Vulture or Zurg')
tryout('Fine # (*day%, to* code@ in Pyth0n3!')
tryout(genRandomWord(10))
tryout(genRandomWord(20))

Example output:

Shift: 7
Yes, good:   pokemon
Iznogoedh!   chip 'n dzle rescue rzngers
Iznogoedh!   ziggy the vulture or zurg
Iznogoedh!   fine # (*dzy%, to* code@ in pyth0n3!
Yes, good:   qrwmfyogjg
Yes, good:   ihrcuvzyznlvghrtnuno

but, ignoring the random string tests, I expected:

Shift: 7
Yes, good:   pokemon
Yes, good:   chip 'n dale rescue rangers
Yes, good:   ziggy the vulture or zurg
Yes, good:   fine # (*day%, to* code@ in pyth0n3!

Upvotes: 1

Views: 756

Answers (1)

Martijn Pieters
Martijn Pieters

Reputation: 1121864

First of all, your tryout() test function forgets to lowercase the input, so it fails for your Ziggy example where that actually passes; the corrected test is:

# Test Function:
def tryout(text):
    test = decode(encode(text))
    try:
        assert test == text.lower(), 'Iznogoedh!'
    except AssertionError as AE:
        print(AE, '\t', test)
    else:
        print('Yes, good:', '\t', test)

The error is in your decode function; for a shift of 7, you can see that the encoded letter for a -> h doesn't map back correctly, while i (from b), does work:

>>> decode('h')
'z'
>>> decode('i')
'b'

However, the error goes further; each of the first 7 letters are mistranslated; g maps to y, f maps to x, etc. If you use a lower shift that's easy to see:

>>> for encoded in 'abcd': print(decode(encoded), end=' ')
... else: print()
...
w x y z

Those should have mapped back to x, y, z and a. So this is a off-by-one error, and it is in your test here:

if ord(c) - shift <= 97:

When shift is 3, and c is d, ord(c) - shift is equal to 97, and should not be adjusted. Change the <= to <:

if ord(c) - shift < 97:

So the fixed decode() function then becomes:

def decode(string):
    decoded_string = []
    for c in string:
        if ord(c) >= 97 and ord(c) <= 122:
            if ord(c) - shift < 97:
                c = (ord(c) % 97) + (122 - shift)
                decoded_string.append(chr(c))
                continue
            c = ord(c) - shift
            decoded_string.append(chr(c))
            continue
        decoded_string.append(c)
    return ''.join(decoded_string)

You may want to learn about the % modulo operator here, which can help 'wrap around' values to fit within a range, like the range of values for the letters a through z.

If you take the ASCII codepoint, subtract 97, then use the adjusted value (minus or plus shift, depending on encoding or decoding) and then wrap the resulting value with % 26, you always come out on the 'other side' and can add the result back to 97:

>>> ord('a') - 97   # a is the 'zeroth' letter in the alphabet, z is the 25th
0
>>> ord('a') - 97 - shift   # shifted by 3 puts it outside the 0 - 25 range
-3
>>> (ord('a') - 97 - shift) % 26  # modulo 26 puts it back in the range, from the end
23
>>> chr((ord('a') - 97 - shift) % 26 + 97)  # add 97 back on to go back to the decoded letter
'x'

Another 'trick' is to use a bytes object, by encoding your input to, say, UTF-8. bytes objects are sequences of integers, already processed by the ord() function, so to say. Just loop and apply the shift to the bytes in the right range, and append these integers a list. You can then create a new bytes object from the list and decode back to a string:

def shift_by_n(n, value):
    as_bytes = value.lower().encode('utf8')
    encoded = []
    for v in as_bytes:
        if 97 <= v <= 122:
            v = ((v - 97 + n) % 26) + 97
        encoded.append(v)
    return bytes(encoded).decode('utf8')

The above function can work for both encoding and decoding, simply by passing in the shift as a positive or negative value:

def encode(string):
    return shift_by_n(shift, string)

def decode(string):
    return shift_by_n(-shift, string)

Finally, rather that test each single letter, you could use the str.translate() function which, given a translation table, makes all replacements for you. You can trivially build a ROT(n) translation table with the str.maketrans() static method. Encoding is simply the alphabet mapped to the same alphabet but with shift characters from the start taken off and added to the end:

alphabet = 'abcdefghijklmnopqrstuvwxyz'

def encode(string):
    # take all letters except the first 'shift' characters, and
    # add those letters to the end instead
    rotated = alphabet[shift:] + alphabet[:shift]
    translate_map = str.maketrans(alphabet, rotated)
    return string.lower().translate(translate_map)

Decoding uses the same rotated string, but the order of the arguments to str.maketrans() is swapped:

def decode(string):
    # take all letters except the first 'shift' characters, and
    # add those letters to the end instead
    rotated = alphabet[shift:] + alphabet[:shift]
    translate_map = str.maketrans(rotated, alphabet)
    return string.translate(translate_map)

Making the above functions work with uppercase letters too, only requires that you concatenate the alphabet.upper() and rotated.upper() results to alphabet and rotated, respectively, when calling str.maketrans() (and remove the .lower() call in encode()). I'll leave that up to the reader to implement.

Upvotes: 2

Related Questions