Features


A Date Object In C++

David Clark


The author has ten years programming experience and is a senior research scientist in the advanced technology group of Miles Diagnostics, a manufacturer of medical diagnostics instruments and reagents.

It is something of a paradox that time is so difficult to define and yet we can measure its passage with more precision than any other physical quantity. One method to mark places in time is to assign dates to particular days. Western civilization keeps track of those dates by referring to calendars, specifically the Gregorian calendar, which was adopted in 1582.

This article describes a date object implemented in C++, which simplifies date manipulation.

Calendars And Julian Dates

Different cultures create different types of calendars. As far as I know, all are derived from attempts to measure the passage of the seasons. The Julian calendar, not to be confused with a Julian date, was one method adopted by the Romans.

The term "Julian date" takes a lot of abuse. Though a Julian date has a very precise meaning in astronomy, we will relax that meaning here somewhat. In general terms, a Julian date is simply an integer representing a particular day in history. Many computer programs use something like a Julian date, assigning numbers to dates. However, many of these programs assign different numbers to the same date. I will use the same numbering system that astronomers use.

One final point about Julian dates: in astronomy, Julian dates change at noon instead of midnight. However, the program I describe interprets date changes as occurring at midnight.

Implementation

The files DATES.HPP, DATES.CPP and DATETEST.CPP in Listing 1 to Listing 3 show an implementation of a date object called DateObject. I compiled and tested the object with Zortech C++ v1.07 under MS-DOS 3.30.

The header file DATES.HPP (Listing 1) contains the declaration of the DateObject type, and several useful constants. DATES.CPP, shown in Listing 2, contains most of the details of the implementation. Listing 3, DATETEST.CPP, is a simple demonstration program illustrating some of the ways that DateObjects may be used.

DateObjects can represent dates ranging from 1 January 4,700 BC to 31 December 25,000 AD. Though the limits are somewhat arbitrary, this range is adequate for my own uses. The boundaries are due to the implementation of the internal calculations which convert from day, month and year to a Julian date, and vice versa. The calculations will overflow the int used to represent the year if the range is exceeded by a large amount. The range could probably be extended by representing the year as a long int, but I have not bothered to try it.

Data

The data associated with a DateObject consists of six fields listed at the top of the DateObject class declaration (Listing 1) . Julian is a long int representing the Julian date. The value BADDATE, #defined above the declaration of the DateObject, represents an invalid date.

The DateFormatPtr field contains a pointer to a dynamically allocated character string. The string acts as a template that describes how a particular DateObject should be converted to a printable string.

The Day, Month, Year and DayOfWeek fields represent what their names imply. The Year field takes on negative values to represent years B.C. These fields are somewhat redundant in that all of the information they contain can be calculated from the Julian date. I have included these fields for my own convenience, based on the untested assumption that some operations on DateObjects are faster if the information is kept at hand rather than recalculated. If storage space is a more pressing concern for you, these fields could probably be removed with little effort or performance degradation.

Internal Functions

In addition to the public methods associated with a DateObject, DATES.CPP (Listing 2) contains functions which are used only within the DateObject code. The function IsLeap() returns a non-zero value if the year passed to it is a leap year and returns zero otherwise.

Checking for a leap year is not just a matter of determining if the year is evenly divisible by four. Leap years occur in the calendar because the time it takes the Earth to orbit the sun is not an integral number of days. It is almost 365.25 days — almost, but not quite. Over a period of 100 years, the small errors accumulate, growing to almost one extra day. So, years evenly divisible by 100 are not leap years, unless they are divisible by 400 (which corrects for even smaller errors). By the same token, there is a special rule for years evenly divisible by 4000. Beyond that, my program does not care. For example, the year 2000 is a multiple of four, which would normally be a leap year. However, it is also a multiple of 100, which is not normally a leap year. However, it is also a multiple of 400, which means it is a leap year.

DaysInMonth() returns the number of days in a particular month in a particular year. If the month is February, the function IsLeap() is called to determine if it is in a leap year. If so, 29 is returned. If the month is not February or the year is not a leap year, the number of days in the month is looked up in the array MonthDays and returned.

The function CheckForValidDate() determines if the Day, Month and Year passed to it represent a valid date. Besides checking that Year is within the limits of MINYEAR and MAXYEAR, the function checks that Month is valid and that Day is valid for the Month and Year.

The small function JulianToDayOfWeek() calculates the day of the week from the Julian date passed as its argument.

The functions DMYtoJulian() and JulianToDMY() convert days, months and years to Julian dates and vice versa. These functions were taken almost directly from Numerical Recipes in C by Press, Flannery, Teukolsky and Vettering, pp. 10-13. Note that these functions take into consideration the ten-day gap that occurred in October of 1582 when converting from the Julian to the Gregorian calendar. I believe these functions are the only ones which make any direct or indirect calls to the floating point library. If recovering the memory used by the floating point calculations is important, these routines could probably be converted to all integer (or probably long int) operations.

Constructors

