# Thinkcspy Chapter 9: Python Strings

Last Updated on November 19, 2023

This one was fun! I had no idea that manipulating strings could be as interesting as manipulating numbers or pixels. These exercises were certainly some of the more challenging ones thus far, but I got through them all!

## Problem 3

Assign to a variable in your program a triple-quoted string that contains your favorite paragraph of text – perhaps a poem, a speech, instructions to bake a cake, some inspirational verses, etc.

Write a function that counts the number of alphabetic characters (a through z, or A through Z) in your text and then keeps track of how many are the letter ‘e’. Your function should print an analysis of the text like this:

``````Your text contains 243 alphabetic characters, of which 109 (44.8%) are 'e'.
``````

My Solution:

``````# Runestone.Academy thinkcspy course
# Chapter 9
# Problem 3

import string

def count(p):
abc = 0
e = 0
alphabet = string.ascii_lowercase + string.ascii_uppercase

for achar in p:
for alphaChar in alphabet:
if achar == alphaChar:
abc += 1
if achar == "e" or achar == "E":
e += 1

if abc != 0:
ePerAbc = e / abc * 100
else:
ePerAbc = 0

finalMsg = "Your text contains {} alphabetic characters, of which {} ({:.1f}%) are 'e'.".format(abc, e, ePerAbc)

return finalMsg

text = "Bell of the ball"
print(count(text))
``````

Result:

I failed a lot of the built in tests for this one, but the output is is fine. I think they were looking for individual variables to be returned somewhere, maybe in separate functions.

I looked at the answer for this one to see if I was missing anything, and the answer provided also failed the built in tests due to a division be zero error so uh… I’m going to table this one and call it a win.

Here is their code and the results:

``````# Runestone.Academy thinkcspy course
# Chapter 9

def count(p):
lows = "abcdefghijklmnopqrstuvwxyz"
ups =  "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

numberOfe = 0
totalChars = 0
for achar in p:
if achar in lows or achar in ups:
totalChars = totalChars + 1
if achar == 'e':
numberOfe = numberOfe + 1

percent_with_e = (numberOfe / totalChars) * 100
print("Your text contains", totalChars, "alphabetic characters of which", numberOfe, "(", percent_with_e, "%)", "are 'e'.")

p = '''
"If the automobile had followed the same development cycle as the computer, a
Rolls-Royce would today cost \$100, get a million miles per gallon, and explode
once a year, killing everyone inside."
-Robert Cringely
'''

count(p)
``````

## Problem 4

Print out a neatly formatted multiplication table, up to 12 x 12.

My code:

``````# Runestone.Academy thinkcspy course
# Chapter 9
# Problem 4

num1 = 1
product = 0

for n in range(12):
num2 = 1
row = ""

for x in range(12):
product = num1 * num2
row += "{} \t".format(product)
num2 += 1

print(row)
num1 += 1
``````

## Problem 5

Write a function that will return the number of digits in an integer.

My code:

``````# Runestone.Academy thinkcspy course
# Chapter 9
# Problem

def numDigits(n):
toText = str(n)
count = 0

for i in range (len(toText)):
count += 1

return count

print(numDigits(13))
``````

Result:

## The Letter Problems

The following three problems are very interrelated. I would work on one and realize the previous one needed to be changed too. Strap in, because this next section is hella long.

### Problem 10

Write a function that counts how many non-overlapping occurences of a substring appear in a string.

For such a short piece of code, this one really threw me for a loop. First, I had to get help figuring out exactly what “non-overlapping occurrences” meant in the context of a string, lol (“anan” has overlapping occurrences of “an”, “panpan” does not). Then, I had to figure out moving through the text by substring while also comparing more than just the first occurrence of that substring. Finally, I had to make sure the program understood when to count, which was tightly connected with how the program moved through the string.

Hopefully my code can explain this better than I can in English:

``````# Runestone.Academy thinkcspy course
# Chapter 9
# Problem 10

def count(substr,theStr):
count = 0
char = 0
pos1 = 0
pos2 = len(substr)
charPos = len(substr) + 1

while char < len(theStr):
if substr == theStr[pos1:pos2]:
count += 1
char += charPos
else:
char += 1

pos1 += 1
pos2 += 1

return count

print(count("e", "tree"))
``````

Result:

I’m really glad this one had unit tests on it, because it helped me validate if I understood the instructions and if I gave the program the right instructions. This problem alone is a great practice for “thinking like a computer scientist” as it were.

That test didn’t mean shit. When I went to the next problem, I realized there was a problem with the program, even though my initial tests and the built in checks checked out.

