Below are exercises from chapter 5 of How to Think Like a Computer Scientist: Learning with Python 3 The bits in italics are from the book; the code is my solutions.

5.14.1 — Days of the week
...

Assume the days of the week are numbered 0,1,2,3,4,5,6 from Sunday to Saturday. Write a function which is given the day number, and it returns the day name (a string).

Most likely the book is looking for something like this:

def main():
    play_again = "Y"
    while play_again == "Y":
        x = int(input("Give me a number representing a weekday.\n"))
        print("That's for ", weekday(x))
        play_again = input("Want to play again? Y/n \n")

def weekday(number):
    """takes an integer (0, 1, 2, 3, 4, 5, or 6) representing a 
    day of the week, where 0 is Sunday, 1 in Monday ... 6 is Saturday. 
    Returns the name of the day as a string."""
    if number == 0:
        return "Sunday"
    elif number == 1:
        return "Monday"
    elif number == 2:
        return "Tuesday"
    elif number == 3:
        return "Wednesday"
    elif number == 4:
        return "Thursday"
    elif number == 5:
        return "Friday"
    elif number == 6:
        return "Saturday"
    else:
        print("Something seems to have gone wrong in the weekday function")
        print("when it was passed a value of ", number)
    
main()

But this is the perfect situation to use a dictionary, not so much for performance gains as because it’s less ugly. So we can revise to this:

# Exercise 5.14.1. of _How to Think Like a Computer Scientist:
# Learning with Python 3_

weekdays = {
    0: "Sunday",
    1: "Monday",
    2: "Tuesday",
    3: "Wednesday",
    4: "Thursday",
    5: "Friday",
    6: "Saturday"
}

while True:
    x = int(input("Give me a number representing a weekday.\n"))
    print("That's for ", weekdays[x])
    if input("Want to play again? Y/n \n") != 'Y':
        break

One you start shortening code, it can get hard to quit. Let’s take out the comments and some whitespace, and compress the dictionary.

weekdays = {0: "Sunday", 1: "Monday", 2: "Tuesday", 3: "Wednesday",
    4: "Thursday", 5: "Friday", 6: "Saturday"}
    
while True:
    x = int(input("Give me a number representing a weekday.\n"))
    print("That's for ", weekdays[x])
    if input("Want to play again? Y/n \n") != 'Y':
        break

Now, let’s ignore the rule about not writing lines of more than 80 characters.

weekdays = {0: "Sunday", 1: "Monday", 2: "Tuesday", 3: "Wednesday", 4: "Thursday", 5: "Friday", 6: "Saturday"}
while True:
    x = int(input("Give me a number representing a weekday.\n"))
    print("That's for ", weekdays[x])
    if input("Want to play again? Y/n \n") != 'Y':
        break

And we can get away with jamming the “break” statement onto the previous line.

weekdays = {0: "Sunday", 1: "Monday", 2: "Tuesday", 3: "Wednesday", 4: "Thursday", 5: "Friday", 6: "Saturday"}
while True:
    x = int(input("Give me a number representing a weekday.\n"))
    print("That's for ", weekdays[x])
    if input("Want to play again? Y/n \n") != 'Y': break

The thing we do with x, where we store an input in it and then look it up in the dictionary, is helpful to human readers. Let’s not be helpful.

weekdays = {0: "Sunday", 1: "Monday", 2: "Tuesday", 3: "Wednesday", 4: "Thursday", 5: "Friday", 6: "Saturday"}
while True:
    print("That's for ", weekdays[int(input("Give me a number representing a weekday.\n"))])
    if input("Want to play again? Y/n \n") != 'Y': break

Now let’s move the “Want to play?” question into the same line as the While statement.

weekdays = {0: "Sunday", 1: "Monday", 2: "Tuesday", 3: "Wednesday", 4: "Thursday", 5: "Friday", 6: "Saturday"}
while input('Wanna play again?') == 'Y':
    print("That's for ", weekdays[int(input("Give me a number representing a weekday.\n"))])

We can also move the print statement onto the same line as the While statement.