Four constructors for DateObjects are declared in Listing 1 and defined in Listing 2. The constructor used depends on how the instance of a DateObject is declared. Instances declared without initializers are simply set to the current date.

The first constructor in the Listing 2 obtains the current date by calling DOS interrupt 33 (21H), service 42 (2AH). The date is then copied to the correct fields of the DateObject. No checks for an illegal date are performed since it is assumed that MS-DOS always returns a legal date. A copy of the default format string is allocated, if possible, by calling the standard library function strdup(). The DateFormatPtr field points to that copy. If a copy of the format string cannot be allocated, the Julian field is assigned the value BADDATE to indicate that the DateObject is invalid.

The second constructor in Listing 2 is a "copy initializer". It takes as its single argument the address of another DateObject and copies the data from its argument into its own data fields. Copy initializers are not used so much by the programmer as by the compiler — when a DateObject is a pass-by-value argument to a function or when a DateObject is returned from a function, for example.

The last two constructors accept initializers for the day, month and year. One uses a default format string pointed to by the static variable CurrentDateFormat (defined near the top of Listing 2) , while the other accepts a format string as one of the initializers. Both of these constructors call the member function ChangeDate() to copy data from the initializers to the new instance. These constructors check that the requested date is legal. Dates like 31 February 1980 will not be accepted. If the initializers would result in an illegal date, the Julian field is assigned the value BADDATE to indicate that the DateObject does not contain valid data.

All of the constructors allocate the format string from dynamic memory by calling the standard library routine strdup(). If the allocation fails, strdup() returns a NULL pointer and the Julian field is set to BADDATE.

Destructor

The destructor for a DateObject simply releases the memory containing the format string, pointed to by DateFormatPtr, with a call to free(). NULL pointers are allowed since free() will accept a NULL argument without harm.

Access Functions

The group of access functions, GetDay(), GetMonth(), GetYear(), GetDayofWeek(), GetFormat(), and GetJulian(), examine the internal contents of a DateObject. Note that GetFormat() returns a pointer to a copy of the format string, not to the format string itself, insuring that the format string cannot be altered by anything other than DateObject methods and forcing the programmer to free the unneeded copies.

Overloaded Operators

One of the nice features of C++ is operator overloading, the ability to define new actions for existing operators. Operator overloading allows you to do date arithmetic and comparison with the same operators you use with "normal" operands.

Because DateObjects contain the Julian date, logical comparison operations are easy. The overloaded comparison operators are implemented inline in Listing 1 and simply perform the relevant comparison on the Julian fields.

The assignment operator copies most of the data from the rvalue, which is passed by reference, into the lvalue (this). A call to strdup() dynamically allocates and copies a new format string. The object containing the newly copied data is returned.

It seems to me that date arithmetic is somewhat analogous to pointer arithmetic in C. It makes no sense to multiply or divide dates, but some addition and subtraction operations seem intuitive. For example, it makes sense to add integral quantities to a date to yield a new date. However, just as with pointers, it does not make sense to add two dates together. What is the meaning of "4 July 1776 + 20 July 1969" anyway? This addition should also be commutative (i.e. integer + Date == Date + integer).

Subtraction with DateObject operands can take two forms. When one DateObject is subtracted from another, the result is an integral value representing the number of days between the two dates. In contrast to addition, subtraction is not commutative. It seems logical to subtract a number of days from a date but not to subtract a date from a number of days, i.e. "3L - 26 January 1986" has no meaning that I can see.

The simplest overloaded operator is the subtraction operator used when one DateObject is subtracted from another. This operator is implemented as an inline friend operator, declared near the bottom of Listing 1. The operator simply subtracts the Julian field of the second DateObject from the Julian field of the first and returns the difference. The returned value represents the number of days by which the two dates differ.

The subtraction operator is also overloaded in the case where a long is subtracted from a DateObject. This operation returns a DateObject whose value preceeds the DateObject argument by the number of days represented by the long. For example, 20 July 1969 - 3L would yield 17 July 1969.

The binary addition operator, +, has been overloaded by two methods: adding a DateObject to a long, and adding a long to a DateObject. Both methods yield a new date which corresponds to the date operand plus a number of days equal to the long operand. For example, 20 July 1969 + -3L yields 17 July 1969. The methods specify a long int as one of the arguments, but an int may be used in an expression since the compiler will promote the int to a long.

The first overloaded operator for addition in Listing 2 is really the foundation for most of the other overloaded arithmetic operators, and is implemented as a friend operator. It first adds the long argument to the Julian field of the DateObject argument. The operator function checks that the result is within the range MINDATE to MAXDATE. If so, the Day, Month, Year and DayOfWeek fields are calculated. The operator function initializes the DateFormatPtr field to point to a copy of the date format string of the DateObject argument.

The remainder of overloaded arithmetic operators, -, ++, -, += and -=, with the exception of the friend subtraction operator, are all implemented as variations of addition.

Formatting