The problem was that my solution broke down when the program looked for only 1 character. When I put a non repeating character in between repeating characters like in “banana”, and looked for “a”, the program only counted two “a’s” instead of three, even though the “a’s” were not “overlapping occurrences”.

I eventually realized that I needed to move through the string differently for single characters in a non-overlapping context, so I added this conditional block to the existing one (I changed `char` to `charCount` because the latter makes more sense for the use of the variable):

``````elif len(substr) == 1 and substr == theStr[pos1:pos2]:
count += 1
charCount += 1
``````

But now I had a different problem. The original version of the code was working perfectly fine for words like “tree”. This additional code, now meant that overlapping characters in these types of words, were being counted. That meant that I either needed to modify an existing condition or add another conditional block.

I opted to modify an existing condition, the original `if` block, and test if the next letter was the same as the current. Great test, but by this point I began playing a game of whack-a-mole. The thing that fixed tests for words like “tree” broke tests words like “banana”, especially when the last letter of the word just so happened to be the letter the program was looking for.

Eventually I switched the order of the if and elif blocks, because that simplified the program and kept the conditions from becoming monstrously long and difficult to understand. Then, I finally had the good sense to check if `pos2` was numerically equivalent to the length of the string. This way the program wouldn’t keep looking for an index position that was out of range (because computers count from 0… not 1).

And with that, I finally found something that, to the best of my current knowledge, could handle any length of strings and substrings given to it. Here’s the final code:

``````# Runestone.Academy thinkcspy course
# Chapter 9
# Problem 10

def count(substr,theStr):
count = 0
charCount = 0
pos1 = 0
pos2 = len(substr)
charPos = len(substr)

while charCount < len(theStr):
if charPos == 1 and substr == theStr[pos1] and (pos2 == len(theStr) or substr != theStr[pos2]):
count += 1
charCount += 1
elif  substr == theStr[pos1:pos2]:
count += 1
charCount += charPos + 1
else:
charCount += 1

pos1 += 1
pos2 += 1

return count

print(count("e", "treeleee"))
``````

I used `treeleee` as a test, because it allowed me to test for all the conditions that I had failed before finding this solution. By those metrics, this bit of code took me to the finish line. Below are the results:

Also, this is the 2nd built in test to fall short in these exercises. I’ll probably be a little less excited when I pass them and less concerned when I don’t.

### Problem 11

Write a function that removes the first occurrence of a string from another string.

So I started with this and then realized there was an issue with the program and went back to problem 10 to fix it.

``````# Runestone.Academy thinkcspy course
# Chapter 9
# Problem 11

def remove(substr,theStr):
char = 0
pos1 = 0
pos2 = len(substr)
found = False
newStr = ""

currentPos = theStr[pos1:pos2]
if substr == currentPos:
found = True
else:
char += 1
newStr = theStr[:pos2]

pos1 += 1
pos2 += 1

newStr += theStr[pos2:]

return newStr

print(remove("e", "tree"))
``````

Then I came back with this:

``````# Runestone.Academy thinkcspy course
# Chapter 9
# Problem 11

def remove(substr,theStr):
count = 0
charCount = 0
pos1 = 0
pos2 = len(substr)
charPos = len(substr)
found = False
newStr = ""

if charPos == 1 and substr == theStr[pos1] and (pos2 == len(theStr) or substr != theStr[pos2]):
count += 1
charCount += 1
found = True
elif  substr == theStr[pos1:pos2]:
count += 1
charCount += charPos + 1
found = True
else:
charCount += 1
newStr = theStr[:pos2]

pos1 += 1
pos2 += 1

newStr += theStr[pos2:]

return newStr

print(remove("e", "tree"))
``````

Well… this code basically does the same thing as the previous bit of code, so I went back to the previous one, because why have more complicated code to do essentially the same thing?

The problem (or at least one of the problems) in my code was here:

``````else:
char += 1
newStr = theStr[:pos2]
``````

Specifically, the way I coded `newStr`. If there is no match. it takes everything up to `pos2` which, by nature of how I wrote the code, includes code that I actually wanted to take out!

I realized that I didn’t actually need to change `newStr` in the `else` block. I actually needed to change it in the `if` block, and I needed to create another conditional block to finish constructing the word based on whether or not the substring was found:

``````if found:
newStr += theStr[pos2-1:]
else:
newStr = theStr
``````

And wallah, it worked! Here is the final code (I changed a variable name for the same reason I changed it in the previous problem):

``````# Runestone.Academy thinkcspy course
# Chapter 9
# Problem 11

