Eda Eren

June 2, 2022
  • Miscellaneous
  • Python

Solving the Problem Sets of CS50's Introduction to Programming with Python — One at a Time: Problem Set 8

This week, we are solving the last problem set of the course. We have covered so much, solved many beautiful problems, and after this week, are ready to implement our very own final project! I know that is a bit emotional indeed, but let me give the disclaimer one last time, that this series was intended as a guide for thinking about the problems, instead of just providing the solutions. And, as always, it is assumed you have read the problem explanations already. You can find the previous posts in this series in the archive.

So far, we have been writing our code procedurally, but this time, we are going to make use of the Object-Oriented programming paradigm, which is definitely exciting as we will explore each problem. Let's dive in!

Seasons of Love

For the first problem of this week, we are working with a datetime object, instead of writing our own classes from scratch. We need to convert days to minutes; 365 days for example, results in 525600 minutes.

Many hints are given for this problem, but let's take a look. For starters, we want our code to be more modular this week as we learn to appreciate the importance of modularity in programming. The things that we need to do, is to get an input (in the YYYY-MM-DD format), convert the input to a date object, calculate the difference of days between that given date and today, convert days to minutes, and finally convert minutes to words. Well, it may seem like a lot, but thinking this way actually makes our job easier. First, as always we can try to get an input, and if we have a ValueError, we can exit the program with a string like Invalid date which will result in an exit status of 1. If we type help(sys.exit) to get information from the documentation in this case, it literally tells you that:

Help on built-in function exit in module sys:

exit(status=None, /)
Exit the interpreter by raising SystemExit(status).

If the status is omitted or None, it defaults to zero (i.e., success).
If the status is an integer, it will be used as the system exit status.
If it is another kind of object, it will be printed and the system
exit status will be one (i.e., failure).
Help on built-in function exit in module sys:

exit(status=None, /)
Exit the interpreter by raising SystemExit(status).

If the status is omitted or None, it defaults to zero (i.e., success).
If the status is an integer, it will be used as the system exit status.
If it is another kind of object, it will be printed and the system
exit status will be one (i.e., failure).

We have been doing this for many weeks, so, no problem.

If the given format is okay (something like 2021-06-02, for example), we calculate how many days have passed from today. Before that, remember that input returns a string, and we need integers to construct a date object, so year, month, and day that are split from the input should be integers. To calculate how many days have passed, datetime.date has some methods that can come in handy here. It is already in the hints section, so if we subtract the given date from today, we have a timedelta object returned, which has its own instance attributes like, say, days. After we have the days, we need to convert it to minutes, and how to do it is also given in the problem explanation. Literally, it is this:

def days_to_minutes(days):
return days * 24 * 60
def days_to_minutes(days):
return days * 24 * 60

After we have our minutes, finally we need to convert it to a nice looking output. The inflect library, which I have come to fall in love with, is tremendously helpful. What we need to do is literally to convert a number to words, but we only want commas and no "and words". So, instead of looking like this:

'five hundred and twenty-five thousand, six hundred'
'five hundred and twenty-five thousand, six hundred'

Our output should look like this:

'Five hundred twenty-five thousand, six hundred minutes'
'Five hundred twenty-five thousand, six hundred minutes'

Notice that we also want our output to be capitalized. Again, a way too obvious hint, but remember that the documentation is your friend.

After these, we have not much to do except writing our tests for our code. How to Test is also given in the problem explanation, all the test specifications should be enough to implement. We are already familiar with testing, and have been used to it already for many weeks, so it should be quite easy as well. Now, let's check out the next one.

Cookie Jar

Here we are, the time has come to write our first class in this course. In this problem, we are storing cookies in a jar. Simple, and really fun as it sounds.

We are given a template already, and need to implement the functions __init__, __str__, deposit, withdraw, capacity, and size. First of all, remember that __init__ function initializes our object construction. So, let's say you have just bought a cookie jar from the store, how many cookies are in it when you first bought it? It has a capacity (which in this case, we default to 12), but since there are no cookies in it, its size is 0 when you initially buy it. As you deposit cookies into it, its size increases, and as you withdraw cookies, its size decreases. Therefore, for the initialization, our cookie jar has the capacity of whatever is given as capacity, and the size of 0.

For deposit and withdraw functions, we need to be careful with some edge cases. For example, if the total number of cookies after you deposit is more than the capacity, we should raise a ValueError. Otherwise, we increase the size. And, if the number to withdraw is more than the size, we also raise ValueError. Otherwise, we decrease the size.

Now, here is the interesting part. While working with capacity and size, you realize that we not only read their values, but also set their values. As the template already indicates, we will use @property decorator for the getter functions. How do we do the setters, then? Before answering that question, let's take a look at an example. Let's say we have created a class for a Hogwarts homework essay, and we are mainly concerned with the number of words in it. We have a default word limit of 2000 (which, I guess, would be nothing for Hermione), but it can be changed. We can add or remove words, but the important thing is how we handle the word limit and the number of words we have written already. It sounds complicated, but let's take a look at this:

class HogwartsEssay:
def __init__(self, word_limit=2000):
self.word_limit = word_limit
self.words_written = 0

def __str__(self):
return f'Number of words written: {self.words_written}'

def add_words(self, number_of_words_to_add):
...

def remove_words(self, number_of_words_to_remove):
...

@property
def word_limit(self):
return self._word_limit

@word_limit.setter
def word_limit(self, word_limit):
if word_limit < 0:
raise ValueError
self._word_limit = word_limit

@property
def words_written(self):
return self._words_written

@words_written.setter
def words_written(self, words_written):
self._words_written = words_written
class HogwartsEssay:
def __init__(self, word_limit=2000):
self.word_limit = word_limit
self.words_written = 0

def __str__(self):
return f'Number of words written: {self.words_written}'

def add_words(self, number_of_words_to_add):
...

def remove_words(self, number_of_words_to_remove):
...

@property
def word_limit(self):
return self._word_limit

@word_limit.setter
def word_limit(self, word_limit):
if word_limit < 0:
raise ValueError
self._word_limit = word_limit

@property
def words_written(self):
return self._words_written

@words_written.setter
def words_written(self, words_written):
self._words_written = words_written

As you can see, add_words and remove_words functions are omitted so as not to be way too close to the solution. But, the idea is simple. We can get the word limit as well as the number of words written, we can also set the word limit as long as it is not less than 0, and set the number of words written. It might be a weird example, but the idea is similar to the example given in the lecture. If you have seen the lecture, then you are already familiar with using getters and setters. This is really a fun problem, even though at a glance might seem complicated a bit. That is really all we need to do. And, for the tests, How to Test section is again your friend, as it tells you what to do almost step-by-step.

And, before we go into the next one, how to print the cookies? Well, we print a cookie emoji for the number of cookies in the jar, and, thank Guido that Python is an amazing language — and has a string operator that helps us repeat our strings.

On to the next (and the last) problem!

CS50 Shirtificate

The very last problem to solve, is kind of customizable. We are making our very own I took CS50 shirts, with the help of fpdf2 library.

Here, the mantra we have for weeks is realized, has taken shape, and stares at us in flesh: "When in doubt, read the documentation." The problem has only five specifications, and beyond them, you are free to use any methods, and create any kind of shirt that you want to.

To be honest, though, this freedom may be a bit intimidating. Although, the bare minimum solution for this problem takes 15-20 lines of code, finding the right methods and attributes to use can be a bit of a pain. Let's see how we can manage to create a shirt with only the required specifications.

As the hints section suggests, we can add a subclass that inherits FPDF class itself to write a header. In this case, our header will be the text "CS50 Shirtificate" that is centered horizontally.

The code in the tutorial for header function literally helps you with that, only we do not need to render a logo. Inside our function for header, we can just set our font, move cursor to right, print title with aligning it to center and without a border, and perform a line break. These are already given to you. After the line break, we can call the image method to insert our image (shirtificate.png). In order to do it properly, we can set its width to effective page width (which is just the width of the page minus the horizontal margins) to make our job easier. Notice that the documentation has this to say for the width option for images:

w : float

optional width of the image. If not specified or equal to zero, it is automatically calculated from the image size. Pass pdf.epw to scale horizontally to the full page width.

We also need to put our text {name} took CS50, where name is the return value of input, on our shirt. We need to color our text white here. For that, you might have already checked the documentation for text styling; however, it might be still a bit confusing.

Now, not to deviate from the subject but, if you are really stuck (which was my experience at some point), the link to the documentation above has also a link to a file called test_text_mode.py in the library's source code. Take a look at this:

def test_text_modes(tmp_path):
pdf = FPDF(format=(350, 150))
pdf.add_page()
pdf.set_font("Helvetica", size=80)
with pdf.local_context(fill_color=(255, 128, 0)):
pdf.cell(txt="FILL default")
with pdf.local_context(text_color=(0, 128, 255)):
pdf.cell(txt=" text mode")
pdf.ln()

...
def test_text_modes(tmp_path):
pdf = FPDF(format=(350, 150))
pdf.add_page()
pdf.set_font("Helvetica", size=80)
with pdf.local_context(fill_color=(255, 128, 0)):
pdf.cell(txt="FILL default")
with pdf.local_context(text_color=(0, 128, 255)):
pdf.cell(txt=" text mode")
pdf.ln()

...

And here it is, easy-to-use text color for the local context! Remember that our color should be white, whose RGB value represents all the colors to the brim. With local context, we create a cell, this time for the text on the shirt. Its height, as the hints in the problem explanation suggest, can be negative to adjust it properly, say, something like 250ish. For the width, our old friend effective page width is helpful. We also align it to the center, of course.

Now that our class seems to be done, it is time to create an instance, add page with the appropriate orientation and format, and output the shirtificate.pdf. With that, that is the end of our problem and the problem sets!

It has been a really fun and delightful journey, but, now that the problem sets are actually over, it does not mean that we are done, yet. Next week, for the last installment in this series, we are going to think about things we have learned so far, what more to discover, and how to continue onward to the final project and beyond.

Until then, happy coding.