CSE2305 - Object-Oriented Software Engineering
exercises

CSE2305
Object-Oriented Software Engineering

Exercise 2: The 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.

 


This material is part of the CSE2305 – Object-Oriented Software Engineering course.
Copyright © Jon McCormack, 2005.  All rights reserved.

Last modified: June 30, 2005