ANSI C and subtracting or adding time (hours, etc.) part 2

In my previous article, I rambled a bit about how I first learned to program in C in the late 1980s under the OS-9 operating system on a Radio Shack Color Computer 3. I mentioned this to point out that, even after 25 years, there were still things about C I had never used.

One of those things involves doing time math (adding or subtracting time). If you take a look at the various C related time functions (see time.h):

http://www.cplusplus.com/reference/ctime/

…you will see that the C library has two ways of representing time. One is a time_t value which is some value (but not defined by the standard as to what that value is), and a struct tm structure, which contains fields for things like hour, minute, second, day, month, year, etc. Some time functions work with time_t values, and some work with struct tm structures.

There are four main time functions. You can get the amount of processor time used by a program with clock(). You can get the current time using time() (as a time_t value of some sort). You can get the difference between two times using difftime(). And you can make a time using mktime().

The other time.h functions are conversion utilities: asctime() returns a string representing the current date/time (from a struct tm). ctime() returns a similar string but works on a time_t value. gmtime() converts a time_t value to a struct tm and adjusts the resulting to to be GMT (universal time zone, Greenwich Mean Time). localtime() is like gmtime but it returns a struct tm in local timezone. And lastly, strftime() is like a printf for time. It lets you create a custom string representation of date and time from a struct tm. This is useful if the asctime() and ctime() do not return the format you need.

The current project I am working on deals with event tickets, and the system needs to know when a ticket is valid. By default, a ticket is valid on the day it is activated and then it shuts off. The problem was that a ticket would stop working after midnight, so I needed to implement a grace period. I wanted to define a ticket good for “Today” (or “Friday” or “5/4/2015”) and have it know that even after midnight (when the day became Tomorrow, Saturday or 5/5/2015) it would still be accepted for a certain amount of time.

Almost all examples of handling time I could find relied on knowing something about what the time_t number was. If you *knew* that time_t was “number of seconds since January 1 1970”, all you would have to do is add or subtract a certain amount of seconds from that value and you would be done.

But, according to the ANSI C standard, time_t is implementation specific. If you really want to write portable ANSI C code, you can’t assume anything about time_t other than it being some number.

My program is currently being built for Windows, Mac and Raspberry Pi. All three of these systems seem to handle time_ t the same way, but what if my code gets ported later to some embedded operating system that did it some other way?

The good news is that it’s really not much work to do things the “proper” way, though I certainly understand the lazy programmer mentality of “if it works for me, ship it!”

Here is what I learned and what prompted me to write this article: you can create struct tm values with invalid values and the C time library functions can normalize them.

Here is an example… If you want to create a time of 2:30 a.m., the struct tm values would look like this:

tm_hour = 2;
tm_min = 30;
tm_min = 0;

If all the tm values are properly formatted, you can pass them in to functions like asctime() and they work.

BUT, you can also represent 2:30 a.m. as “150 seconds after midnight” like this:

tm_hour = 0;
tm_min = 0;
tm_sec = 150;

This tm structure appears to be invalid since minutes is listed as being 0-59 in the references I looked at. Because of this, it never dawned on me I might be able to pass in a value other than 0-59.

If you pass this invalid struct tm in to asctime(), it will fail. However, if you pass it in to mktime(), it will normalize the values (adjusting them to the proper hour, minute and second values) and return that as a time_t time. Interesting.

It seemed I might be able to add time simply by adding a number of seconds or hours. 2 hours in the future might be as simple as:

tm_hour = time_hour + 2;

…then I would use mktime() to get an adjust time_t that now represents the time 2 hours in the future.

A few quick tests showed that this did work. Unfortunately, the way I was approaching my ticket expiration task required me to look 2 hours in the past. It didn’t seem possible, since that would mean using negative numbers and surely that wouldn’t work.

Or would it?

I had noticed that the tm_xxx variables were “int” values rather than “unsigned int”. Why? If values are 0-59 (minutes) or 0-23 (hour) or 0-365 (days since January 1), why would it ever need to be a signed negative value? But since a value could clearly be greater than 59 for seconds, perhaps negative values worked as well and that’s why they were “ints”.

Indeed, this is the case. I had never known you could do something like this:

tm_hour = tm_hour - 2;

By doing that, then converting it using mktime() in to a time_t, you end up with a time_t representation of two hours in the past.

Simple, and portable, and “proper.” Even if it looks strange.

The only issue with doing it this way is you need a few more steps. In my case, I had a time_t value that represented when a ticket was activated. It began life as something like this:

time_t activationTime = time(NULL);

When I would be doing my time checks, I would need to know the current time:

time_t currentTime = time(NULL);

And since I was not concerned with the time of day of the activation, just the actual day (month/day/year), I would need to convert each of these in to tm structures so I could look at those values:

struct tm *activationTmPtr;
int activationMonth, activationDay activationYear;
activationTmPtr = localtime(&activationTime);
activationMonth = activationTmPtr->tm_mon;
activationDay = activationTmPtr->tm_mday;
activationYear = activationTmPtr->tm_year;

NOTE: I am copying the values in to my own local variables because localtime(), gmtime() and other calls return a pointer to static data contained inside those functions. If I were to do something like this:

struct tm *activationTmPtr, *currentTmPtr;
activationTmPtr = localtime(&activationTime);
currentTmPtr = localtime(&currentTime);

…that might look proper, but each time localtime() is called, it handles the conversion and returns a pointer to the static memory inside the function. Every call to localtime() is returning the same pointer, so each call to localtime() updates that static memory.

activationTmPtr and currentTmPtr would both be the same address, and would both point to whatever the last localtime() conversion was.

Easy mistake to make, and one of the reasons returning pointers to static data is problematic. The caller has to understand this, and make copies of any data it wishes to keep. (Yeah, this is something I learned the hard way.)

With this in mind, I could get the parts of the local time the same way:

struct tm *currentTmPtr;
int currentMonth, currentDay currentYear;
currentTmPtr = localtime(&currentTime);
currentMonth = currentTmPtr->tm_mon;
currentDay = currentTmPtr->tm_mday;
currentYear = currentTmPtr->tm_year;

Now to see if Activation Day was the same as Today, I could just compare:

if ((currentDay==activationDay) && (currentMonth==activationMonth) && (currentYear==activationYear))

Simple. Though in my application, the ticket could also specify a day-of-week, so there could be a weekend pass active only on “Saturday” and “Sunday”, or a pass good for only “Thursday”. I would do this the same way, but I would use the tm_wday variable (day of week, 0-6).

To deal with a grace period, my logic looked like this:

  • Check current day (either month/day/year or day of week) against target valid day (again, either a month/day/year value, or a specific day of week).
  • If invalid, try the comparison again, but this time have “current day” be X hours earlier. If X was 2 hours, then I could test at 1:30 a.m. and it would be comparing as if the time was still 11:30 p.m. the previous day, and it would pass.

Easy peasy.

To do this, I simply created a special graceTime and graceTmPtr like this:

// Start with current time again and convert to a struct tm we can do math on.
graceTmPtr = localtime(&currentTime);
// Adjust the values to be 2 hours earlier.
gracetTmPtr->tm_hour = graceTmPtr->tm_hour - 2;
// We need to normalize this so we can get the real Month/Year/Day.
graceTime = mktime(graceTmPtr);
// And now we need it back as a struct tm so we can get to those elements.
graceTmPtr = localtime(&graceTime);
// And now we can get to those values.
graceMonth = graceTmPtr->tm_mon;
graceDay = graceTmPtr->tm_mday;
graceYear = graceTmPtr->tm_year;

After this, I could do the same check:

if ((graceDay==activationDay) && (graceMonth==activationMonth) && (graceYear==activationYear))

Problem solved.

Maybe this will help someone else. I know I spent far too much time searching for how to do this before I stumbled on some old post somewhere that mentioned this.

Have fun!

Leave a Reply