Reputation: 11892
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
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.
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
.
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_point
s 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:
using namespace date;
date::sys_seconds
to std::chrono::sys_seconds
1900_y
to 1900y
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