weekdays = {0: "Sunday", 1: "Monday", 2: "Tuesday", 3: "Wednesday", 4: "Thursday", 5: "Friday", 6: "Saturday"}
while input('Wanna play again?') == 'Y': print("That's for ", weekdays[int(input("Give me a number representing a weekday.\n"))])

And finally, what we’re doing with the variable weekdays, we could just do with composition.

while input('Wanna play again?') == 'Y': print("That's for ", {0: "Sunday", 1: "Monday", 2: "Tuesday", 3: "Wednesday", 4: "Thursday", 5: "Friday", 6: "Saturday"}[int(input("Give me a number representing a weekday.\n"))])

I’m not yet at the point where I can write zero-line code, so we’ll have to stop there.

5.14.2
...

You go on a wonderful holiday (perhaps to jail, if you don’t like happy exercises) leaving on day number 3 (a Wednesday). You return home after 137 sleeps. Write a general version of the program which asks for the starting day number, and the length of your stay, and it will tell you the name of day of the week you will return on.

This one is basically just some modular math and some inputs and outputs.

# Dictionary to convert numbers to weeks
weekdays = {0: "Sunday", 1: "Monday", 2: "Tuesday", 3: "Wednesday", \
            4: "Thursday", 5: "Friday", 6: "Saturday"}
# Prompt user, and tell them what day of the week they gave.
print("Give me a number from 0 to 6, representing the day you went to ", \
       "your destination, where 0 represents Sunday, 1 represents " \
       "Monday, etc. \n")
x = int(input("What is the number? \n"))
print("That's a {}.".format(weekdays[x]))

y = int(input("How many nights did you sleep at your destination? \n"))

# Modular math to find ending day of the week.
z = (x + y) % 7
print("You left your destination on a {}.".format(weekdays[z]))

5.14.6
...

Write a function which is given an exam mark, and it returns a string — the grade for that mark — according to this scheme:

a chart of grades

The square and round brackets denote closed and open intervals. A closed interval includes the number, and open interval excludes it. So 39.99999 gets grade F3, but 40 gets grade F2. Assume

xs = [83, 75, 74.9, 70, 69.9, 65, 60, 59.9, 55, 50,
                     49.9, 45, 44.9, 40, 39.9, 2, 0]

Test your function by printing the mark and the grade for all the elements in this list.

Notice that the grades are arranged, except for F3, as a series of thresholds. That is, if your score is greater than or equal to 75, you’ve got a “First”. You’re done evaluating your score. If not, then you check the next threshold, 70, to see if you have a Second. And so on. If you fail all the thresholds down to 40, then you get an F3.

So we can think of each threshold as “paired” with a grade, and the thresholds need to be tested in the order 75, 70, 60, 50, 45, 40. We run through the thresholds in descending order, and as soon as one is met, we return the grade. If none are met, we return “F3”.

def grade(mark):
    """Takes a mark and returns a grade using the scheme in 
    _How to Think Like a Computer Scientist: Learning with Python 3_, 
    exercise 5.14.6"""
    
    # These are the thresholds to make the various grades.
    mark_scheme = [(75, "First"), (70, "Upper Second"), (60, "Second"),
                (50, "Third"), (45, "F1 Supp"), (40, "FS")]
    
    # Check the thresholds in descending order and return the highest
    # threshold met.
    for threshold in mark_scheme:
        if mark >= threshold[0]:
            return threshold[1]
    
    # If no threshold is met, we return "F3"
    return "F3"
    
# Marks to grade
xs = [83, 75, 74.9, 70, 69.9, 65, 60, 59.9, 55, 50,
                     49.9, 45, 44.9, 40, 39.9, 2, 0]
                     
# Grade the marks and print them with their results.
for x in xs:
    print(x, grade(x))

5.14.7 — Making a prettier graph
...

Modify the turtle bar chart program so that the pen is up for the small gaps between each bar.

In this chapter, the textbook shows us how to write a particular program, which is below. (I had to add line 15 because the program assumed use of the module “turtle” without importing it).