def remove(substr,theStr):
charCount = 0
pos1 = 0
pos2 = len(substr)
found = False
newStr = ""

currentPos = theStr[pos1:pos2]
if substr == currentPos:
newStr = theStr[:pos1]
found = True
else:
charCount += 1

pos1 += 1
pos2 += 1

if found:
newStr += theStr[pos2-1:]
else:
newStr = theStr

return newStr

print(remove("e", "tree"))
print(remove("iss", "Mississippi"))
print(remove("an", "banana"))
print(remove("e", "treeeleee"))
``````

Results:

But now… I had a new challenge…

### Problem 12

Write a function that removes all occurrences of a string from another string.

My initial logic was to break the initial `while` statement of the previous solution into nesting `while` statements and initialize in the variables in the first one:

``````# Runestone.Academy thinkcspy course
# Chapter 9
# Problem 12

def remove_all(substr,theStr):
charCount = 0

while charCount < len(theStr):
pos1 = 0
pos2 = len(substr)
found = False
newStr = ""

currentPos = theStr[pos1:pos2]
if substr == currentPos:
newStr = theStr[:pos1]
found = True
else:
charCount += 1

pos1 += 1
pos2 += 1

if found:
newStr += theStr[pos2-1:]
else:
newStr = theStr

return newStr

print(remove_all("e", "tree"))
print(remove_all("iss", "Mississippi"))
print(remove_all("an", "banana"))
print(remove_all("e", "treeeleee"))
``````

I may as well had not made any changes at all, because it just gave the same results as if the code were the exact same.

Then, I realized that, unlike in the first problem, I actually needed to adjust `newStr` in an `else` statement, or more specifically in an `elif` statement to test if the condition had been found:

``````elif not found:
charCount += 1
newStr = theStr[:pos1+1]
``````

This block ensured that I still had any characters that existed before any matches were found. But I still had the issue of adding characters after any matches were found. I had bunch of failed ideas, but finally I came up with one that produced better results:

``````# Runestone.Academy thinkcspy course
# Chapter 9
# Problem 12

def remove_all(substr,theStr):
charCount = 0
pos1 = 0
pos2 = len(substr)
newStr = theStr

while charCount < len(newStr):
currentPos = newStr[pos1:pos2]

if substr == currentPos:
newStr = newStr[:pos1+1] + newStr[pos2+1:]
charCount += 1
else:
pos1 += 1
pos2 += 1
charCount += 1

return newStr

print(remove_all("e", "tree"))
print(remove_all("iss", "Mississippi"))
print(remove_all("an", "banana"))
print(remove_all("e", "treeeleee"))
``````

I took out everything related to the `found` variable, because I no longer needed to stop the program once the first instance of `substr` was found. I then moved the iteration of `pos1` and `pos2` to the `else` block, because if nothing was found, then we needed to move on to the next set of characters, but only if nothing was found.

This allowed me to pass most of the built in checks and half my own, but there was still a piece missing. Actually, there were too many pieces.

It turns out that I didn’t need to add to `pos1` and `pos2` when recreating `newStr`, because the end of an index is excluded and the beginning is excluded. Additionally, the variable I needed to test for in the while loop was actually `pos1`. `charCount` was redundant. Turns out, it was redundant in the previous problem’s code too.

Here’s the final code:

``````# Runestone.Academy thinkcspy course
# Chapter 9
# Problem 12

def remove_all(substr,theStr):
pos1 = 0
pos2 = len(substr)
newStr = theStr

while pos1 < len(newStr):
currentPos = newStr[pos1:pos2]

if substr == currentPos:
newStr = newStr[:pos1] + newStr[pos2:]
else:
pos1 += 1
pos2 += 1

return newStr

print(remove_all("e", "treeeleee"))
``````

Results:

And the actual final code for problem 11:

``````# Runestone.Academy thinkcspy course
# Chapter 9
# Problem 11

def remove(substr,theStr):
pos1 = 0
pos2 = len(substr)
found = False
newStr = theStr

currentPos = newStr[pos1:pos2]

if substr == currentPos:
newStr = newStr[:pos1] + newStr[pos2:]
found = True
else:
pos1 += 1
pos2 += 1

return newStr

print(remove("e", "tree"))
print(remove("iss", "Mississippi"))
print(remove("an", "banana"))
print(remove("e", "treeeleee"))
``````

Results:

## Problem 14

Here is a dragon curve. Use 90 degrees.:

``FXX -> X+YF+Y -> -FX-Y``

Most of this code came from the lesson on L-Systems in this chapter. I’m just including it here, because I think it’s cool and wanted to show at least one of these types of problems in this set of exercises.

