2

I was having fun solving the riddles from the Pythonchallenge website

when I stumbled upon a weird behaviour:

With this input: *g fmnc wms bgblr rpylqjyrc gr zw fylb. rfyrq ufyr amknsrcpq ypc dmp. bmgle gr gl zw fylb gq glcddgagclr ylb rfyr'q ufw rfgq rcvr gq qm jmle. sqgle qrpgle.kyicrpylq() gq pcamkkclbcb. lmu ynnjw ml rfc spj.

We should be able to get the following output: *"i hope you didnt translate it by hand. thats what computers are for. doing it in by hand is inefficient and that's why this text is so long. using string.maketrans() is recommended. now apply on the url." *

Instead what we get when we decipher it with a simple ROT2 script is: i hope you didnt tr{nsl{te it |y h{nd0 th{ts wh{t computers {re for0 doing it in |y h{nd is inefficient {nd th{t)s why this text is so long0 using string0m{ketr{ns+ is recommended0 now {pply on the url0*

My ROT2 script I refer to is as follows:

user_input = input().split(' ')
newletter_int = 0
new_output = []

for word in user_input:
    newletter_int = 0 
    newstr = ''

    for letter in word:
        newletter = ord(letter) + 2
        newstr += chr(newletter)
    new_output.append(newstr)
print(" ".join(new_output))

This naturally happens of course, because the letter "y" has the order number of 121 and when we add 2 to 121 we get the character with order number 123 which is "{". But why would then the Python maketrans result in the correct character?

Please, note that I have solved the task with maketrans and what I am looking for is not a solution to the riddle, as I have been able to find it out myself. I am looking for a simple explanation what is the difference between the two methods. Also, please, do not refer to the pages where the solution is linked, as I am not looking for them, but for explanation of the difference of the functionalities between my script above and the string.maketrans() method and also an answer to the question why is this the recommended way to solve the riddle.

apingaway
  • 33
  • 1
  • 1
  • 17

2 Answers2

2

I'm assuming that while using str.maketrans you specifically created a mapping table between alphabets. That's exactly the point: you specify a 1-to-1 map between characters and so you're guaranteed that those transformations will happen as you've specified them.

Let's now take a look at your script:

  1. You're using ord, which returns an integer representing the Unicode code point of the character you pass it. This inherently means that we should have some basic knowledge about character encodings. For this problem you can ignore Unicode code points as we're dealing with characters that can be encoded using ASCII (the smart people that designed Unicode made sure that the first 256 codepoints are the same). In order to understand what's going on an ASCII table is going to be your best friend:

  1. Let's now look at the most important line of your ROT2 implementation: newletter = ord(letter) + 2. Taking a look at the table above, it should be clear why y is transformed into { or why . results in 0. Because of this, we need to be a little smarter about our implementation; specifically, we need to take a close look at the scenarios where we cross that bound. A common way to circunvent this is to use something like (ord(letter) - 97 + 2) % 26 + 97. I'll let you figure out why that works on your own.

  2. I see you're using str.split so you avoid transforming spaces. Unfortunately that's not going to be enough as the string you're transforming contains other non-letters like ' or .. I suggest you take a look at the constants provided by the string module.

As to why that might be the recommended way to solve the riddle, I'm guessing it's precisely because of all the effort involved with manually transforming characters using addition. As I've tried to illustrate with my answer, it's much simpler to specify a 1-to-1 character mapping and apply it directly to strings.

tlgs
  • 643
  • 6
  • 16
  • Hey @tlgs, thank you a lot for the informative and descriptive answer! I tried implementing the '''(ord(letter) - 97 + 2) % 26 + 97 ''' within a for cycle and the output now is almost correct; now the dot sign "." gets replaced by a "d" and the "'" by a "w". I am trying to find out why now and will update the comment as soon as I do. In the meantime if you have any idea, feel free to share :) "i hope you didnt translate it by handd thats what computers are ford doing it in by hand is inefficient and thatws why this text is so longd using stringdmaketransxy is recommendedd now apply.." – apingaway Jan 20 '22 at 13:46
  • Take a look at my third point. Just like you're not converting spaces, you should find a way to skip those characters. – tlgs Jan 20 '22 at 13:54
  • So... for the sake of experimenting and learning new things at every iteration, I found out the modular division by 26 for checking the overflow works perfectly fine for the purpose of building the output string that otherwise would be built without any hassle with maketrans() as long as I ignore the special chars, e.g. by including them within an else statement to go past them when encountered. ` if ord(letter) >= 97: newletter = (ord(letter) - 97 + 2) % 26 + 97 else: newletter = (ord(letter)) newstr += chr(newletter)` now works too! – apingaway Jan 20 '22 at 13:57
  • Yes, thanks, I was just writing back as you pointed out the third point. – apingaway Jan 20 '22 at 14:01
  • Happy you found a working solution! However, that approach still won't work correctly if for example your string contains `{` or `}`. I was hinting at using something like `if letter in string.ascii_lowercase: ... else: ...`. – tlgs Jan 20 '22 at 14:05
1

But why would then the Python maketrans result in the correct character?

makeTrans simply provides a method of iterating over the characters; in the end the passed mapping is still up to the dictionary / input parameters provided.

If it is passed a mapping that maps Y to A for encryption and A to Y for decryption then makeTrans obviously works fine, as the dictionary already contains the wrap-around that you forgot to apply.

So the function you define maps:

ABCDEFGHIJKLMNOPQRSTUVWXYZ
CDEFGHIJKLMNOPQRSTUVWXYZ{|

while it should perform the following mapping for encryption:

ABCDEFGHIJKLMNOPQRSTUVWXYZ
CDEFGHIJKLMNOPQRSTUVWXYZAB

and makeTrans simply recieves the right mapping (and leaves the other characters as is, if you don't specify a third parameter).


Note that the nice way to perform the wrap around is to convert the letter to an index into the provided alphabet, and then to perform the wrapping by using the modulus operator: % instead of operating on the ASCII value directly (so A maps to 0 instead of decimal 97).

Maarten Bodewes
  • 90,524
  • 13
  • 150
  • 263