steveo225
steveo225

Reputation: 11892

Convert UTC time_t to UTC tm

All my internal times are UTC stored in time_t. I need to convert them to struct tm. If I use localtime the time is correct, except that tm_isdst may be set resulting in the time being off an hour. If I use gmtime it gets the wrong time, off by the time zone difference.

Edit I am looking for a cross platform solution that works in Windows and Linux

Upvotes: 1

Views: 1672

Answers (1)

Howard Hinnant
Howard Hinnant

Reputation: 219488

Here is a cross platform solution that requires C++11 or better, and a free, open-source, header-only date library. And when your vendor brings you C++20, you can loose the date library as it is incorporated into C++20 <chrono>.

It is actually easier to convert from time_t to a UTC tm by going through <chrono> than it is to use the C API. There do exist various extensions to do this on each platform, but the extensions have different syntaxes. This solution has a uniform syntax across all platforms.

In C++11, though not specified, it is a de-facto standard that both time_t and std::chrono::system_clock track Unix Time, though at different precisions. In C++20 this becomes specified for std::chrono::system_clock. For time_t the de-facto precision is seconds. One can take advantage of this knowledge to create extremely efficient conversions between the C API and the C++ <chrono> API.

Step 1: Convert time_t to a chrono::time_point

This is very easy and efficient:

date::sys_seconds
to_chrono(std::time_t t)
{
    using namespace date;
    using namespace std::chrono;

    return sys_seconds{seconds{t}};
}

date::sys_seconds is simply a type alias for:

std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>

I.e. a time_point based on system_clock but with seconds precision.

All this function does is change type from time_t to seconds and then to time_point. No actual computation is done. Here is an optimized clang compilation of to_chrono:

    .globl  __Z9to_chronol          ## -- Begin function _Z9to_chronol
    .p2align    4, 0x90
__Z9to_chronol:                         ## @_Z9to_chronol
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    movq    %rdi, %rax
    popq    %rbp
    retq
    .cfi_endproc

All that's there is the boiler plate for a function call. And if you inline this, even that disappears.

Furthermore this function will port to C++20 by simply removing using namespace date and changing date::sys_seconds to std::chrono::sys_seconds.

Step 2: Convert sys_seconds to a tm

This is where the computation happens:

std::tm
to_tm(date::sys_seconds tp)
{
    using namespace date;
    using namespace std::chrono;

    auto td = floor<days>(tp);
    year_month_day ymd = td;
    hh_mm_ss<seconds> tod{tp - td};  // <seconds> can be omitted in C++17
    tm t{};
    t.tm_sec  = tod.seconds().count();
    t.tm_min  = tod.minutes().count();
    t.tm_hour = tod.hours().count();
    t.tm_mday = unsigned{ymd.day()};
    t.tm_mon  = (ymd.month() - January).count();
    t.tm_year = (ymd.year() - 1900_y).count();
    t.tm_wday = weekday{td}.c_encoding();
    t.tm_yday = (td - sys_days{ymd.year()/January/1}).count();
    t.tm_isdst = 0;
    return t;
}

All of the computation happens in the first three lines:

auto td = floor<days>(tp);
year_month_day ymd = td;
hh_mm_ss<seconds> tod{tp - td};  // <seconds> can be omitted in C++17

Then the rest of the function just extracts the fields to fill out the tm members.

auto td = floor<days>(tp);

The first line above simply truncates the precision of the time_point from seconds to days, rounding down towards negative infinity (even for time_points prior to the 1970-01-01 epoch). This is little more than a divide by 86400.

year_month_day ymd = td;

The second line above takes the count of days since the epoch and converts it to a {year, month, day} data structure. This is where most of the computation happens.

hh_mm_ss<seconds> tod{tp - td};  // <seconds> can be omitted in C++17

The third line above subtracts the days-precision time_point from the seconds-precision time_point resulting in a std::chrono::seconds time duration since midnight UTC. This duration is then broken out into a {hours, minutes, seconds} data structure (the type hh_mm_ss). In C++17 this line can optionally be simplified to:

hh_mm_ss tod{tp - td};  // <seconds> can be omitted in C++17

Now to_tm simply extracts the fields to fill out the tm according to the C API.

int   tm_sec;        //   seconds after the minute -- [0, 60]
int   tm_min;        //   minutes after the hour -- [0, 59]
int   tm_hour;       //   hours since midnight -- [0, 23]
int   tm_mday;       //   day of the month -- [1, 31]
int   tm_mon;        //   months since January -- [0, 11]
int   tm_year;       //   years since 1900

int tm_wday; // days since Sunday -- [0, 6]
int tm_yday; // days since January 1 -- [0, 365]
int tm_isdst; // Daylight Saving Time flag

It is important to first zero-initialize the tm because different platforms have extra tm data members as extensions that are best given the value 0.

tm t{};

For the hours, minutes and seconds one simply extracts the appropriate chrono::duration from tod and then extracts the integral values with the .count() member function:

t.tm_sec  = tod.seconds().count();
t.tm_min  = tod.minutes().count();
t.tm_hour = tod.hours().count();

day has an explicit conversion to unsigned and this is one of the few places where the C API doesn't give a tm data member an unexpected bias:

t.tm_mday = unsigned{ymd.day()};

tm_mon is defined as "months since January" so that bias has to be taken into account. One can subtract January from the month, resulting in a months duration. This is a chrono::duration, and the integral value can be extracted with the .count() member function:

t.tm_mon  = (ymd.month() - January).count();

Similarly, tm_year is years since 1900:

t.tm_year = (ymd.year() - 1900_y).count();

One can convert a days-precision time_point (td) to a weekday with conversion syntax, and then weekday has a member function .c_encoding() to extract an integral value which matches the C API: days since Sunday -- [0, 6]. Alternatively there is also a .iso_encoding() member function if one desires the ISO encoding [Mon, Sun] -> [1, 7].

t.tm_wday = weekday{td}.c_encoding();

tm_yday is days since January 1 -- [0, 365]. This is easily computed by subtracting the first of the year from the days-precision time_point (td), creating a days chrono::duration:

t.tm_yday = (td - sys_days{ymd.year()/January/1}).count();

Finally tm_isdst should be set to 0 to indicate Daylight Saving Time is not in effect. Technically this step was already done when zero-initializing tm, but is repeated here for readability purposes:

t.tm_isdst = 0;

to_tm can be ported to C++20 by:

  • remove using namespace date;
  • change date::sys_seconds to std::chrono::sys_seconds
  • change 1900_y to 1900y

Example Use:

Given a time_t, here is how you can use these functions to convert it to a UTC tm:

std::time_t t = std::time(nullptr);
std::tm tm = to_tm(to_chrono(t));

Here are the necessary headers:

#include "date/date.h"
#include <chrono>
#include <ctime>

Or in C++20, just:

#include <chrono>
#include <ctime>

Upvotes: 2

Related Questions