EX02 - Basic Functions


Introduction

In this lab you will gain practice writing functions by implementing Julius Caesar’s cipher to encode and decode secret 4-letter strings. For example, the string "code" encodes to "dpef". In a later exercise you will improve your functions to support encoding and decoding str values of any length. Soon you’ll be able to send your crush secret love messages on zoom.

Before beginning, be sure you have completed Lesson 8 on Functions, Lesson 9 on Constants, and handed-in their respective questions on Gradescope.

The Caesar Cipher

The Caesar Cipher is one of the simplest ways to encode a message. It’s easily cracked. The idea is each letter is replaced by a letter some offset away from it in the alphabet. For our purposes, we will assume an offset shift of 1.

Thus, the mappings are:

a -> b
b -> c
c -> d
....
y -> z
z -> a

Notice when z is encoded it “wraps around” to a.

ASCII Encoding and the ord function

Under the hood our computer uses numeric codes to represent characters, or strs of length one. The standard encoding is ASCII (“American Standard Code for Information Exchange”) which uses 7 binary digits (0s and 1s) to reresent each character. You’ll learn about this more in later classes, so no worries about the details here. The table of codes can be found here: https://en.wikipedia.org/wiki/ASCII#Printable_characters. In the table, the Dec column is the int value of each character and the Glyph column contains the character representation. Scroll down to Dec 97 to see the lowercase range.

For this exercise, you will make use of the built-in ord. The ord function is short for “ordinal” and it gives the numerical int ordering of a single-character str in ASCII. For example, the character n has corresponds to the int value 110 in ASCII per the table linked above. You can confirm this with the following demo code:

>>> character: str = "n"
>>> ascii_code: int = ord(character)
>>> print(ascii_code)
110

Encoding a character and the chr function.

According to our cipher mapping, we want to encode each character with the letter following it in the alphabet. We can do this by performing integer arithmetic on the ascii_code.

Building on the previous example, we will encode the character variable, whose value is n, and store the result in encoded_character, whose value we expect to be o.

The built-in chr function, as first demonstrated in lesson 3, can be given an int ASCII code and return a str made of its corresponding character.

>>> character: str = "n"
>>> ascii_code: int = ord(character)
>>> encoded_ascii_code: int = ascii_code + 1
>>> encoded_character: str = chr(encoded_ascii_code)
>>> print(encoded_character)
o

Try running this code yourself in a REPL to make sure you are confident about what is going on!

What about if we have to wrap around? In the case of encoding z, we can’t just add 1 to get back to a. The character z’s ASCII code is 122 while a is 97.

Some clever arithmetic, in conjunction with our good friend the remainder operator %, can help us achieve this! Since there are 26 lowercase letters, we can take the remainder of dividing any number by 26 and are guaranteed for it to result in a value of 0 through 25. This would be great if a was 0 and z was 25, because then (25 + 1) % 26 would be 0 which is a! The problem is, lowercase ASCII codes begin at 97 with a and end at 122 with z. For a fun challenge, pause here and see if you can figure out how we’ll get around this conundrum.

Are you ready for the spoiler? Ok, here goes! An oft-used trick in situations like this, (this comes up in 3D graphics, too!), is we’ll momentarily normalize our value so that a is 0 and z is 25 by subtracting a’s ASCII value which is 97. Then, we’ll encode by adding 1 and performing the remainder operation, finally we’ll denormalize by adding a’s ASCII value back so that we once again have a number in the correct range for lower case letters. This modification still works for all letters in the alphabet, not just z.

>>> character: str = "z"
>>> ascii_code: int = ord(character)
>>> normalized_code: int = ascii_code - 97
>>> encoded_code: int = (normalized_code + 1) % 26 + 97
>>> encoded_character: str = chr(encoded_code)
>>> print(encoded_character)
'a'

Give it a try in a REPL to convice yourself these steps work for any lowercase character. No worries if this still seems a bit fuzzy or magical – we are more worried about the function writing in the next two sections.

Setup Exercise Directory

Now that you have a sense for the steps to encode a single character string using a caeser cipher, let’s define a function to abstract away the details of all those steps into functions which are easier to use.

Open the course workspace in VS Code (File > Open Recent > comp110-workspace) and open the File Explorer pane. Expand exercises.

Right click on the word exercises and select “New Folder”.

Name the folder exactly: ex02

Part 1 - cipher.py Encoding

Right click on the ex02 folder you setup above and select “New File”, name the file cipher.py. In this part of the exercise, we will be walking through how to encode a str.

1. Write the encode_char function

Now that you’ve experimented with the process of encoding a single-length str, it is your turn to write a function which carries out those same steps.

Define a function named encode_char that given a single-length str parameter returns the encoded version of that character.

Once you’ve defined your encode_char function, save your work and try importing the function definition into a REPL to try out the function calls shown below.