def draw_bar(t, height):
    """ Get turtle t to draw one bar, of height. """
    t.begin_fill()           # Added this line
    t.left(90)
    t.forward(height)
    t.write("  "+ str(height))
    t.right(90)
    t.forward(40)
    t.right(90)
    t.forward(height)
    t.left(90)
    t.end_fill()             # Added this line
    t.forward(10)
    
import turtle

wn = turtle.Screen()         # Set up the window and its attributes
wn.bgcolor("lightgreen")

tess = turtle.Turtle()       # Create tess and set some attributes
tess.color("blue", "red")
tess.pensize(3)

xs = [48,117,200,240,160,260,220]

for a in xs:
    draw_bar(tess, a)
    
wn.mainloop()

That program outputs something like this:

line graph

If you know about turtles and walk through the draw_bar function, you can see that line 13 is where our turtle, using its blue pen, draws the little section of blue line that clutters up the graph. So we just need to lift the pen — t.penup() — before t.forward(10), and then put the pen back down — t.pendown() — afterward.

So now the code looks like this:

"""This is basically copied straight from chapter 5 of _How to Think Like a 
   Computer Scientist: Learning with Python 3_"""
   
def draw_bar(t, height):
    """ Get turtle t to draw one bar, of height. """
    t.begin_fill()           # Added this line
    t.left(90)
    t.forward(height)
    
    t.write("  "+ str(height))
    t.right(90)
    t.forward(40)
    t.right(90)
    t.forward(height)
    t.left(90)
    t.end_fill()             # Added this line
    t.penup()
    t.forward(10)
    t.pendown()
    
import turtle

wn = turtle.Screen()         # Set up the window and its attributes
wn.bgcolor("lightgreen")

tess = turtle.Turtle()       # Create tess and set some attributes
tess.color("blue", "red")
tess.pensize(3)

xs = [48,117,200,240,160,260,220]

for a in xs:
    draw_bar(tess, a)
    
wn.mainloop()

And the program draws this:

a bar graph

5.14.8 — Multi-colored bars
...

Modify the turtle bar chart program so that the bar for any value of 200 or more is filled with red, values between [100 and 200) are filled with yellow, and bars representing values less than 100 are filled with green.

To do this we just need to add some lines to the beginning of draw_bar, setting fill-color based on the value of the height variable.

    if height >= 200:
        t.color("blue", "red")
    elif height >= 100:
        t.color("blue", "yellow")
    elif height < 100:
        t.color("blue", "green")

5.14.9 — Negative values
...

In the turtle bar chart program, what do you expect to happen if one or more of the data values in the list is negative? Try it out. Change the program so that when it prints the text value for the negative bars, it puts the text below the bottom of the bar.

If we change a couple of the values to negative, say, xs = [48,117,200,-240,160,-260,220], we get:

a bar chart

The problem is where the numbers are on the negative bars. We can write in the numbers in a better spot. We can replace the t.write(" "+ str(height)) bit with an ifelse structure. If height is negative, then we need to do a little repositioning. Otherwise, we carry on as usual.

    if height <0:
        t.penup()                 # So we don't leave messy lines
        t.backward(17)            # Moves us down into position
        t.write("  "+ str(height))    # Write our value
        t.forward(17)             # retrace our steps
        t.pendown()               # Pen down to continue drawing
    else:                         # Or, if height is positive,
        t.write("  "+ str(height))     # it's business as usual.

Now the whole code looks like this:

"""This is basically copied straight from chapter 5 of _How to Think Like a 
   Computer Scientist: Learning with Python 3_, and then I modified it."""

def draw_bar(t, height):
    """ Get turtle t to draw one bar, of height. Fills bar with color 
        as specified in _How to Think Like a Computer Scientist: 
        Learning with Python 3_, chapter 5, exercise 14.8."""

    # Set fill-color
    if height >= 200:
        t.color("blue", "red")
    elif height >= 100:
        t.color("blue", "yellow")
    elif height < 100:
        t.color("blue", "green")

    t.begin_fill()             # Turn fill on before drawing.
    t.left(90)                 # Turn to face upward
    t.forward(height)          # Draw line upward (if height is positive)

    # Write caption for our bar
    if height <0:                      # If height is negative, 
        t.penup()                      # we pick up pen to stop drawing for a bit,
        t.backward(17)                 # move downward to an appropriate spot,
        t.write("  "+ str(height))     # write our caption, 
        t.forward(17)                  # move back up to resume,
        t.pendown()                    # and put our pen back down to continue.
    else:                              # Otherwise, height is positive, and 
        t.write("  "+ str(height))     # we can just go ahead and caption.


    t.right(90)             # Turn to produce "top" (for positive values) of bar.
    t.forward(40)           # Draw the top.
    t.right(90)             # Turn to draw other wise of bar.
    t.forward(height)       # Draw other side of bar.
    t.left(90)              # Turn back to original position.
    t.end_fill()            # Fill in the bar.
    t.penup()               # Stop drawing.
    t.forward(10)           # Move into position for the next bar.
    t.pendown()             # And get ready to draw again.

import turtle

wn = turtle.Screen()         # Set up the window and its attributes
wn.bgcolor("lightgreen")

tess = turtle.Turtle()       # Create a turtle and set some attributes
tess.color("blue", "red")
tess.pensize(3)

xs = [48,117,200,-240,160,-260,220]      # Our data to graph

for a in xs:                            # Graph the bars in order.
    draw_bar(tess, a)

wn.mainloop()                           # Hold screen open till user is done.

Our draw_bar function is getting a little busy, so we could spin off the part that writes the caption into its own function height_caption(t, height). Likewise, we can spin off the code that sets the fill color. So now we get this:

def draw_bar(t, height):
    """ Get turtle t to draw one bar, of height. Fills bar with color 
        as specified in _How to Think Like a Computer Scientist: 
        Learning with Python 3_, chapter 5, exercise 14.8."""

    set_fill_color(t, height)

    t.begin_fill()             # Turn fill on before drawing.
    t.left(90)                 # Turn to face upward
    t.forward(height)          # Draw line upward (if height is positive)
    height_caption(t, height) # Write height caption for our bar
    t.right(90)             # Turn to produce "top" (for positive values).
    t.forward(40)           # Draw the "top".
    t.right(90)             # Turn to draw other side of bar.
    t.forward(height)       # Draw other side of bar.
    t.left(90)              # Turn back to original position.
    t.end_fill()            # Fill in the bar.
    t.penup()               # Stop drawing.
    t.forward(10)           # Move into position for the next bar.
    t.pendown()             # And get ready to draw again.

def height_caption(t, height):
    """When called at the right time, this writes a caption for our 
       bar, displaying its height. If the bar represents a positive number, it 
       writes the caption above the bar. For a negative number, below the bar."""

    if height <0:                      # If height is negative, 
        t.penup()                      # we pick up pen to stop drawing for a bit,
        t.backward(17)                 # move downward to an appropriate spot,
        t.write("  "+ str(height))     # write our caption, 
        t.forward(17)                  # move back up to resume,
        t.pendown()                    # and put our pen back down to continue.
    else:                              # Otherwise, height is positive, and 
        t.write("  "+ str(height))     # we can just go ahead and caption.

def set_fill_color(t, height):
    """Sets fill-color of a turtle based on the scheme
       laid out in exercise 5.14.9 of _How to Think Like a Computer
       Scientist: Learning with Python 3_"""

    if height >= 200:
        t.color("blue", "red")
    elif height >= 100:
        t.color("blue", "yellow")
    elif height < 100:
        t.color("blue", "green")


import turtle

wn = turtle.Screen()         # Set up the window and its attributes
wn.bgcolor("lightgreen")

tess = turtle.Turtle()       # Create a turtle and set some attributes
tess.color("blue", "red")
tess.pensize(3)

xs = [48,117,200,-240,160,-260,220]      # Our data to graph

for a in xs:                            # Graph the bars in order.
    draw_bar(tess, a)

wn.mainloop()                           # Hold screen open till user is done.

5.14.10 — Hypotenuse
...

