Assignment 6
Objective and Overview
Objective: The objective of this assignment is to gain experience with object-oriented programming, including class design, writing methods, and client code to use objects of your newly-created class.
Overview: In this assignment, you will develop a Date
class to repesent objects that
are calendar dates. You will store data for the month, day and year, and provide methods
to manipulate Date
s as explained below.
Preliminaries
In your work on this assignment, make sure to abide by the collaboration policies of the course.
If you have questions while working on this assignment, please post them on Piazza! This is the best way to get a quick response from your classmates and the course staff.
General Guidelines
-
Refer to the class Coding Standards for important style guidelines. The grader will be awarding/deducting points for writing code that comforms to these standards.
-
Include comments at the top of the file that are similar to the ones that we gave you at the top of
a1task1.py
. -
Your functions must have the exact names that we have specified, or we won’t be able to test them. Note in particular that the case of the letters matters (all of them should be lowercase), and that you should use an underscore character (
_
) wherever we have specified one (e.g., inconvert_from_inches
). -
Each of your functions should include a docstring that describes what your function does and what its inputs are.
-
If a function takes more than one input, you must keep the inputs in the order that we have specified.
-
You should not use any Python features that we have not discussed in class or read about in the textbook.
-
Unless expressly stated, your functions do not need to handle bad inputs – inputs with a type or value that doesn’t correspond to the description of the inputs provided in the problem.
-
Make sure that your functions return the specified value, rather than printing it. Unless it is expressly stated in the problem, none of these functions should use a
print
statement.
Important note regarding test cases and Gradescope:
-
You must test each function after you write it. Here are two ways to do so:
- Run your file after you finish a given function. Doing so will bring you to the Shell, where you can call the function using different inputs and check to see that you obtain the correct outputs.
-
Add test calls to the bottom of your file, inside the
if __name__ == '__main__'
control struture. For example:if __name__ == '__main__': print("mystery(6,7) returned", mystery(6,7))
These tests will be called every time that you run the file, which will save you from having to enter the tests yourself. We have given you an example of one such test in the starter file.
-
You must not leave any
print
statements in the global scope. This will cause an error with the Gradescope autograder. Make sure all of yourprint
statements are inside a function scope or insude theif __name__ == '__main__'
control struture.
Important note Python libraries:
You may NOT use any functions or tools from the Python calendar
module or any similar
pre-existing libraries.
Task 1: A Date
class
70 points; individual-only
Some people have an extraordinary talent to compute (in their heads) the day of the week that any past date fell on. For example, if you tell them that you were born on October 13, 1995, they’ll be able to tell you that you were born on a Friday!
In this assignment, you will create a Date
class, from which you will be able
to create Date
objects that represent a day, month, and year. You will add
functionality to this class that will enable Date
objects to find the day
of the week to which they correspond.
Getting started
To start, create a new file and save it as a6task1.py
. Put all of
the code that you write for this task in this file.
Don’t forget to include appropriate comments at the top of the file, and a docstring for
your your class and each method in the class.
Your tasks
-
Begin by creating a class definition, with the following:
-
The
__init__(self, new_month, new_day, new_year)
method, which is the constructor forDate
objects. In other words, this is the method that Python uses when making a newDate
object. It defines the attributes that compose aDate
object (month
,day
, andyear
) and accepts parameters to set an object’s attributes to some initial values. -
The
__repr__(self)
method, which returns a string representation of aDate
object. This method will be called when an object of typeDate
is printed. It can also be tested by simply evaluating an object from the Shell. This method formats the month, day, and year that represent aDate
object into a string of the form'mm/dd/yyyy'
and returns it.Then, try the following interactions in the Python Shell to experiment with the
__init__
,__repr__
methods:# Create a Date object named d1 using the constructor. >>> d1 = Date(7, 25, 2022) # An example of using the __repr__ method. Note that no quotes # are displayed, even though the function returns a string. >>> d1 07/25/2022
-
The
copy(self)
method, which returns a newly-constructed object of typeDate
with the same month, day, and year that the called object has. This allows us to create deep copies ofDate
objects.Next, try the following examples in the Python Shell to illustrate why we will need to override the
__eq__
method to change the meaning of the==
operator:>>> d1 = Date(1, 1, 2022) >>> d2 = d1 >>> d3 = d1.copy() # Determine the memory addresses to which the variables refer. >>> id(d1) 430542 # Your memory address may differ. >>> id(d2) 430542 # d2 is a reference to the same Date that d1 references. >>> id(d3) 413488 # d3 is a reference to a different Date in memory. # The == operator tests whether memory addresses are equal. >>> d1 == d2 True # Shallow copy -- d1 and d2 have the same memory address. >>> d1 == d3 False # Deep copy -- d1 and d3 have different memory addresses.
You will override the
==
operator in the__eq__
method below.
-
-
Write the method
is_leap_year(self)
, which returnsTrue
if the called object is in a leap year, andFalse
otherwise. In other words, when we create aDate
object and call itsis_leap_year
method, the method will return whether that specificDate
object falls in a leap year.# Create a Date object named d1 using the constructor. >>> d1 = Date(1, 1, 2020) # Check if d1 is in a leap year -- it is! >>> d1.is_leap_year() True # Create another object named d2 >>> d2 = Date(1, 1, 2021) # Check if d2 is in a leap year. >>> d2.is_leap_year() False
Here are the rules for determining if a year is a leap year:
-
If the year is not evenly divisible by 4, it is not a leap year.
-
If the year is a century not evenly divisible by 400, it is not a leap year.
-
If the year is a century divisibly by 400, it is a leap year.
# Here are some further test cases: >>> d1 = Date(1, 1, 2000) # Check if d1 is in a leap year -- it is! >>> d1.is_leap_year() True # Create another object named d2 >>> d2 = Date(1, 1, 2100) # Check if d2 is in a leap year. >>> d2.is_leap_year() False
-
-
Write the method
is_valid_date(self)
method, which returnsTrue
if the object is a valid date, and False otherwise. You must enforce the following rules:-
Valid months have between 1 and 12.
-
Valid days are between 1 and 30 for the months of April, June, September, and November.
-
Valid days are between 1 and 31 for the months of January, March, May, July, August, October and December.
-
Valid days are between 1 and 29 for February if the year is a leap year.
-
Valid days are between 1 and 28 for February if the year is not a leap year.
# Here are some test cases to try: >>> d1 = Date(7,25,2022) >>> d1.is_valid_date() True >>> d2 = Date(14,25,2022) >>> d2.is_valid_date() False >>> d3 = Date(2,30,2020) >>> d3.is_valid_date() False >>> d4 = Date(2,29,2020) >>> d4.is_valid_date() True >>> d5 = Date(2,29,2022) >>> d5.is_valid_date() False >>> d6 = Date(-4,-4, -5) >>> d6.is_valid_date() False >>> d7 = Date(2,29,2100) >>> d7.is_valid_date() False
-
-
Write the method
add_one_day(self)
that changes the called object so that it represents one calendar day after the date that it originally represented.Notes:
- This method should not return anything. Instead, it should change the value of one or more variables inside the called object.
- Since we are advancing the
Date
object by one day,self.day
will change. Depending on what day it is,self.month
andself.year
may also change. - You may find it helpful to use the following list by declaring it
on the first line of the method:
days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
You can then use this list to quickly determine the number of days in a month. For example,
days_in_month[1]
is 31 to represent that January (month 1) has 31 days. You can useself.month
to index this list to find the number of days in the month that is represented by aDate
object.If you use this approach, be sure to take into account that the
days_in_month
list is not accurate forDate
objects that represent February during leap years. However, you can use anif
statement to account for this case when necessary.Examples:
>>> d = Date(7, 25, 2022) # an easy case first! >>> d 07/25/2022 >>> d.add_one_day() >>> d 07/26/2022 >>> d = Date(7, 31, 2022) # a more difficult case >>> d 07/31/2022 >>> d.add_one_day() >>> d 08/1/2022 >>> d = Date(12, 31, 2022) # a hard case >>> d 12/31/2022 >>> d.add_one_day() >>> d 01/01/2021 >>> d = Date(2, 28, 2020) # a harder case >>> d.add_one_day() >>> d 02/29/2020 >>> d.add_one_day() >>> d 03/01/2020
- You are responsible for testing this method thoroughly. Make sure it works for special situations like change of month or change or year.
-
Write the method
rem_one_day(self)
that changes the called object so that it represents one calendar day before the date that it originally represented. This will be similar to theadd_one_day(self)
method above, but will obviously require different logic and test cases. Test it thoroughly.Notes:
- This method should not return anything. Instead, it should change the value of one or more variables inside the called object.
-
Since we are advancing the
Date
object by one day,self.day
will change. Depending on what day it is,self.month
andself.year
may also change. -
You are responsible for testing this method thoroughly. Make sure it works for special situations like change of month or change or year.
-
Write the method
add_n_days(self, n)
that changes the calling object so that it representsn
calendar days after the date it originally represented.Notes:
-
This method should not return anything. Instead, it should change the value of one or more variables inside the called object.
-
Don’t copy code from the
add_one_day
method. Instead, you should call theadd_one_day
method in a loop to accomplish the necessary changes. -
Because the
add_one_day
method doesn’t explicitly return a value, it will implicitly return the special valueNone
. As a result, you need to be careful how you call it. In particular, you should not call it as part of an assignment or as part of aprint
statement. For example, this would not work:# don't do this! print(self.add_one_day())
because you will end up printing
None
. Rather, you should simply call the method on its own line, and ignore the value ofNone
that is returned:self.add_one_day()
-
To print the current state of the
Date
object, you can simply do the following:print(self)
since doing so will call the
__repr__
method to produce a string representation ofself
that you can print.-
This method should work for any nonnegative integer
n
. -
It might be helpful to add a print statement while you are testing this method, but to comment it out later. This print statement would print all of the dates from the starting date to the finishing date, inclusive of both endpoints. If
n
is 0, only the starting date should be printed.
Examples:
>>> d = Date(7, 25, 2022) >>> d.add_n_days(3) 07/25/2022 07/26/2022 07/27/2022 07/28/2022 >>> d 07/28/2021 >>> d = Date(7, 25, 2022) >>> d.add_n_days(0) 07/25/2022 >>> d 07/25/2022
Remove this print statement once you are certain that your method works correctly.
-
-
Write the method
rem_n_days(self, n)
that changes the calling object so that it representsn
calendar days before the date it originally represented. This method should be very similar to theadd_n_days(self, n)
method above, but obviously it will require different logic and test cases. Test it thouroughly. -
Write the method
__eq__(self, other)
that returnsTrue
if the called object (self
) and the argument (other
) represent the same calendar date (i.e., if the have the same values for theirday
,month
, andyear
attributes). Otherwise, this method should returnFalse
.Recall from lecture that the name
__eq__
is a special method name that allows us to override the==
operator–replacing the default version of the operator with our own version. In other words, when the==
operator is used withDate
objects, our new__eq__
method will be invoked!This method will allow us to use the
==
operator to see if twoDate
objects actually represent the same date by testing whether their days, months, and years are the same, instead of testing whether their memory addresses are the same.After implementing your
__eq__
method, try re-executing the following sequence of statements from Task 0:>>> d1 = Date(1, 1, 2022) >>> d2 = d1 >>> d3 = d1.copy() # Determine the memory addresses to which the variables refer. >>> id(d1) 430542 # Your memory address may differ. >>> id(d2) 430542 # d2 is a reference to the same Date that d1 references. >>> id(d3) 413488 # d3 is a reference to a different Date in memory. # The new == operator tests whether the internal date is the same. >>> d1 == d2 True # Both refer to the same object, so their internal # data is also the same. >>> d1 == d3 True # These variables refer to different objects, but # their internal data is the same!
Notice that we now get
True
when we evaluated1 == d3
. That’s because the new__eq__
method compares the internals of the objects to whichd1
andd3
refer, rather than comparing the memory addresses of the objects. -
Write the method
is_before(self, other)
that returnsTrue
if the called object represents a calendar date that occurs before the calendar date that is represented byother
. Ifself
andother
represent the same day, or ifself
occurs afterother
, the method should returnFalse
.Notes:
- This method is similar to the
__eq__
method that you have written in that you will need to compare the years, months, and days to determine whether the calling object comes beforeother
.
Examples:
>>> ny = Date(1, 1, 2023) >>> d = Date(7, 25, 2022) >>> ny.is_before(d) False >>> d.is_before(ny) True >>> d.is_before(d) False >>> d3 = Date(12,31,2022) >>> d3.is_before(ny) True >>> d4 = Date(12,31,2023) >>> d4.is_before(ny) False
You will need additional test cases to be certain that this works correctly!
- This method is similar to the
-
Write the method
is_after(self, other)
that returnsTrue
if the calling object represents a calendar date that occurs after the calendar date that is represented byother
. Ifself
andother
represent the same day, or ifself
occurs beforeother
, the method should returnFalse
.Notes:
- There are two ways of writing this method. You can either emulate
your code for
is_before
OR you can think about how you could call__eq__
(==
) andis_before
to make writing this method very simple.
- There are two ways of writing this method. You can either emulate
your code for
-
Write the method
diff(self, other)
that returns an integer that represents the number of days betweenself
andother
.Notes:
- This method should not change
self
nor should it changeother
during its execution. -
The sign of the return value is important! In particular:
- If
self
andother
represent the same calendar date, this method should return 0. - If
self
is beforeother
, this method should return a negative integer equal to the number of days between the two dates. - If
self
is afterother
, this method should return a positive integer equal to the number of days between the two dates.
- If
Suggested Approach:
- Since this method should not change the original objects, you should
first create true (deep) copies of
self
andother
. - Then, use
is_before
oris_after
to figure out which date comes first. - You can use the
add_one_day
method that you have already written in a similar way to how you used it in theadd_n_days
method to count up from one date to another. However, unlike in that method, indiff
it is not clear how many times you need to calladd_one_day
to get an appropriate count from one date to the other. What kind of loop is well-suited for this kind of problem? - Once you know how many days separate the two values, you can again
use
is_before
oris_after
to figure out whether the returned result should be positive or negative. - You should not try to subtract years, months, and days between the two dates. This technique is too prone to mistakes.
- You should also not try to use
add_n_days
to implement yourdiff
method. Checking all of the possible difference amounts will be too slow!
Examples:
>>> d1 = Date(7, 25, 2022) >>> d2 = Date(8, 1, 2022) >>> d2.diff(d1) 7 >>> d1.diff(d2) -7 >>> d1 # Make sure the original objects did not change. 07/25/2022 >>> d2 08/1/2022 >>> #another example: >>> d3 = Date(7, 25, 2022) >>> d4 = Date(4, 17, 2023) >>> d4.diff(d3) 266 >>> d3.diff(d4) -266 # Here is an example that pass over a leap day. >>> d5 = Date(2, 15, 2020) >>> d6 = Date(3, 15, 2020) >>> d6.diff(d5) 29
- This method should not change
-
Write the method
day_of_week(self)
that returns a string that indicates the day of the week of theDate
object that calls it. In other words, the method should return one of the following strings:'Monday'
,'Tuesday'
,'Wednesday'
,'Thursday'
,'Friday'
,'Saturday'
,'Sunday'
.Suggested Approach:
- Try using the
diff
method from a known date. For example, how could it help to know the number of days between the called object and aDate
object representing Monday, April 18, 2016? How might the modulus (%
) operator help? - Calling
diff
will give you a negative number if theDate
you are operating on comes before the known date used byday_of_week
. You should leave the result as a negative number in such cases; you should not take its absolute value. - It will be useful to copy and paste the following list to the first
line of your method:
day_of_week_names = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
Examples:
>>> d = Date(4, 17, 2023) >>> d.day_of_week() 'Monday' >>> Date(1, 1, 2100).day_of_week() 'Friday' >>> Date(7, 4, 1776).day_of_week() 'Thursday'
- Try using the
Task 2: Some Date
clients
30 points; individual-only
Now that you have written a functional Date
class, we will put it to use!
Remember that the Date
class is only a blueprint, or template, for how
Date
objects should behave. We can now create Date
objects according to
that template and use them in client code.
Getting started
To start, create a new file and save it as a6task2.py
.
Put all of the code that you write for this problem in this file.
Don’t forget to include appropriate comments at the top of the file, and a docstring for
your function.
IMPORTANT: Since your clients will need to construct Date
objects,
you need to import the Date
class. Therefore, make sure that a6task2.py
is in the same directory as a6task1.py
, and include the following
statement at the top of a6task2.py
:
from a6task1 import Date
Your tasks
-
Most standard options expire on the third Friday of the month. Write a function called
options_expiration_days(year)
that returns a list of all of theDate
s on which options expire during a calendar year.Example output:
>>> options_expiration_days(2020) [01/17/2020, 02/21/2020, 03/20/2020, 04/17/2020, 05/15/2020, 06/19/2020, 07/17/2020, 08/21/2020, 09/18/2020, 10/16/2020, 11/20/2020, 12/18/2020] >>> >>> options_expiration_days(2021) [01/15/2021, 02/19/2021, 03/19/2021, 04/16/2021, 05/21/2021, 06/18/2021, 07/16/2021, 08/20/2021, 09/17/2021, 10/15/2021, 11/19/2021, 12/17/2021]
Notes/Hints:
-
We know that every month has a third Friday, and we know the earliest date it can occur is the 15th of the month. Use one or more repetition constructs to find the correct date.
-
Your function should use an accumulator pattern to build a list of Date objects, and return that list.
-
-
The US stock market and options exchanges are closed on the following holidays each year:
- New Year’s Day (January 1st, but observed on January 2nd if January 1st is a Sunday)
- Martin Luther King Day (the third Monday in January)
- President’s Day (the third Monday in February)
- Memorial Day (the last Monday in May)
- Independence Day (July 4th, observed on July 5th if July 4th is a Sunday)
- Labor Day (the first Monday in September)
- Thanksgiving Day (the fourth Thursday in November)
- Christmas Day (the 25th of December, observed on December 26th if the 25th is a Sunday)
- (The markets are also closed on Good Friday, but its date depends on the Lunar calendar and not the Gregorian Calendar, so we will skip it for now.)
Write a function
market_holidays(year)
that will returns a list of theDate
s of all market holidays for a given year. In addition, the function should print out each holiday as it generates it.Example output:
>>> holidays = market_holidays(2020) New Year's Day is observed on Wednedsay 01/01/2020 Martin Luther King Day is observed on Monday 01/20/2020 President's Day is observed on Monday 02/17/2020 Memorial Day is observed on Monday 05/25/2020 Independence Day is observed on Saturday 07/04/2020 Labor Day is observed on 09/07/2020 Thanksgiving Day is observed on Thursday 11/26/2020 Christmas Day is observed on Friday 12/25/2020 >>> holidays [01/01/2020, 01/20/2020, 02/17/2020, 05/25/2020, 07/04/2020, 09/07/2020, 11/26/2020, 12/25/2020] >>> >>> holidays = market_holidays(2021) New Year's Day is observed on Friday 01/01/2021 Martin Luther King Day is observed on Monday 01/18/2021 President's Day is observed on Monday 02/15/2021 Memorial Day is observed on Monday 05/31/2021 Independence Day is observed on Monday 07/05/2021 Labor Day is observed on 09/06/2021 Thanksgiving Day is observed on Thursday 11/25/2021 Christmas Day is observed on Saturday 12/25/2021 >>> holidays [01/01/2021, 01/18/2021, 02/15/2021, 05/31/2021, 07/05/2021, 09/06/2021, 11/25/2021, 12/25/2021]
Notes:
-
In your function, use multiple
Date
objects to represent each of the holidays, and use theDate
methods to find the correct day of week as needed. -
The sample output above includes the print out generated by the function, as well as a list of
Date
s returned by the function.
Submitting Your Work
Use the link to GradeScope (left) to submit your work.
Be sure to name your files correctly!
Under the heading for Assignment 6, attach these 2 required files:
a6task1.py
, and a6task2.py
.
When you upload the files, the autograder will test your program.
Notes:
- You may resubmit multiple times, but only the last submission will be graded.
Warnings about Submissions
-
Make sure to use these exact file names, or Gradescope will not accept your files. If Gradescope reports that a file does not have the correct name, you should rename the file using the name listed in the assignment page.
-
If you make any last-minute changes to one of your Python files (e.g., adding additional comments), you should run the file in Spyder after you make the changes to ensure that it still runs correctly. Even seemingly minor changes can cause your code to become unrunnable.
-
If you submit an unrunnable file, Gradescope will accept your file, but it will not be able to auto-grade it. If time permits, you are strongly encouraged to fix your file and resubmit. Otherwise, your code will fail most if not all of our tests.
Important note regarding test cases and Gradescope:
-
You must test each function after you write it. Here are two ways to do so:
- Run your file after you finish a given function. Doing so will bring you to the Shell, where you can call the function using different inputs and check to see that you obtain the correct outputs.
-
Add test calls to the bottom of your file, inside the
if __name__ == '__main__'
control structure. For example:if __name__ == '__main__': print("mystery(6,7) returned", mystery(6,7))
These tests will be called every time that you run the file, which will save you from having to enter the tests yourself. We have given you an example of one such test in the starter file.
-
You must not leave any
print
statements in the global scope. This will cause an error with the Gradescope autograder. Make sure all of yourprint
statements are inside a function scope or insude theif __name__ == '__main__'
control structure.