>>> from exercises.ex02.cipher import encode_char
>>> encode_char("c") 
'd'
>>> encode_char("z")
'a'

Wow! Behold the power of process abstraction! Once you have correctly implemented your function, notice how much easier it is to encode a single character using the function than moving through all those earlier steps!

Functional Requirements – encode_char

  1. Define a function with the following signature:
    1. Name: encode_char
    2. Arguments: a str that can be assumed to be single-length
    3. Returns: a single-length str
  2. The new str should be the parameter given shifted one letter to the right in the alphabet, per the mapping shown above.
  3. Your function should make use of the ord and chr built-in functions.
  4. You can assume only letter characters will be tested on your function. You cannot assume whether the character will be upper or lowercase, though, so you should convert your parameter to lowercase first using a method call expression to the lower method:
    1. To do this, make the first line in your function: some_variable_name: str = parameter_name.lower(). The lower method is built-in to Python str objects and converts any uppercase characters in a string to lowercase.

2. Write an encode_str function to encode 4-letter strings

Now that your encode_char function is complete, you can use it to implement a function for encoding strs of lengths greater than 1. For the purposes of this exercise, we will assume length 4. In a future exercise, you will rewrite this function to work with a str of any length, but we need to learn a few more concepts first.

Define a new function called encode_str that takes in a str parameter and returns the encoded version of that str.

Here’s an example of how you should be able to use the function, once implemented:

>>> from exercises.ex02.cipher import encode_str
>>>> encode_str("abcz") 
'bcda'

Functional Requirements – encode_str

  1. Define another function with the following signature:
    1. Name: encode_str
    2. Arguments: a str that can be assumed to have a length of 4.
    3. Returns: a str of length 4.
  2. The result of this function should be the parameter str with each letter shifted one to the right.
  3. Call encode_char inside this function for each letter of the parameter.
  4. Use str indexing to get the individual characters without looping. Hint: you’ll want to use str indexing.

Part 2 - cipher.py Decoding

Now that we have learned how to encode a str, we want undo this operation and decode it. These next two functions will look VERY similar to the ones you wrote in Part 1. This is expected – the main goal of this exercise is to get comfortable with the function writing process.

1. Write a decode_char function

Define a function named decode_char that given a single-length str parameter returns the originial, unencoded version of that character.

Example function call:

decode_char("c") --> returns "b"

Functional Requirements – decode_char

  1. Define a function with the following signature:
    1. Name: decode_char
    2. Arguments: a str that can be assumed to be single-length
    3. Returns: a single-length str
  2. The new str should be the parameter given shifted one letter to the left in the alphabet, per the mapping shown above.
  3. Your function should make use of the ord and chr built-in functions.
  4. Ensure you are only operating on lowercase strings, because ASCII codes are different for uppercase/lowercase
    1. To do this, make the first line in your function: some_variable_name = <parameter_name>.lower(). The lower function is built into python and converts any string into lowercase.

2. Write a decode_str function to decode 4-letter strings

We can now use our decode_char function to help us decode strs of length 4. Define a new function called decode_str that takes in a str parameter and returns the original, unencoded version of that str.

Example function call:

decode_str("bdef") --> returns "acde"

Functional Requirements – decode_str

  1. Define another function with the following signature:
    1. Name: decode_str
    2. Arguments: a str that can be assumed to have a length of 4.
    3. Returns: a str of length 4.
  2. The result of this function should be the parameter str with each letter shifted one to the left.
  3. Call decode_char inside this function for each letter of the parameter.
  4. Use str indexing to get the individual characters without looping. Hint: you’ll want to use str indexing.

Testing your program

To check that your functions work as expected, you can load your cipher file into a REPL by opening up a new REPL, and then running from exercises.ex02.cipher import encode_char, encode_str, decode_char, decode_str. From here you can practice calling your functions and seeing if the results match what you expected. Once you have completed parts 1 and 2, you should be able to try combining function calls to decode and encode.

For example, decode_char and encode_char are inverses of each other. This means if you apply encode_char to something followed by decode_char, you should end up with what you started with.

decode_char(encode_char("a")) --> returns "a"

This same property holds for decode_str and encode_str. You should see the following behavior:

decode_str(encode_str("flex")) --> returns "flex"

Style and Documentation Requirements

For the both parts of the exercise, we will manually grade your code and are looking for good choices of meaningful variable names. Your variable names should be descriptive of their purposes. We will also manually grade to check that you declared your variables with explicit types.

Once your program is working, add a docstring at the top of your file with a one-sentence description of your program’s purpose.

Then, after the docstring but before your program’s code, add your __author__ variable assigned to your name and e-mail address.

Lastly, there should be no magic numbers in your code. Make any magic numbers into named constants. Hint: In the program snippets above there are 3 magic numbers. More info on this can be found here.