MF 602

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 Dates 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., in convert_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 your print statements are inside a function scope or insude the if __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

  1. Begin by creating a class definition, with the following:

    • The __init__(self, new_month, new_day, new_year) method, which is the constructor for Date objects. In other words, this is the method that Python uses when making a new Date object. It defines the attributes that compose a Date object (month, day, and year) and accepts parameters to set an object’s attributes to some initial values.

    • The __repr__(self) method, which returns a string representation of a Date object. This method will be called when an object of type Date 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 a Date 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 type Date with the same month, day, and year that the called object has. This allows us to create deep copies of Date 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.

  2. Write the method is_leap_year(self), which returns True if the called object is in a leap year, and False otherwise. In other words, when we create a Date object and call its is_leap_year method, the method will return whether that specific Date 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
      
  3. Write the method is_valid_date(self) method, which returns True 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
      
  4. 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 and self.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 use self.month to index this list to find the number of days in the month that is represented by a Date object.

    If you use this approach, be sure to take into account that the days_in_month list is not accurate for Date objects that represent February during leap years. However, you can use an if 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.
  5. 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 the add_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 and self.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.

  6. Write the method add_n_days(self, n) that changes the calling object so that it represents n 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 the add_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 value None. 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 a print 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 of None 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 of self 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.

  7. Write the method rem_n_days(self, n) that changes the calling object so that it represents n calendar days before the date it originally represented. This method should be very similar to the add_n_days(self, n) method above, but obviously it will require different logic and test cases. Test it thouroughly.

  8. Write the method __eq__(self, other) that returns True if the called object (self) and the argument (other) represent the same calendar date (i.e., if the have the same values for their day, month, and year attributes). Otherwise, this method should return False.

    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 with Date objects, our new __eq__ method will be invoked!

    This method will allow us to use the == operator to see if two Date 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 evaluate d1 == d3. That’s because the new __eq__ method compares the internals of the objects to which d1 and d3 refer, rather than comparing the memory addresses of the objects.

  9. Write the method is_before(self, other) that returns True if the called object represents a calendar date that occurs before the calendar date that is represented by other. If self and other represent the same day, or if self occurs after other, the method should return False.

    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 before other.

    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!

  10. Write the method is_after(self, other) that returns True if the calling object represents a calendar date that occurs after the calendar date that is represented by other. If self and other represent the same day, or if self occurs before other, the method should return False.

    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__ (==) and is_before to make writing this method very simple.
  11. Write the method diff(self, other) that returns an integer that represents the number of days between self and other.

    Notes:

    • This method should not change self nor should it change other during its execution.
    • The sign of the return value is important! In particular:

      • If self and other represent the same calendar date, this method should return 0.
      • If self is before other, this method should return a negative integer equal to the number of days between the two dates.
      • If self is after other, this method should return a positive integer equal to the number of days between the two dates.

    Suggested Approach:

    • Since this method should not change the original objects, you should first create true (deep) copies of self and other.
    • Then, use is_before or is_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 the add_n_days method to count up from one date to another. However, unlike in that method, in diff it is not clear how many times you need to call add_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 or is_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 your diff 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
    
  12. Write the method day_of_week(self) that returns a string that indicates the day of the week of the Date 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 a Date object representing Monday, April 18, 2016? How might the modulus (%) operator help?
    • Calling diff will give you a negative number if the Date you are operating on comes before the known date used by day_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'
    

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

  1. 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 the Dates 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.

  2. 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 the Dates 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 the Date 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 Dates 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:

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 your print statements are inside a function scope or insude the if __name__ == '__main__' control structure.