Date
Class: solution
You were asked to write a class that represents a date and give it specific functionality.
Observations:
A Date class should represent a date only, not a time. Dates can be entered
and viewed in different formats, but the internal method used to represent them
should be independent of the particular format.
We could use three components to represent day, month and year internally, but this may make some calculations difficult.
A look at the UNIX system call ctime(3)
and the definition of
struct tm
, the basic date/time representation on my (BSD based)
UNIX system only handles years from 1900. This might be reasonable for a structure
to store the current date and time, but if we are dealing with a date in general
we might want to store dates before 1900 (think of a database of historical
documents from the first fleet, or artifacts from ancient Egypt, for example).
A common date system used by astronomers is known as a Julian Date. A Julian day begins at noon of the calendar date. Julian day 0 was 1 January, 4713 B.C.! (4713B.C. = -4712)
There is no year 0 as a calendar date — the year after 1 B.C. was 1 A.D.
The Gregorian calendar was adopted on 15 October 1582. There is a gap of ten
days between 4 Oct 1582 and 15 Oct 1582, meaning that the 15th of October was
directly following the 4th of October (Pope Gregor forced this in order to achieve
better agreement between the civil and the astronomical calendar). If we use
a long
to represent the Julian day internally, we can represent
dates from 1/1/4713 B.C. up to 31/12/31766 A.D.
For further information on Julian days and some code fragments to deal with
conversion, see:
Press, W.H. et. al. (1988) Numerical Recipes in C (2nd Ed.), Cambridge
U.P., p. 11. (Copies available from the library and bookshop). You may also
find this
web site helpful. These references details on how to convert from Julian
Dates to Calendar Dates and back.
The question asked for the Date class to be capable of a number of functions. So let's look at the basic class declaration:
class Date {
public:
// types for year, month and day
typedef short year_t;
typedef unsigned short month_t;
typedef unsigned short day_t;
typedef unsigned short week_t;
// constructors
Date(); // default constructor
Date(year_t year, month_t month, day_t day); // e.g. Date(2005,3,28);
Date( const Date& theDate );
// Assignment.
Date& operator=( Date& theDate );
// Date component accessors.
year_t year() const;
month_t month() const;
day_t day() const;
week_t weekOfTheYear() const;
day_t dayOfTheYear() const;
day_t dayOfTheWeek() const;
long julianDay() const;
// setter methods
bool setDate(year_t year, month_t month, day_t day); // to change the date returns false for illegal dates
static Date today(); // change the date to today
// Testing.
bool isLeapYear() const;
// printing to a stream
void print(ostream& stream ) const;
string toString() const;
private:
long myDate; // stores the date a a Julian date
};
There are several points to consider. First, what about errors? It's possible
to initialise a Date
with numbers that don't represent proper dates,
e.g. Date d(2001,2,31)
. Constructors can't return values so what
should we do?
The best thing to do would be to throw an exception - a topic covered
in a latter lecture.
For the time being it would be satisfactory to print an error message to cerr
.
Clearly we also will need support methods to check valid numbers for day, month
and year — since these might need to be used without creating a Date object
it would be best to define them as static to the class, e.g.:
class Date {
public:
.
.
.
static bool isYear( year_t year );
static bool isMonth( month_t month );
static bool isDayInMonth( year_t year, month_t month, day_t day);
static bool isLeapYear( year_t year );
.
.
.
};
Static methods are global to all objects of a given class; they cannot access individual objects directly, but they can be called before object initialisation. They can also be accessed without the need to create an object of that class, e.g.:
Date::year_t aYear;
cout << "Enter your year of birth" << endl;
cin >> aYear;
if (! Date::isYear(aYear) ) {
cout << "That's not a proper year!" << endl;
exit(-1);
} else {
Date d(aYear,1,1);
}
Implementation:
Not all the implementation will be covered here, but let's look at
a few things — first constructors. What Date should the default constructor
initialise to? We could define a default date, but what should it be? Some UNIX
systems use 1 January 1970 at their default. Another option might be to make
the default today, but this involves extra work and many default constructed
Date objects may be initialised shortly after construction to a new value. So
for efficiency we should make the default date a constant:
// default Julian Day is 1 January, 1970
static const long defaultJulianDay = 2440588L;
// Constructor defaults to the value of 'defaultJulianDay'
Date::Date() : myDate( defaultJulianDay ) {
}
The other constructors can be handled using the setDate
methods
defined in the class.
Here's the assignment operator:
// Assign myself from the supplied date
Date&
Date::operator=( const Date& date )
{
myDate = date.myDate;
return *this;
}
Check for leap years:
/* static */ bool
Date::isLeapYear( year_t year )
{
// Invalid years can not be leap years.
if ( !isYear( year ) )
return false;
// Adjust for BC
if (year < 0)
++year;
// Leap years are divisible by 4, years ending in 00, which
// are normal unless divisible by 400.
return (year % 100) ? (year % 4 == 0) : (year % 400 == 0);
}
If we have a method julianDay() that returns the Julian day representation then we can work out the day of the year:
day_t
Date::dayOfTheYear() const
{
return (day_t)(julianDay() - Date( year(), 1, 1 ).julianDay() );
}
Formatting:
As an option, you were asked to provide formatted output. Some C system
calls let you do this to an extent, but we are still faced with the problem
of dates before 1900. Also, what if we change languages? A better solution might
be to create a separateDateFormat
class that will return strings
and formats for output. DateFormat
could also be an abstract base
class, allowing for multiple language support. e.g.
DateFormat * ausfmt = new AustralianDateFormat;
DateFormat * usfmt = new USDateFormat;
ausfmt->useLongFormat();
ausfmt->showDayOfTheWeek();
usfmt->setSeperator(":");
Date d(7,8,2005);
d.print(cout, fmt); // prints "Wednesday, 7 August 2005"
d.print(cout, usfmt); // prints "8:7:2005"
Summary:
We could add many extra classes to represent a time period (microseconds to
years) so we could add time periods to dates and get a new date:
Date now, in50years
TimePeriod p;
now.today(); // set now to today's date
p.setYears(50); // a time period of 50 years
in50years = now + p;
cout << "In 50 years from today it will be " << in50years << endl;
The Date
class would be better with more overloaded operators,
including increment, decrement and comparison.
We could also add other classes to represent day in a year or things like "the first Tuesday in November", which might be useful for calendar and reminder programs.
What looked like a simple task (represent a date) can actually turn out to be pretty complex! The more you want to do the task well, the more classes and support is needed (error handling for example). Suddenly your simple "kit bag" of basic classes is looking quite complex...
At some point, these simple "support" classes become a whole framework for building applications. Many commercial and public domain frameworks are available for a variety of purposes, most commonly for building whole applications.