Dates and Times in C

Every time I need to work with dates and times in C I find myself scrabbling around trying to refresh my memory on how the data structures and functions provided by time.h work. I have therefore put together a short program to demo the most important functionality, mainly as a reference for myself but I hope you find it useful as well.

Dealing with dates and times is a pain. The units are incredibly messy - not only do we have to deal with the various units being split up in different ways, we have to deal with leap years, daylight saving time, time zones and even the occasional leap second.

Fortunately every mainstream language or framework provides us with off-the-shelf functionality for handling dates and times but even these are not entirely straightforward. This is particularly true with C so I thought I would provide myself with a few bits of sample code which I can copy and paste whenever I need, and also share with anybody else who might find it useful.

time.h

The C standard library includes time.h which provides us with a struct and an integer type to represent dates and times. These are combined; there are no separate types for dates and times. This makes sense when you think about it, as the separation between the two is entirely arbitrary and has no real meaning.

These are the two types used to represent dates and times.

Types used to store a date and time
time_tThis is an integer type, usually long, which holds the datetime as the number of seconds before or after midnight on 1 Jan 1970.
tmA struct with nine members, all ints, to represent the various parts of a datetime.

Of course the two types show above both represent what is essentially the same information, albeit in very different forms. The standard library therefore provides us with functions for obtaining a variable of each type from the other.

Converting between time_t and tm
gmtimeCreates a tm struct from a time_t using GMT (Greenwich Mean Time)
localtimeCreates a tm struct from a time_t using local time
mktimeCreates a time_t variable from a tm struct

Finally let's look at a few functions to provide human-readable strings from time_t variables or tm structs.

Creating strings from date and time variables
asctimeCreates a string from a tm in a fixed "ddd mmm dd hh:mm:ss yyyy" format
ctimeCreates a string from a time_t in a fixed "ddd mmm dd hh:mm:ss yyyy" format
strftimeCreates a string from a tm according to the specified format.

The Project

For this project I will write three functions to illustrate the types and functions listed above.

  • Get the current time as a time_t, use it to create tm structs in both GMT and local time, then print out the members of the local time struct

  • Create and initialize a tm struct, then use mktime to both normalize the tm and get a time_t

  • Get the local time and print it in various formats using strftime

The project consists of just one file called datetime.c. You can download it as a zip or clone/download the Github repository if you prefer. This is the first part.

Source Code Links

ZIP File
GitHub

datetime.c part 1

#include<stdio.h>
#include<stdlib.h>
#include<time.h>

void current_time(void);
void create_time(void);
void time_to_string(void);


int main(int argc, char* argv[])
{
    puts("-------------------");
    puts("| codedrome.com   |");
    puts("| Dates and Times |");
    puts("-------------------\n");

    current_time();

    // create_time();

    // time_to_string();

    return EXIT_SUCCESS;
}


void current_time(void)
{
    // Get current time as time_t which
    // is an integer, usually long.
    // This is generally the number of
    // seconds from midnight on 1 Jan 1970.
    time_t t = time(NULL);

    // Get tm structs from time_t.
    // tm has members for seconds, minutes,
    // hours, day, month, year etc.
    struct tm* tm_gmt = gmtime(&t);
    struct tm* tm_local = localtime(&t);

    printf("Current GMT time is:\n%s\n", asctime(tm_gmt));

    printf("Current local time is:\n%s\n", asctime(tm_local));

    // Print the members of the local tm struct.
    // Note these are all of type int.
    puts("The members of the local tm struct are:");
    printf("tm_sec   %d\n", tm_local->tm_sec);
    printf("tm_min   %d\n", tm_local->tm_min);
    printf("tm_hour  %d\n", tm_local->tm_hour);
    printf("tm_mday  %d\n", tm_local->tm_mday);
    printf("tm_mon   %d\t0-based\n", tm_local->tm_mon);
    printf("tm_year  %d\tfrom 1900\n", tm_local->tm_year);
    printf("tm_wday  %d\t0-based from Sunday\n", tm_local->tm_wday);
    printf("tm_yday  %d\t0-based\n", tm_local->tm_yday);
    printf("tm_isdst %d\t-1 = not known, 0 = no, 1 = yes\n", tm_local->tm_isdst);
}

The main function merely calls the other three functions.

I don't usually include a lot of comments unless something unusual is going on which needs an explanation, but as this code is intended mainly as a reference I have included comprehensive descriptions. Therefore I won't include details of the code in the text.

When you have read the code and comments compile the program and run it:

Compile and Run

gcc datetime.c -std=c11 -lm -o datetime
./datetime

This is the output.

Program Output - current_time();

-------------------
| codedrome.com   |
| Dates and Times |
-------------------

Current GMT time is:
Wed Jul 24 12:41:00 2019

Current local time is:
Wed Jul 24 12:41:00 2019

The members of the local tm struct are:
tm_sec   0
tm_min   41
tm_hour  12
tm_mday  24
tm_mon   6 0-based
tm_year  119 from 1900
tm_wday  3 0-based from Sunday
tm_yday  204 0-based
tm_isdst 1 -1 = not known, 0 = no, 1 = yes

Note that tm_mon, tm_wday and tm_yday are zero-based, and that the year counts up or down from 1900.

Now let's look at create_time.

datetime.c part 2

void create_time(void)
{
    // Create and initialize a tm struct.
    struct tm st = {.tm_sec = 30,
                    .tm_min = 15,
                    .tm_hour = 9,
                    .tm_mday = 12,
                    .tm_mon = 7 - 1, // 0-based so 6 is July
                    .tm_year = 2019 - 1900, // 1900 + or -
                    .tm_isdst = 1};

    // Create a time_t from a tm struct.
    // This also sets tm_wday and tm_yday.
    time_t t = mktime(&st);

    printf("The value of the time_t variable is:\n%ld\n\n", t);

    printf("which represents:\n%s\n", ctime(&t));

    puts("The mkdir function set these members of the tm struct:");
    printf("tm_wday  %d\t0-based from Sunday\n", st.tm_wday);
    printf("tm_yday  %d\t0-based\n", st.tm_yday);
}

When creating a tm I have used 7 as the month but subtracted 1, and 2019 as the year but subtracted 1900. This emphasises that they start at 0 and 1900 respectively.

After printing out the value of the time_t variable both as a number and a human-readable string I have printed tm_wday and tm_yday to illustrate that the mktime function sets these.

The mktime function also adjusts any values which exceed their maximum. For example if you create a tm struct with a date of 31 June then mktime will change it to 1 July. For this reason it is important to set all six date/time members even if you aren't interested in them. If you do not they will contain garbage values which mktime will use to adjust the members which you did set. (You don't have to look far to find complaints about this "bug"!)

In main comment out current_time();, uncomment create_time(); and build and run again. This is the output.

Program Output - create_time();

-------------------
| codedrome.com   |
| Dates and Times |
-------------------

The value of the time_t variable is:
1562919330

which represents:
Fri Jul 12 09:15:30 2019

The mkdir function set these members of the tm struct:
tm_wday  5 0-based from Sunday
tm_yday  192 0-based

The fixed string format provided by asctime and ctime is OK for demonstration or perhaps writing to a log but isn't very flexible. The strftime function allows us to specify a format string so we can create a string with the various members of a tm struct in any order or format we want.

strftime takes the following arguments:

  • char* restrict n - a buffer to hold the formatted date

  • size_t n - the maximum number of characters to write

  • const char* restrict format - a string representing the format

  • const struct tm* restrict timeptr - the tm to create a string representation of

The format string can contain any number of specifiers consisting of the % sign followed by an upper case or lower case letter. I won't list all the valid letters but you might like to read this http://man7.org/linux/man-pages/man3/strftime.3.html.

Now take a look at the code which uses strftime on the current time with three different formats.

datetime.c part 3

void time_to_string(void)
{
    char datestring[64];

    time_t t = time(NULL);
    struct tm* tm_local = localtime(&t);

    // YYYY-MM-DD
    strftime(datestring, 64, "%Y-%m-%d", tm_local); // can use the %F shorthand
    puts(datestring);

    // Day, day of month, month, year in full
    strftime(datestring, 64, "%A %d %B %Y", tm_local);
    puts(datestring);

    // HH:MM:SS
    strftime(datestring, 64, "%H:%M:%S", tm_local); // can use the %T shorthand
    puts(datestring);
}

After getting a tm with the local time I have used strftime with three different format string examples. A few of the more common formats have single letter shortcuts, two of which I have mentioned in the comments.

In main comment out create_time();, uncomment time_to_string(); and build and run again. This is the output.

Program Output - time_to_string();

-------------------
| codedrome.com   |
| Dates and Times |
-------------------

2019-07-24
Wednesday 24 July 2019
12:58:13

As you can see strftime provides as much flexibility as you need. Not only do the various % + letter specifiers let you format the members of a tm in any way you need but you can also include other characters or text in the output.