While using Microsoft's Excel spreadsheet, I found one of the most attractive features to be the user-definable formats that could be assigned to cells. From that inspiration, I built a simple format interpreter into the DateObject. The member function DateToString() dynamically allocates a string and fills it with a printable representation of a DateObject. The format of the string is controlled by the formatting instructions pointed to by DateFormatPtr.

Basically, the interpreter recognizes four special characters and several combinations thereof. These control the format for the day, month and year. The characters are d for the day, m for the month, y for the year, and \ as an escape prefix. The number of special characters encountered determines the exact formatting.

The current format is stored in the string pointed to by CurrentDateFormat and by default is m-dd-yyy. MS-DOS uses this same format to display file and system dates.

Format Examples

If a DateObject contained the date 24 July 1995, Table 1 shows how the date would be formatted with several different format strings.

In the first example, the date is in the default format, just as MS-DOS would display it. In the second example, the order of the day and month have been reversed and the numeric month has been replaced by an alphabetic abbreviation. In the third example, the d character is used in two places: first to format the day of the week, then to generate the day of the month. Notice the comma after the dddd. Any text that is not recognized as a special character is simply placed in the output string unchanged. The next two examples cause only the year and the day of the week, respectively, to be placed in the string. The last example shows that arbitrary text can be placed in the format string and passed through to the output string. Notice that the d in "date" must be preceeded by a \ or it will be replaced by the day of the month instead.

ChangeFormat() changes the string used to format a DateObject. The function attempts to dynamically allocate a copy of its argument, the new format string. If successful, the old format string is released, and a pointer to the new string is installed in DateFormatPtr field. If the new format string cannot be allocated, no changes are made.

The function ChangeDefaultDateFormat() is not a DateObject method. It changes the default format string attached to DateObjects when they are instantiated without an initializer for the format. The format string used for these initializations is pointed to by the static variable CurrentDateFormat in Listing 2. ChangeDefaultFormat() copies its argument to the heap and points CurrentDateFormat to the copy. The first call to ChangeDefaultDateFormat() sets the bookkeeping variable OnHeap to a nonzero value. Subsequent calls to ChangeDefaultDateFormat() will free the old string on the heap.

Miscellaneous

The function ChangeDate() changes the date of a DateObject. The function first checks that its arguments represent a valid date. If not, zero is returned. If the requested change is valid, a new Julian date is calculated, and the new data is copied into the DateObject. ChangeDate() does not alter the date format string, though ChangeFormat() can be used for that purpose. For a valid new date, the function returns 1. Note that two of the constructors are actually implemented with calls to ChangeDate().

The function ValidDate() determines whether a DateObject contains valid data by checking whether the Julian field has been set to BADDATE.

Demonstration Program

The program in Listing 3 demonstrates how a DateObject might be used. Although output is accomplished by means of the stream operators, these operators are not necessary to use DateObjects. Listing 4 shows the output produced by the program when the system date is set to 7 April 1993.

The program begins by declaring five variables of type DateObject. The first three are assigned specific dates and formats. Since the last two variables are not explicitly initialized, when the program starts they are set to the current system date using the default format.

The variables d1 and d2 are set to the earliest and latest dates respectively that a DateObject can hold, with the difference between the two assigned to Diff and displayed in a single statement. The value of Diff is thus the largest long operand that may be used in an arithmetic operation on DateObjects.

The variable d3 is initialized to 23 May 1968, which has the nice, round Julian date 2,440,000. The second group of statements checks whether or not the variable is assigned the Julian date we know it must have.

The next three groups of statements in Listing 3 illustrate more date arithmetic. The first group causes an overflow while the second group results in an underflow. In both cases, the result of the expression is a bad date. The third group does not cause overflow and returns a valid date, which is displayed.

The final group of statements in the example first displays the date contained in d4, which was initialized to the system date using the default format. Next the format is changed by a call to ChangeFormat. The new format will cause DateToString() to return a string containing only the week day. Next, DateObjects are used in a for statement, being preincremented, postincremented and compared. Note that d5 is never explicitly initialized. It has the same initial value as d4 since d5 was initialized to the system date by default. When the for statement is executed, the five days of the week following the system date are displayed.

The example does not exercise all of the capabilities of the DateObject, but does give you some idea of their flexibility.

Conclusion

There are a number of additions and improvements that could be made to the DateObject. An obvious addition would be to overload the stream operators, << and >>, to accept dates. The input operator would be somewhat more difficult because of the variety of formats in which dates can be written as strings.

Reading and writing dates to binary files can be accomplished simply by storing the Julian field and the format string. All of the other information can be reconstructed. Additional conversion methods might include functions to convert between DateObjects and MS-DOS file dates.

As implemented, the DateObject does not implement many of the powerful features of object-oriented programming (no inheritance or polymorphism for example). However, it is easy to imagine a DateObject as a descendant of a more general Time class. In addition to the Gregorian calendar, it should be possible to create sibling classes for date objects based on the Chinese, Hebrew, Moslem or Aztec calendars, all descending from an abstract Date class.