Portable Custom Time Structures

Dr. Dobb's Sourcebook July/August 1997

Coding for a multiplatform environment

By Donald Bryson

Donald is the president of Quality Software Solutions and the creator of TimeClock. He can be contacted at http:// www.tclock.com/.

TimeClock, a commercially available employee attendance system I developed, automatically computes regular hours, overtime, sick time, vacation, and holidays. When I started developing TimeClock, I knew I would be releasing it for MS-DOS, XENIX, UNIX, and AIX. I have since ported to Linux. Consequently, I wanted the code to be as portable as possible. To achieve this, I initially planned on using FairCom's c-tree database engine, curses for screen I/O, and Standard C library functions for time calculations. I assumed all the ANSI time functions would be available on every system -- that's why they call it "ANSI." Unfortunately, I was in for a surprise.

I coded my initial prototype in Borland C++ 2.0, then developed a prototype on a XENIX system. That's when I discovered XENIX libraries did not have a mktime() function. mktime() takes a pointer to a date structure and returns a long, which is the number of seconds since January 1, 1970. The date structure is called tm and the long is time_t. Users enter time in hours and minutes; and dates in years, months, and days. Using these one-dimensional variables, computers simply perform calculations. Clearly, mktime() (or something like it) is the most important function in the software. Thus, it became equally clear that I was going to have to write my own.

I started by examining the "standard" tm structure in each of the operating systems I intended to support. Unfortunately, "standard" is relative. The tm structure generally differs from one environment to another. The first part of the structure is the same in all environments -- from tm_sec to tm_yday. However, beginning with tm_ isdst (a flag indicating daylight saving time), some are different. The older XENIX C library uses any nonzero value to indicate the current time is daylight saving time. Both positive and negative numbers indicate daylight saving time in XENIX. However, in newer C/C++ libraries, only a positive number indicates daylight saving time. A negative number indicates that you do not know if it is daylight saving time. After tm_isdst, even the actual fields are different between environments. Microsoft C++ 1.5 for MS-DOS, for instance, does not have any elements after tm_isdst. Most of the UNIX libraries (with the exception of AIX) have tm_tzadj and tm_name after tm_isdt. The tm_tzadj is the amount of adjustment needed to convert local time to Universal Coordinated Time (abbreviated UTC, due to its French origins), also called Greenwich Mean Time (GMT); and the tm_name is the name of the time zone. The GNU C++ libraries have similar fields, but they are called __tm_ gmtoff__ and __tm_zone__.

Custom Data Structures: Advantages and Disadvantages

The upside of having to write your own data structure is that the process gives you the opportunity to make the program more portable. I started by removing nearly all references to the tm structure from my program. (Storing tm or any other structure in the database was straightforward because c-tree allows any type of information to be stored in a fixed-length file.)

For instance, compare the replacement structure, tt, with tm (see Listings One and Two). The tm_mon field in tm starts with 0 through 11. This requires converting input from users every time they enter a month. The program must do the reverse when printing or displaying the month. The month field in tt starts with 1 and continues through 12; conversion is not necessary. The same holds true for day-of-the-year fields of the two structures (yearday in tt and tm_yday in tm). Again, conversion is not necessary for inputting, displaying, and printing. The biggest difference is the year field. The tm_year field in tm stores only the last two digits of the year, while year field in tt stores the complete year. (The code executes correctly for the year 2000 and beyond.)

TimeClock does not need the additional time fields included in some of the libraries, so I excluded them and made the structure smaller. It has no need for the name of the time zone or the flag for daylight saving time. I handle daylight saving dynamically in the code and none of my customer installations split time zones. Reduced size is usually a small benefit in disk storage, but it is a big benefit when loading a large array of structures into memory for sorting.

Don't capriciously exclude elements from a structure. You should carefully review your design document before finalizing the format of your structures. Make sure you are not removing essential elements. For example, my first pass at the tt structure did not include the weekday field. When I looked at the employee scheduling module, it depended on the current weekday to lookup the employee's schedule for that day. Schedules are stored in a structure called WeekSchedule (see Listing Two) that contains an array of structures called DaySchedule. Each day of the week is one of the seven elements in the array. The program uses the weekday element of the tm structure as a vector into the correct element of the array. While I could dynamically calculate the day of the week, that would unnecessarily consume CPU time. It makes more sense to store the day of the week in the tt data structure. This costs an extra byte per record but it saves CPU cycles every time the program looks at an employee schedule.