The code:

``````# Runestone.Academy thinkcspy course
# Chapter 9
# Problem 14

import turtle

def createLSystem(numIters,axiom):
startString = axiom
endString = ""
for i in range(numIters):
endString = processString(startString)
startString = endString

return endString

def processString(oldStr):
newstr = ""
for ch in oldStr:
newstr = newstr + applyRules(ch)

return newstr

def applyRules(ch):
newstr = ""
if ch == 'X':
newstr = 'X+YF+'   # Rule 1
elif ch == 'Y':
newstr = '-FX-Y'   # Rule 2
else:
newstr = ch    # no rules apply so keep the character

return newstr

def drawLsystem(aTurtle, instructions, angle, distance):
for cmd in instructions:
if cmd == 'F':
aTurtle.forward(distance)
elif cmd == 'B':
aTurtle.backward(distance)
elif cmd == '+':
aTurtle.right(angle)
elif cmd == '-':
aTurtle.left(angle)

def main():
inst = createLSystem(10, "FX")   # create the string
print(inst)
t = turtle.Turtle()            # create the turtle
wn = turtle.Screen()
wn.tracer(10)

t.up()
t.back(100)
t.down()
t.speed(0)
drawLsystem(t, inst, 90, 5)   # draw the picture
# angle 90, segment length 5
wn.exitonclick()

main()
``````

Results:

I’m just including the visual result, because the text is just a very loooonng string of repeating `FX+YF++-FX-YF++-FX+YF+--`.

## Encrypt, Decrypt

I needed a little help figuring out transferring one index to the other, but once I got that, it was smooth sailing from there.

### Problem 18

Write a function that implements a substitution cipher. In a substitution cipher one letter is substituted for another to garble the message. For example A -> Q, B -> T, C -> G etc. your function should take two parameters, the message you want to encrypt, and a string that represents the mapping of the 26 letters in the alphabet. Your function should return a string that is the encrypted version of the message.

My code:

``````# Runestone.Academy thinkcspy course
# Chapter 9
# Problem 18

import string

def encrypt(msg, code):
alpha = string.ascii_lowercase
newStr = ""
pos = 0
msg = msg.lower()

for char in msg:
if char in alpha:
pos = alpha.index(char)
newStr += newAlpha[pos]
else:
newStr += char    # no rules apply so keep the character

return newStr

newAlpha = "qwertyuiopasdfghjklzxcvbnm"

print(encrypt("Hello World!", newAlpha))
``````

Result:

### Problem 19

Write a function that decrypts the message from the previous exercise. It should also take two parameters. The encrypted message, and the mixed up alphabet. The function should return a string that is the same as the original unencrypted message.

I literally just had to switch the role of the two indexes:

``````# Runestone.Academy thinkcspy course
# Chapter 9
# Problem 19

import string

def decrypt(msg, code):
alpha = string.ascii_lowercase
newStr = ""
pos = 0
msg = msg.lower()

for char in msg:
if char in alpha:
pos = newAlpha.index(char)
newStr += alpha[pos]
else:
newStr += char    # no rules apply so keep the character

return newStr

newAlpha = "qwertyuiopasdfghjklzxcvbnm"

print(decrypt("itssg vgksr!", newAlpha))
``````

### Problem 21

Write a function called `rot13` that uses the Caesar cipher to encrypt a message. The Caesar cipher works like a substitution cipher but each character is replaced by the character 13 characters to ‘its right’ in the alphabet. So for example the letter a becomes the letter n. If a letter is past the middle of the alphabet then the counting wraps around to the letter a again, so n becomes a, o becomes b and so on. Hint: Whenever you talk about things wrapping around its a good idea to think of modulo arithmetic.

That hint at the end was really helpful. The one thing I messed up on was forgetting that lists in programming languages tend to start at 0 instead of 1. I had to print `pos` before I figured out what the issue was, but once I did, it was an easy fix. Here’s the final code:

``````# Runestone.Academy thinkcspy course
# Chapter 9
# Problem 21

import string

def rot13(mess):
alpha = string.ascii_lowercase
newStr = ""
pos = 0
mess = mess.lower()

for char in mess:
if char in alpha:
pos = alpha.index(char)

if pos <= 12:
pos += 13
else:
pos = pos % 13

newStr += alpha[pos]

else:
newStr += char    # no rules apply so keep the character

return newStr

print(rot13('abcde'))
print(rot13('nopqr'))
print(rot13(rot13('Since rot13 is symmetric you should see this message')))
``````

Results: