How to Convert a Struct Tm (Expressed in Utc) to Time_T Type

Easy way to convert a struct tm (expressed in UTC) to time_t type

Use timegm() instead of mktime()

How to convert UTC date to time_t

Here's how I'm going about it:

  1. Pretending against better knowledge the tm structure holds local time (non-DST if anyone asks; it doesn't matter, but must be consistent with step 3), convert it to time_t.
  2. Convert the date back into a tm structure, but this time in UTC representation.
  3. Pretending against better knowledge that tm structure to also hold local (non-DST if anyone asks, but more importantly consistent with step 1), and convert it to time_t once more.
  4. From the two time_t results I can now compute the difference between local time (non-DST if anyone asks) and UTC in time_t units.
  5. Adding that difference to the first time_t result gives me the proper time in UTC.

Convoluted, but I'm confident it should work. Unless the computer uses a TAI-based clock, in which case on rare occasions the result might be off by a second. The consistent DST setting (as opposed to having mktime auto-detect it) is necessary so that I don't get thrown off by an entire hour when trying to compute a date/time close to the start or end of the DST period.

tm tt;
// populate tt here
tt.tm_isdst = 0;
time_t tLoc = mktime(&tt);
tt = *gmtime(&tLoc);
tt.tm_isdst = 0;
time_t tRev = mktime(&tt);
time_t tDiff = tLoc - tRev;
time_t tUTC = tLoc + tDiff;

However, this approach comes with the caveat that gmtime() is not thread-safe. So if anyone has any better solution, I'd love to hear it.

How to convert a UTC date & time to a time_t in C++?

I have decided to implement my own version of mkgmtime and it was easier than I thought.

const int SecondsPerMinute = 60;
const int SecondsPerHour = 3600;
const int SecondsPerDay = 86400;
const int DaysOfMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

bool IsLeapYear(short year)
{
if (year % 4 != 0) return false;
if (year % 100 != 0) return true;
return (year % 400) == 0;
}

time_t mkgmtime(short year, short month, short day, short hour, short minute, short second)
{
time_t secs = 0;
for (short y = 1970; y < year; ++y)
secs += (IsLeapYear(y)? 366: 365) * SecondsPerDay;
for (short m = 1; m < month; ++m) {
secs += DaysOfMonth[m - 1] * SecondsPerDay;
if (m == 2 && IsLeapYear(year)) secs += SecondsPerDay;
}
secs += (day - 1) * SecondsPerDay;
secs += hour * SecondsPerHour;
secs += minute * SecondsPerMinute;
secs += second;
return secs;
}

My main concern was that mkgmtime must be consistent with gmtime. Such that gmtime(mktime(t)) returns the original input values. Therefore I have compared the results for all multiples of 61 between 0 and MAX_INT for time_t and they are indeed equal (at least on my system). Therefore the above routine is correct.

This outcome also means that the C library does not take leap seconds into account, which is a bad thing in itself but good for my purpose. The two functions will stay consistent for a long time. To be absolutely sure, my Timestamp class that uses this function always performs a quick check on program start and proves the consistency for a couple of meaningful values.

Convert UTC time_t to UTC tm

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>

Convert date and time numbers to time_t AND specify the timezone

It makes me want to throw up in my mouth a little bit, but you could convert it to a string with strftime(), replace the timezone in the string and then convert it back with strptime() and into a time_t with mktime(). In detail:

#ifdef UGLY_HACK_VOIDS_WARRANTY
time_t convert_time(const struct tm* tm)
{
const size_t BUF_SIZE=256;
char buffer[BUF_SIZE];
strftime(buffer,256,"%F %H:%M:%S %z", tm);
strncpy(&buffer[20], "+0001", 5); // +0001 is the time-zone offset from UTC in hours
struct tm newtime = {0};
strptime(buffer, "%F %H:%M:%S %z", &newtime);
return mktime(&newtime);
}
#endif

However, I would highly recommend you convince the powers that be that boost is an option after all. Boost has great support for custom timezones. There are other libraries that do this elegantly as well.

Does struct tm store time zone information as its data member

The C standard says in 7.27.1 Components of time:

The tm structure shall contain at least the following members, in
any order. The semantics of the members and their normal ranges are
expressed in the comments.318)

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

(emphasis is mine)

That is, implementations are allowed to add additional members to tm, as you found with glibc/time/bits/types/struct_tm.h. The POSIX spec has nearly identical wording.

The result is that %Z (or even %z) can not be considered portable in strftime. The spec for %Z reflects this:

%Z is replaced by the locale’s time zone name or abbreviation, or
by no characters if no time zone is determinable.
[tm_isdst]

That is, vendors are allowed to throw up their hands and simply say: "no time zone was determinable, so I'm not outputting any characters at all."

My opinion: The C timing API is a mess.


I am attempting to improve things for the upcoming C++20 standard within the <chrono> library.

The C++20 spec changes this from "no characters" to an exception being thrown if the time_zone abbreviation is not available:

http://eel.is/c++draft/time.format#3

Unless explicitly requested, the result of formatting a chrono type
does not contain time zone abbreviation and time zone offset
information. If the information is available, the conversion
specifiers %Z and %z will format this information (respectively).
[ Note: If the information is not available and a %Z or %z
conversion specifier appears in the chrono-format-spec, an exception
of type format_­error is thrown, as described above. — end note ]