Fortunately, adding elements was not a problem during the initial design/code/port phase. However, expanding a structure in a program with an installed user base can be inconvenient -- especially if your application requires a large amount of disk space for its data files. You can run out of disk space during an upgrade that expands the structure, resulting in unhappy customers. Try to include all the elements you currently need and try to anticipate elements you might need in the future.

Standard C Time Functions and Variables

The ANSI C function time() (see Listing One) retrieves the system time, storing it in a variable of type time_t. This variable, usually a long integer, is the number of seconds from midnight January 1, 1970 to the current time. The magic time -- midnight on January 1, 1970 -- is called the "epoch date." The system adjusts for your time zone into UTC. The information to perform this adjustment is contained in the TZ environment variable. TZ usually includes: daylight, a flag to indicate that daylight saving time is in effect; timezone, the number of seconds to adjust between UTC and local time; and tzname, one or two three-letter abbreviations for your local time zone. Also, you will need to execute _tzset() to make the environment variables available to your program if you use them on an MS-DOS system. (Incidentally, an MS-DOS system will always report the time zone in Redmond, Washington, if TZ is not actually set; see Listing Three for syntax.)

There are basically two functions that create a tm structure from the time_t variable. The gmtime() function populates a tm structure that represents time_t in GMT. The function, localtime(), populates a tm structure that represents time_t adjusted for the local time zone. Both of these functions use a static buffer to populate the same tm structure. Each call invalidates any previous results from the functions (see Listing One). If you need both, copy the global buffer to your own buffers. Calculating time difference is simple, provided both times are converted to UTC. Simply subtract the earlier time from the later time. Always convert to UTC before performing any time difference calculations. Given two local times in different time zones, your results will be off by the time zone adjustment factor unless you convert both of them to UTC. (Also, calculating between two local times that split the daylight saving time boundary will be off by one hour.)

While UTC is needed for calculations, it is not a good idea to store UTC in your database. Users don't care what time it is in Greenwich, they want the current time at their terminal. If you store time in UTC, then your program must convert between local and UTC every time it reads/writes a record to disk. I store the time in a database as local time, but convert to UTC when performing math calculations.

The New Time Functions

Listing Two presents the new time functions. ItIsLeapYear() determines if a given year is a leap year. ItIsLeapYear() is based on the well-known fact that a year is always a leap year if it is divisible by 400. It is also a leap year if it is divisible by 4 and not divisible by 100. Its return value is used as an offset into the daytable array and is also used in the function days_yr().

The days_yr() function returns the number of days per year given the year. If it is a leap year, it returns 366. Otherwise, it returns 365. The tt_Convert() function is one of the two main conversion functions. Its one parameter is a pointer to properly populated tt structure. It returns a time_t that is normalized for the time zone. (However, TimeClock does not normalize in the MS-DOS environment because DOS users normally do not properly set the TZ environment variable.) For illustration purposes, I left the time zone adjustments in the function (see Listing Three). First, the function checks the range of each field in the given tt structure. It returns -1 if any of the ranges are invalid. The next thing it does is tally the number of days in each year since 1970. It adds this total to the number of days in each month of the given year. Notice that it makes corrections for a leap year by using an offset into the variable daytable. It then adds the number of days in the given month to the total. After the function determines the number of days since the epoch, it multiplies that number by the number of seconds in a day and stores it in the variable EpochDate. It then multiplies the number of seconds in the given hours and adds that to EpochDate. It then adds the number of seconds in the given minutes to the EpochDate.

Finally, the function makes the correction for daylight saving time and time zone, using the global variables timezone and daylight. If daylight is True, then it subtracts an hour from EpochDate. It adds the timezone value to EpochDate. The other main conversion function, Timet_to_tt, populates a tt structure from a time_t long. It populates a tm structure with localtime(). It then parses out those fields into the fields of tt. Do you see the bug in my program? All the dates after 2070 and before 2170 will be exactly 100 years off. While I could have fixed this, it would require extra steps in the function. This function executes many times in the program and it was not worth the computer resources to add those steps. Also, the mktime() in Microsoft C++ 1.52 is scheduled to break in February 5, 2036. Mine is good for an extra 34 years. Besides, no one will be using this code close to either of those two breaking dates.

DDJ

Listing One

/* Example of time functions and structures */#include <stdio.h>
#include <time.h>

void main( void ) { struct tm *UTCtime; struct tm *LocalTime; long ltime; time( &ltime ); /* _tzset() sets the following global variables that have been set in the environment: (1) _daylight Nonzero value if a daylight-saving-time zone is specified in the TZ setting; otherwise, 0 (2) _timezone Difference in seconds between UTC and local time (3) _tzname[0] String value of the three-letter time-zone name from the TZ environmental variable (4) _tzname[1] String value of the daylight-saving-time zone, or an empty string if the daylight-saving-time zone is omitted from the TZ environmental variable */ _tzset(); printf("Daylight Savings Flag: %d\n", _daylight); printf("Offset in seconds from UTC and local time: %d\n", _timezone); printf("Regular Time Zone: %s\n", (char *)_tzname[0]); printf("Daylight Savings Time Zone: %s\n", (char *)_tzname[1]); /* An example of incorrectly populating two tm structs */ /* Both gmtime() and locatime() use the same global static buffer */ /* for conversion. Calling the two functions and then trying */ /* to use the two pointers will result in an incorrect answer. */ /* The buffer contains UTC time after gmtime() is called, but */ /* it contains local time after locatime() is called. */ UTCtime = gmtime( &ltime ); LocalTime = localtime( &ltime ); /* a \n is not needed in the following printf statement. It is added to the string by asctime() */ printf( "Incorrect Universal Coordinated Time: %s", asctime(UTCtime) ); printf( "Correct Local Time: %s", asctime(LocalTime) );

/* This is one way of getting the both functions together */ UTCtime = gmtime( &ltime ); /* Note: we use the tm we got */ printf( "Universal Coordinated Time is %s", asctime(UTCtime) );

/* Now we get the other tm */ LocalTime = localtime( &ltime ); printf( "Local Time is %s", asctime(UTCtime) ); /* Note: You can also copy the global buffer to your own buffers with with each call. Use that method if you need to compare different tm structures. */

return; }

Back to Article

Listing Two

/* Example of custom time structures and functions */#include <stdio.h>
#include <time.h>

/* ****************************************************************** */ /* This table is used to determine the number of days in the month given the month and the flag for leap year as determined by ItIsLeapYear()*/ static unsigned char daytable[2][13] = { {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} }; /* ****************************************************************** */ /* The Custom time structure tt */ typedef struct { unsigned int year; /* year stored as a full year, i.e., 19XX */ unsigned char month; /* month - Jan=1, Dec=12*/ unsigned char day; /* day 1-31 */ unsigned char hour; /* hour 0-59*/ unsigned char minute; /* minute 0-59 */ unsigned weekday; /* Day of week 0=Sunday, 6=Saturday */ int yearday; /* Day of year Jan 1 = 1 */ } tt; /* The following tm structure is only included for comparison */ #ifdef ONLYFORDISPLAY struct tm { /* Standard Section of the tm structure */ int tm_sec; /* Seconds - [0,59] */ int tm_min; /* Minutes - [0,59] */ int tm_hour; /* Hours - [0,23] */ int tm_mday; /* Day of the month - [1,31] */ int tm_mon; /* Months - [0,11] */ int tm_year; /* Years since 1900 */ int tm_wday; /* Days of week - [0,6] */ int tm_yday; /* Days since January 1 - [0,365] */ int tm_isdst; /* Daylight savings time flag */ }; #endif /* ****************************************************************** */ /* Employee Schedule Structures */ typedef struct { char In1hr; /* Hour of 1st Clock-in */ char In1mn; /* Minute of 1st Clock-in */ char Out1hr; /* Hour of 1st Clock-out */ char Out1mn; /* Minute of 1st Clock-out */ char In2hr; /* Hour of 2nd Clock-in */ char In2mn; /* Minute of 2nd Clock-in */ char Out2hr; /* Hour of 2nd Clock-out */ char Out2mn; /* Minute of 2nd Clock-out */ } DaySchedule;

typedef struct { char StatusFlag; long ScheduleNum; char ScheduleName[21]; DaySchedule WorkDay[7]; } WeekSchedule; /* ****************************************************************** */ /* Determine if the given year is a leap year */ /* The year parameter must be a full year i.e. 1997 */ int ItIsLeapYear(int year) { if ( ( year % 4 == 0 ) && ( year % 100 != 0 ) || ( year % 400 == 0) ) return 1; else return 0; } /* ****************************************************************** */ /* Determine the number of days per year */ unsigned int days_yr(int year) { if (ItIsLeapYear(year)) return 366; else return 365; } /* ****************************************************************** */ /* Convert from the custom time structure, tt, into the standard time_t variable */ time_t tt_Convert(tt *ttPtr) { int unsigned t_year; int month; int leap; /* Flag to indicate if leap year is in effect */ time_t DaysInPartial; /* Number of Days in the Partial Year */ time_t EpochDate; struct tm *tmPtr; EpochDate = 0L; DaysInPartial = 0L; month = 0;

/* Check the validity of the tt parameter */ leap = ItIsLeapYear(ttPtr->year); if ( ttPtr->yearday == -1 ) { return -1L; } if ( ttPtr->month > 12 ) { return -1L; } /* Use leap year in case current year is a leap year */ if ( ttPtr->day > daytable[leap][ttPtr->month] ) { return -1L; } if ( ttPtr->hour > 24 ) { return -1L; } if ( ttPtr->minute > 60 ) { return -1L; } /* Set the temporary year to the epoch year */ t_year = 1970; /* Calculate the number of days in each full year since 1970 */ while ( t_year < ttPtr->year ) { DaysInPartial += (long)days_yr(t_year); ++t_year; } /* Calculate the number of days in each month in current year */ while ( month < ttPtr->month ) { DaysInPartial += daytable[leap][month]; month++; } /* Add the number of full days this month */ DaysInPartial += (long)(ttPtr->day - 1); /* Now we know the number of days since Jan. 1, 1970 */ /* Convert the number of days to seconds */ EpochDate = (time_t)((time_t)DaysInPartial * (time_t)( 60L * 60L *24L) ); /* Convert the hours to seconds and add to total */ EpochDate += (time_t)((time_t)ttPtr->hour * 60L * 60L); /* Convert the minutes to seconds and add to total */ EpochDate += (time_t)((time_t)ttPtr->minute * 60L); /* Determine if Daylight Savings Time is in Effect */ /* And adjust for the time zone and daylight savings time */ /* Note: I included these lines only in the *NIX versions, but I am leaving it here for demonstration purposes only for MS-DOS programmers. */ tmPtr = localtime(&EpochDate);

if ( tmPtr->tm_isdst > 0 ) { EpochDate -= ( 60L * 60L ); } EpochDate += timezone; /* Like the standard mktime() normalize the year day and week day. */ tmPtr = localtime(&EpochDate); ttPtr->yearday = tmPtr->tm_yday + 1; ttPtr->weekday = tmPtr->tm_wday;

return EpochDate; } /* convert from a time_t variable to a tt variable */ tt *Timet_to_tt(time_t tlong, tt *ttPtr) { struct tm *t; t = localtime(&tlong); /* Code will break in 2070 */ if ( t->tm_year > 70 ) { ttPtr->year = t->tm_year + 1900; } else { ttPtr->year = t->tm_year + 2000; } ttPtr->month = t->tm_mon + 1; ttPtr->day = t->tm_mday; ttPtr->hour = t->tm_hour; ttPtr->minute = t->tm_min; ttPtr->yearday = t->tm_yday + 1; ttPtr->weekday = t->tm_wday; return ttPtr; } void main(void) { tt OurNewStruct; struct tm *UTCtime; struct tm *LocalTime; long ltime;

/* Set the global time zone variables */ _tzset();

time( &ltime ); printf("The time_t for the current time: %ld\n", ltime); /* Obtain Both local time and UTC */ UTCtime = gmtime( &ltime ); printf( "Universal Coordinated Time is %s", asctime(UTCtime) ); LocalTime = localtime( &ltime ); printf( "Local Time is using tm %s", asctime(UTCtime) );

/* Populate the custom structure tt from time_t */ Timet_to_tt(ltime, &OurNewStruct); printf("tt converted from current time_t: %d/%d/%d %d:%d\n", OurNewStruct.month, OurNewStruct.day, OurNewStruct.year, OurNewStruct.hour, OurNewStruct.minute);

/* Convert from tt to time_t */ ltime = tt_Convert(&OurNewStruct); LocalTime = localtime(&ltime); printf("The time_t variable calculated tt: %ld\n", ltime); printf("Local Time from tm calculated from time_t calculated from tt: %s", asctime(LocalTime) ); }

Back to Article

Listing Three

REM MS-DOS BATCH for playing with Listing 1 and Listing 2rem The syntax to setting the TZ environment variable is
rem set TZ=tzn[+ | -]hh[:mm[:ss] ][dzn]
set TZ=EDT4EST
LISTING1
LISTING2

Back to Article