Stan Milam is a C programmer working in the Dallas area. He also teaches C at Mountain View College in Dallas. He can be reached through his internet address: milam@metronet.com.
Introduction
One of the most convenient attributes of the Standard C library time and date functions is their relatively fine granularity, which is in seconds. Having one-second resolution allows date and time to be stored as a single field, which is wonderful for logging purposes. The downside of this granularity is that it limits the range of the date and time functions. Most libraries use the long data type to store the combined date and time in the number of seconds elapsed since 1 January, 1970. As a result, standard functions cannot handle dates before 1 January, 1970. Also, since the long data type is 32 bits on most machines, the upper limit of the standard functions will be 18 January, 2038.This upper limit is fine for most date calculations, and perhaps the long data type will someday be extended to 64 or more bits, but some date calculations need to exceed this limit now.
In this article I describe a suite of functions that is as versatile as the Standard C time/date functions, but have a greatly extended range. To accomplish both versatility and extended range I've mimicked the standard time/date functions, but raised the granularity of the stored time from elapsed seconds to elapsed days. For example, the standard time() function returns a value of type time_t, which contains the number of seconds elapsed since 1 January, 1970. I've written a corresponding function called date() which returns the number of days elapsed since 1 January, 0001 A.D. I've also written several functions which do not correspond to any function in the Standard C library, but extend the capabilities of my date functions. Because my date functions are similar in nature to the standard functions, these extended functions could be easily adapted to work with standard time/date functions.
The Date Types
My date functions work with one of two date types which are analogous to the two time types in the Standard C library. These new date types are date_t (a long), and a structure, struct dt. The following code fragment shows how these new types are declared in the dates.h header file (this file is not listed here, but is included on this month's code disk):
typedef long date_t; /* Sequential day value */ struct dt { int dt_leap_year; /* Indicates a leap year */ int dt_year; /* The actual year */ int dt_month; /* The month, 0 - 11 */ int dt_mday; /* The day of month, 1-31 */ int dt_yday; /* The day of the year, 0-365 */ int dt_wday; /* The day of the week, 0-6,*/ /* 0=Sun*/ };You can use date_t to define variables which represent a given date (uniquely) as a long integer. Because dates are usually comprised of three different values, (years, months, and days), representing a date as a long integer greatly simplifies date math. (To see how this integer is calculated, see the sidebar "Converting a date to a date_t".)The second date type, struct dt, corresponds nicely to the structure used in the standard time/date functions, struct tm. dt includes only members which represent dates hours, minutes, and seconds are not included. dt includes one additional member which does not correspond to any member of the time structure. This member, dt_leap_year, is set to 1 when year is a leap year and set to 0 otherwise. This structure is available for the programmer's use and is also used extensively by the date functions themselves.
The Date Functions
The date functions correspond closely to the standard time/date functions. However, they do not necessarily mimic the standard functions in every last detail. I will discuss the date function first since it is a building block for other functions. date (Listing 2) corresponds to the time function in the standard library. date returns the current date as type date_t. date actually calls the standard time function to get the current date/time value; it then passes the date/time value to a global utility function, time_to_date. time_to_date (Listing 1) produces a date value which function date then returns. This value can be used for date math, or as an argument to other date functions, including the extended date functions.localdate (Listing 3) is one of the date functions that takes a date_t type as an argument. This function corresponds to the standard library's localtime function. localdate returns a pointer to an internal static structure of type struct dt, with members filled in to represent the date which was passed as an argument. This structure can also be used for date math, but it is more useful as an argument to other date functions. Since the extended date functions make heavy use of localdate you should make a copy of the returned structure value before calling any other functions.
Example:
struct dt dt; dt = *localdate ( &date_value );The mkdate (Listing 4) function is the inverse of the localdate function and corresponds to mktime in the standard library. mkdate converts a date structure back to a single date value of type date_t. mkdate uses the dt_year member, and attempts to use the dt_month and dt_mday members to calculate a date value. If the dt_month and dt_mday values are not available (or not valid), mkdate uses the dt_yday value to calculate the date. This behavior departs radically from mktime's. mkdate differs from mktime in another way: unlike mktime, mkdate does not fix member values in the date structure if it finds them to be incorrect. (I implemented this adjustment early on, but removed it for the sake of efficiency.) If the values in the date structure are unsuitable for calculating a date value, mkdate returns a value of (date_t)(-1) to indicate an error.Another function that accepts a date structure as an argument is the extremely useful strfdate (Listing 5) . This function corresponds to the strftime function in the standard library. You use strfdate to build regular text strings of date values. strfdate uses format specifiers in a manner similar to printf. For example, when you call strfdate, %A is replaced with the full name of a weekday, %B is replaced with the full name of the month, and %Y is replaced by a four-digit year. strfdate's format specifiers are exactly the same as strftime's, except that strfdate does not recognize specifiers for time values.
The Extended Date Functions
As powerful as the regular date functions are, it becomes very tedious work doing date math, building date strings, and parsing date strings. For this reason I've written three extra functions. The first two functions are complementary and deal with date strings. Date strings usually occur in a limited number of formats; Building a "wrapper" function for strfdate to handle common date formats greatly reduces the workload of setting up and calling strfdate. to_char is such a function. to_char (Listing 6) accepts the address of a character array where the formatted date is to be stored, a date value, and a value which indicates the desired format of the output date string. I've defined these in the dates.h header file since they correspond to date formats that I use frequently. The most common date format is represented by GREGDATE, which specifies the format MM/DD/CCYY, where MM is the month, DD is the day of month, and CCYY is the year. to_char returns the address of the character array where the date string is built. Thus, all you need to display the current date is:
puts( to_char( char_buffer, date(NULL), GREGDATE ) );Function to_date (Listing 6) is the complement of to_char. to_date takes a date string plus a format indicator and calculates a date (date_t) value. This function is useful for conversions on date strings from data files and from the keyboard. Furthermore, since to_date calls mkdate, it can also validate the date strings. Note: if the century in the date string is omitted, to_date uses the year value to determine the century. If the year is less than 80, to_date will consider the century to be 2000. If the year is greater than 79, to_date will consider the century to be 1900. I've included this feature to accommodate the century change which will occur in just a little over five years. Thus, to obtain a value for today's date you could use the following expression:
date_t date_val; date_val = to_date( "27-Nov-93", SPELLDATE );The final function in the extended suite is perhaps the most useful of all: compute_date (Listing 6) . You can use this function to add or subtract combinations of years, months, weeks, and days to a given date value. The arguments for this function are a starting date value, plus the number of years, months, weeks, and days to add or subtract. To demonstrate the power and flexibility of this function the following sample code obtains the current date, adds one year, subtracts three months, adds two weeks, and subtracts four days. It then displays the computed date.
start = date(NULL); finish = compute_date(start, 1, -3, 2, -4); puts(to_char(buffer, finish, SPELLDATE));Adding years, weeks, and days to a given date is relatively straightforward. However, date math using months is a bit complicated, because not all months contain the same number of days. For example, suppose the current date is the last day of May. If you wish to compute one month into the future, what day is it The 30th of June, or the 1st of July? I chose to enforce the former convention, so in this case the computed date would be the 30th of June. However, if the starting date were the 30th of June and one month were added the result would be 30th of July, not the 31st. Another problem arises when adding or subtracting years when the starting date is the 29th of February in a leap year. Is the resulting date the 28th of February or the 1st of March? Again, for consistency, I chose the former convention.
Miscellaneous Date Functions
Several functions do not figure so prominently as those mentioned already, but are worth mentioning. I've already mentioned the time_to_date function (Listing 1) in passing it converts the values returned from time and mktime in the standard library to a date value used by the extended functions. I've implemented this conversion so as to maintain portability. The first_day_of_month and last_day_of_month functions, (Listing 6) when given a date value, determine the month in which the date value falls and returns a date value for the first day or the last day of the month respectively. The next_day_of_week and the previous_day_of_week functions (Listing 6) are useful for determining either a future or past day of the week from a given date value. For example, in our speech we often say something like "The event is three weeks from the coming Saturday." To compute this with the extended date functions you would simply write:
date_v = next_day_of_week(date(NULL), SATURDAY) + 21;The dates.h header file defines values for each day of the week.
Compilation Notes
All the date functions I've described, as well as a few internal functions are implemented in a file called dates.c, available on this month's code disk. For convenience, dates.c was split into Listing 1 thru Listing 6 in this article. Listing 1 contains some #includes and declarations which appear at the beginning of dates.c. If you compile any of the date functions separately, be sure to include these statements in your source text.
Summary
The standard time/date functions are quite adequate for most date calculations, but their range is limited because their unit of measure is seconds. The date routines presented here overcome the limited range of the standard functions while maintaining their flexibility. I've presented three additional functions which remove much of the burden of formatting date strings, parsing date strings, and computing past or future dates. My new date functions align themselves closely with the Standard C date/time functions: Since I designed the three extended date functions to work with these new date functions, it should be easy to adapt the extended functions to work with the Standard C functions. One drawback to the date routines results from the unit of measure being in days: The date values lose the ability to serve as timestamps, However, in many cases, the increase in range more than compensates for the loss of precision.