In this project you will build a class hierarchy that allows us to extend our Gregorian calendar class to other calendars. In our case, these will be the Shire calendar from J. R. R. Tolkien’s The Lord of the Rings and the calendar of the French Revolution, le calendrier Republicain.
The Shire calendar and the French Revolutionary calendar both have 12 months of 30 days each. They differ in where they place the remaining five or six days of the year. Both calendars have the convenient property that the weekdays of dates do not shift from year to year.
The Shire calendar is discussed in Appendix D of The Lord of the Rings.
The seven days of the week, listed with their English equivalent, are
- Sterday (Saturday)
- Sunday (Sunday)
- Monday (Monday)
- Trewsday (Tuesday)
- Hensday (Wednesday)
- Mersday (Thursday)
- Highday (Friday)
Every year begins on the first day of the week, Sterday (Saturday) and ends on the last day of the week, Highday (Friday). Midyear’s Day, and, in leap-years, Overlithe, have no weekday name. The Lithe before Midyear’s Day is 1 Lithe, and the one after is 2 Lithe. The Yule at the end of the year is 1 Yule, and the one at the beginning of the year is 2 Yule. Lithe and Yule are conveniently located to serve as times for feasting and merrymaking.
In the Shire calendar every fourth year is a leap year except the last of year of the century (i.e., a year ending in 00). Unlike the Gregorian calendar, years divisible by 400 are not leap years.
Because there are only 364 weekdays and weeks are 7 days long, dates fall on the same day of the week every year:
The Shire-folk introduced one small innovation of their own. . . They found the shifting of the weekday names in relation to dates from year to year untidy and inconvenient. So in the time of Isengrim II they arranged that the odd day which put the succession out, should have no weekday name. After that Midyear’s Day (and the Overlithe) was known only by its name and belonged to no week. In consequence of this reform the year always began on the First Day of the week and ended on the Last Day; and the same date in any one year had the same weekday name in all other years. . .
The calendar of revolutionary France is another attempt at calendric reform. Like the Shire calendar it consists of 12 months, each of 30 days. However, months were organized into three 10-day metric weeks. The remaining five or six days, which came at the end of the year, belonged to no month and were not weekdays.
There are five or six special days tacked onto the end of year. These do not belong to any month nor are they weekdays. Le jour de la Revolution only appeared in leap-years.
For simplicity we will assume the year starts in Vendemiaire and the numbering of years is the same as the Gregorian calendar. The new year was actually the day of the autumn equinox in Paris, and year 1 corresponded to our year 1792.
Because there are only 360 weekdays and weeks are 10 days long, le calendrier Republicain also has the convenient property that the weekdays of dates do not shift from year to year.
The last calendar is our old friend the Gregorian calendar. As you are by now very well aware, it does not have the convenient property that the days of the year occur on fixed weekdays.
In this project we will refer to months by their names (strings), rather than by a number (ints). We will also drop the restriction on the Gregorian calendar that we only accept years from 1753 on.
You code should reside in a file named calendar.py. A stub of this file is provided as part of the project. Implement a superclass (parent class) for calendars named Calendar.
This class will implement the following three public methods:
def date_to_day_of_year(self, *args):
The valued returned is a number between 1 and the number of days in the year (365 or 366). The input *args is a variable number of inputs, as discussed here. Given a Calendar object obj, this method can be called with either three arguments, month, day, year,
obj.date_to_day_of_year("Messidor", 14, 1789)
or with two arguments, day and year,
obj.date_to_day_of_year("Midyear' s Day", 1418)
You need to determine which by looking at len(args); the latter is a tuple of inputs.
This function should return the day of the year corresponding to the date. For instance,
obj.date_to_day_of_year("Midyear' s Day" , 1418)
should return 183.
def day_of_year_to_date(self, year, day_of_year):
The input year is the year (e.g., 1693, 2018) and day_of_year) is a number between 1 and the number of days in the year (365 or 366). This method should convert day_of_year into the corresponding date in terms of the month (a string), and (possibly) day and year (integers). For instance, for the Gregorian calendar, the call
greg.day_of_year_to_date(self, 2018, 1)
```should return `('Jan', 1, 2018)`, and for the Shire calendar,
day_of_year_to_date(self, 2018, 31)
('Afteryule', 30, 2018). On the other hand, for the Shire calendar,
day_of_year_to_date(self, 2018, 182)
('1 Lithe', 2018).
def date_to_day_of_week(self, month, day, year):
This can be implemented in a general way by first converting the month/day/year information into the day of the year, and applying methods that convert that to the day of the week.
There is also a private method that should be implemented the Calendar class. This method checks whether a given year is a leap year. In my code is looks something like this; you are free to name it what you like:
def __is_leap_year(self, year):
# Return True if year is a leap year and False otherwise.
# Leap years are years devisible by 4 unless they are also
# divisible by 100, in which case they are leap years only
# if they are also divisible by 400.
This function should use the leap year rules for the Gregorian and French revolutionary calendar. The Shire calendar class will need its own special version of this function since years divisible by 400 are not leap years.
Derive from this parent class three child classes:
Each class will implement its own version of a third method that takes the number of the day in a year and returns the day of the week:
def day_of_year_to_day_of_week(self, year, day_of_year):
The value returned should be a string that conforms to the names of months and special days given in calendar.py.
In addition, the Shire calendar class will need its own version of the leap-year testing method:
def __is_leap_year(self, year):
# Return True if year is a leap year and False otherwise.
# Leap years are years divisible by 4 unless they are also
# devisible by 100.
This is needed because the Shire calendar lacks the extra day every 400 years. (The events in The Hobbit and The Lord of the Rings occur about 1400 years after the founding of the
Shire, so they would only be 3-4 days off and perhaps had not yet noticed the drift.)
- You may not use any of the functions from the Python datetime module in your code. You may, however, use functions from datetime to test your code.
- Your methods should check that the inputs are valid. If they are not, they should return None. For instance, 387 is not a valid day of the year. If the month is ‘Feb’, then 30 is not a valid value for the day of the month. Days that are not part of months should not have a day specified for them, so if shire is a Shire Calendar object should be flagged as bogus. Another error to check for is incorrect types. The Python idiom for checking the type of a variable.
If you do encounter a bogus date, you should raise an RuntimeError exception. You can read about exceptions here and here.
- If you implement an init() method, it should not take any inputs other than self.
- You are free to implement any addition private methods inside your class. The names of private methods and variables should begin with (double underscores, sometimes called “dunders” in the world Pythonic). This makes them inaccessible to outsiders.
- You should try to modularize your code as much as possible by introducing methods that perform tasks common to the four public methods. For instance, methods that determine whether or not a year is a leap year or whether inputs are valid can be used in multiple places. Avoid replicating the same code in different places.
- Any print() statements or test code needs to be under the control.
- If you get stuck and cannot implement a method, just leave a stub instead. Don’t submit broken code that won’t run but which will inhibit grading.
- Please do not look up how to do this project on the Internet. Besides being a Level III violation of the Honor Code, you may end up cutting and pasting code you don’t understand and can’t explain, and then it’s off to the Honor Council. You are free to search the Internet for information about how to do simple tasks in Python.
- Please be very careful when working with others on the projects. The software analysis tools I am using are beginning to reveal patterns where groups of people are writing code that is only cosmetically different (i.e., variables have different names) but the code is structurally identical. Remember what the syllabus says:
You may discuss algorithm design only with others currently enrolled in the CSCI 141. An “empty hands” policy must be observed when you meet with your classmates to discuss a programming assignment. You are free to discuss any aspect of the assignment, but you must leave the meeting without any written or electronic record of these discussions. This includes photographs.
Keep in mind that “Submitting a paper, lab report, project, thesis or other assignment as ones own that has been significantly created by someone else, whether the work has been purchased, borrowed, found, etc.” and “Soliciting another to participate in unethical behavior” are both Level III violations of the Honor Code. This restriction on working with others includes test code as well as code that solves the problem of interest.
You should create test files:
- test_french.py, which contains tests code for the Calendrier Republicain class,
- test_gregorian.py, which contains tests code for the Gregorian Calendar class,
- test_shire.py, which contains tests code for the Shire Calendar class, and
- test.py, which executes all the tests in one go.
You are free to construct tests as you wish. Stubs of the preceding files using Python’s unittest framework are provided as part of this project.
A requirement of this project is that your test code at the very least executes every line of your calendar code. As we discussed in class, you can check your test coverage using the Python coverage tool.
The CS machines already have coverage installed. If your personal machine does not have coverage installed, you can install it with pip install coverage in a terminal window on a Mac, a Linux subsystem terminal window in Windows 10, or an Anaconda command window under Windows.
If test.py executes all your tests, then you can obtain an annotated source file calendar.py, cover with
coverage run test.py coverage annotate
Remember that lines that are executed are indicated by
> and lines that are not executed are indicated by !. You can obtain an HTML version of the coverage results with
coverage run test.py coverage html
This will produce a directory htmlcov that contains a file index.html that you can open to see the test coverage with untested lines highlighted in a light red.
I treat the special days (e.g., Midyear’s Day) as a one-day or zero-day months (zero-day for those special days that only appear in leap-years). I found that my Gregorian calendar code generalized to the other two calendar without much trouble using this approach.
The code for computing the day of the week for the French Revolutionary calendar is straightforward and doesn’t involve the year. For the Shire calendar you need to pay attention to the non-weekdays Midyear’s Day and Overlithe that show up in the middle of the year, but other than that it’s straightforward.
- Constructing a class hierarchy consisting of Calendar and Gregorian Calendar with the correct functionality and test program will suffice for a grade of C.
- Constructing a class hierarchy consisting of Calendar, Gregorian_Calendar, and one of the other calendars with the correct functionality and test program will suffice for a grade of at least B.
- Constructing a class hierarchy consisting of Calendar, Gregorian_Calendar, and both of the other calendars with the correct functionality and test program is needed for full credit.
Upload your calendar code calendar.py and test code mytests.py to the Blackboard site.