Write a function find_hypot which, given the length of two sides of a right-angled triangle, returns the length of the hypotenuse. (Hint: x ** 0.5 will return the square root.)

The famous formula

a^2+b^2=c^2

gives the relationship between the three sides of a right triangle, in which c is the hypotenuse. This can be rearranged to

c=sqrt(a^2+b^2)

Then it's all a simple matter of rewriting our math in Python:

def find_hypot(a, b):
    return (a**2 + b**2) ** 0.5

5.14.11 — Check triangle for rightness
...

Write a function is_rightangled which, given the length of three sides of a triangle, will determine whether the triangle is right-angled. Assume that the third argument to the function is always the longest side. It will return True if the triangle is right-angled, or False otherwise.

Hint: Floating point arithmetic is not always exactly accurate, so it is not safe to test floating point numbers for equality. If a good programmer wants to know whether x is equal or close enough to y, they would probably code it up as:

if  abs(x-y) < 0.000001:    # If x is approximately equal to y
    ...

Here’s an answer:

def is_rightangled(a, b, c):
    return abs(a**2 + b**2 - c**2) < 0.000001

But the problem here is that this algorithm is “stricter” when dealing with larger triangles. For example, suppose we test our algorithm with

print(is_rightangled(1,  1,  1.4142135),
      is_rightangled(10, 10, 14.142135))

It prints True False, which seems odd given that 10, 10, 14.142135 has exactly the same angles as 1, 1, 1.4142135. A test of right-angled-ness that winds up being sensitive to triangle size is a problem. And as the triangles get bigger, so does the sensitivity problem.

This sensitivity problem grows to the point of absurdity with very small and very large triangles. Consider the following:

print(is_rightangled(0.0001,  0.0001,  0.0009),
      is_rightangled(10**100, 10**100, 1.4142135623730950488016887242096980785696718753769480731766797379907324784621*10**100))

This prints True False. In other words, we have convinced the computer that a tiny “right” triangle can have a hypotenuse nine times longer than each of its legs. On the other hand, it rejects a triangle where the hypotenuse is correct to 77 significant digits. To measure the size of the observable universe so accurately that your measurement is off by no more than the width of an atom requires only 28 significant digits.

This is absurd and useless, but it was the first solution that popped into my head. So, knowing what we know now, let’s try again:

def is_rightangled(a, b, c):
    square_of_legs = a**2 + b**2
    square_of_hypotenuse = c**2
    ratio = square_of_hypotenuse / square_of_legs
    ratio_error = abs(ratio - 1)
    return ratio_error < 0.000001

That behaves better. Now, we can quibble over whether the function is strict enough, but that’s a practical question, and we can adjust our function depending on how strict we need to be for whatever application we’re building.

5.14.12
...

Extend the above program so that the sides can be given to the function in any order.

def is_rightangled(a, b, c):
    square_of_hypotenuse = (max(a, b, c))**2
    square_of_legs = a**2 + b**2 + c**2 - square_of_hypotenuse
    ratio = square_of_hypotenuse / square_of_legs
    ratio_error = abs(ratio - 1)
    return ratio_error < 0.000001

This is an algebraically correct solution, relying on the fact that a^2 + b^2 + c^2 – c^2 = a^2 + b^2, and many readers will be able to figure out why it works if they think about it hard enough or if it is documented well enough. But there’s a potential for confusion here, and we can do better, with a clearer logic.

def is_rightangled(a, b, c):
    sides_list = [a, b, c]
    sides_list.sort()
    short_leg = sides_list[0]
    long_leg = sides_list[1]
    hypotenuse = sides_list[2]
    legs_squared = short_leg**2 + long_leg**2
    hypotenuse_squared = hypotenuse**2
    ratio = hypotenuse_squared / legs_squared
    ratio_error = abs(1 - ratio)
    return ratio_error < 0.000001

This page is released under the GNU Free Documentation License, any version 1.3 or later produced by the Free Software Foundation. I am grateful to the authors of the original textbook for releasing it that way: Peter Wentworth, Jeffrey Elkner, Allen B. Downey, and Chris Meyers.