Except that the above paragraph is not describing C's strftime, but a new format function that operates on std::chrono types, not tm. Additionally there is a new type: std::chrono::zoned_time (http://eel.is/c++draft/time.zone.zonedtime) that always has the time_zone abbreviation (and offset) available and can be formatted with the afore mentioned format function.

Example code:

#include <chrono>
#include <iostream>

int
main()
{
using namespace std;
using namespace std::chrono;
auto now = system_clock::now();
std::cout << format("%Z\n", zoned_time{current_zone(), now}); // HKT (or whatever)
std::cout << format("%Z\n", zoned_time{"Asia/Hong_Kong", now}); // HKT or HKST
std::cout << format("%Z\n", zoned_time{"Etc/UTC", now}); // UTC
std::cout << format("%Z\n", now); // UTC
}

(Disclaimer: The final syntax of the formatting string in the format function is likely to be slightly different, but the functionality will be there.)

If you would like to experiment with a preview of this library, it is free and open source here: https://github.com/HowardHinnant/date

Some installation is required: https://howardhinnant.github.io/date/tz.html#Installation

In this preview, you will need to use the header "date/tz.h", and the contents of the library are in namespace date instead of namespace std::chrono.

The preview library can be used with C++11 or later.

zoned_time is templated on a std::chrono::duration which specifies the precision of the time point, and is deduced in the example code above using C++17's CTAD feature. If you are using this preview library in C++11 or C++14, the syntax would look more like:

cout << format("%Z\n", zoned_time<system_clock::duration>{current_zone(), now});

Or there is a non-proposed-for-standardization helper factory function which will do the deduction for you:

cout << format("%Z\n", make_zoned(current_zone(), now));

(#CTAD_eliminates_factory_functions)

Convert a time (UTC ) given as a string to local time

Your problem is that if time_t is a 32 bit value, the earliest possible date it's capable of encoding (given a 1970-1-1 epoch) is 1901-12-13.

However you're not setting the date fields of your tm struct, which means it is defaulting to 0-0-0 which represents 1900-1-0 (since tm_day is 1-based, you actually end up with an invalid day-of-month).

Since this isn't representable by a 32-bit time_t the mktime function is failing and returning -1, a situation you're not checking for.

Simplest fix is to initialise the date fields of the tm struct to something a time_t can represent:

time_sample_struct.tm_year = 114;
time_sample_struct.tm_mday = 1;

std::mktime and timezone info

mktime assumes that the date value is in the local time zone. Thus you can change the timezone environment variable beforehand (setenv) and get the UTC timezone.

Windows tzset

Can also try looking at various home-made utc-mktimes, mktime-utcs, etc.

Changing the timezone in libc time at compile time

However, when we use mktime it attempts to put everything into localtime ...

The compiler's time zone setting at compile time is irrelevant to how code handles time at run time.

The easiest way to avoid local time in conversions is to hope your compiler offers struct_tm (UTC) to time_t, instead of using mktime() (time zone dependent) as an extension like time_t timegm(struct tm *tm).

There is no simple standard solution. Tricks with divining the offset via locatime(), gmtime() fail corner cases.


IMO, to convert struct_tm (UTC) to UNIX time time_t, simply write that code and get it reviewed. It is not that hard as there are few corner cases (aside from overflow).

Some sample code to change int year, int month, int day (0:00:00) to MJD to get OP started.

#include <stdint.h>
static const short DaysMarch1ToBeginingOfMonth[12] = { //
0, 31, 61, 92, 122, 153, 184, 214, 245, 275, 306, 337};
#ifndef INT32_C
#define INT32_C(x) ((int_least32_t)1*(x))
#endif
#define DaysPer400Years (INT32_C(365)*400 + 97)
#define DaysPer100Years (INT32_C(365)*100 + 24)
#define DaysPer4Years (365*4 + 1)
#define DaysPer1Year 365
#define MonthsPerYear 12
#define MonthsPer400Years (12*400)
#define MonthMarch 3
#define mjdOffset 0xA5BE1
#define mjd1900Jan1 15020
// November 17, 1858

// Example: 2015 December 31 --> ymd_to_mjd(2015, 12, 31)
int2x ymd_to_mjd(int year, int month, int day) {
// int2x is a type twice as wide as int to handle extreme int values.
// Use int (at least 32-bit) to handle common values.
int2x year2x = year;

year2x += month / MonthsPerYear;
month %= MonthsPerYear;
// Adjust for month/year to Mar ... Feb
while (month < MonthMarch) {
month += MonthsPerYear;
year2x--;
}

int2x d = (year2x / 400) * DaysPer400Years;
int y400 = (int) (year2x % 400);
d += (y400 / 100) * DaysPer100Years;
int y100 = y400 % 100;
d += (y100 / 4) * DaysPer4Years;
int y4 = y100 % 4;
d += y4 * DaysPer1Year;
d += DaysMarch1ToBeginingOfMonth[month - MonthMarch];
d += day;
// November 17, 1858 == MJD 0
d--;
d -= mjdOffset;
return d;
}


Related Topics



Leave a reply



Submit