Reputation: 2205
Consider the following C++ code
#include <ctime>
#include <iostream>
int main()
{
std::time_t now = std::time(nullptr);
struct tm local = *std::localtime(&now);
struct tm gm = *std::gmtime(&now);
char str[20];
std::strftime(str, 20, "%Z", &local);
std::cout << str << std::endl; // HKT
std::strftime(str, 20, "%Z", &gm);
std::cout << str << std::endl; // UTC
return 0;
}
So stored in now
is an unambiguous integral value, while local
and gm
are struct tm
that store human-readable date/time information. Then I print out the formatted information (timezone) based only on the struct tm
objects.
According to the cplusplus reference, the data members of struct tm
are
tm_sec
tm_min
tm_hour
tm_mday
tm_mon
tm_year
tm_wday
tm_yday
tm_isdst
If that's all that a struct tm
contains, how does the program know that the timezone information from it? That is, how does it know that the timezone is HKT
for local
, and that the timezone is UTC
for gm
?
If that's not all that a struct tm
contains, please explain how it stores timezone information.
By the way, though the demo code is in C++, I guess this question in essence stands as a legitimate C question as well.
Upvotes: 11
Views: 8603
Reputation: 218770
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 typeformat_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)
Upvotes: 6
Reputation: 47952
One of the reasons date and time programming is so difficult is that it's fundamentally at least a somewhat difficult problem: "Thirty days hath September", and sexagesimal arithmetic, and time zones, and daylight saving time, and leap years, and let's not even talk about leap seconds.
But the other reason it's difficult is that all too many libraries and languages make a perfect mess of it, and C is unfortunately no exception. (C++ is trying to do better, as Howard mentions in his answer.)
Even though everybody knows global variables are Bad, C's date/time functions basically use a couple of them. In effect, the concept of "this system's current time zone" is a global variable, and the global data which describes that time zone is shared willy-nilly between localtime
and strftime
and a number of other functions.
So strftime
can fill in %z
and %Z
based on that global data, even if it isn't passed in as part of a struct tm
value.
That's obviously a suboptimal arrangement, and it would start causing real problems if there were a way for a program to dynamically change the time zone it wants to use for localtime
and the rest. (And this arrangement persists in part because there's not actually a good, portable, Standard way for a program to change the local time zone it's using.)
Over the years there have been various half-hearted attempt to clean some of the mess up (while preserving backwards compatibility, of course). One of those attempts involves the extended tm_gmtoff
and tm_zone
fields you've discovered in some systems' versions of struct tm
. Those additions are a huge improvement -- I can't imagine doing serious date/time programming on a system without them -- but they're still not Standard, and there are still plenty of systems that don't have them (not even with the "hidden" spellings __tm_gmtoff
and __tm_zone
).
You can read much more about the sordid history of date/time support in C in this paper: Time, Clock, and Calendar Programming In C, by Eric Raymond.
Upvotes: 1
Reputation: 2205
Thanks for all the comments to the question which help pointing to the right direction. I post some of my own research below. I speak based on an archived repo of GNU C Library that I found on the GitHub. Its version is 2.28.9000
.
In glibc/time/bits/types/struct_tm.h
there is
struct tm
{
int tm_sec; /* Seconds. [0-60] (1 leap second) */
int tm_min; /* Minutes. [0-59] */
int tm_hour; /* Hours. [0-23] */
int tm_mday; /* Day. [1-31] */
int tm_mon; /* Month. [0-11] */
int tm_year; /* Year - 1900. */
int tm_wday; /* Day of week. [0-6] */
int tm_yday; /* Days in year.[0-365] */
int tm_isdst; /* DST. [-1/0/1]*/
# ifdef __USE_MISC
long int tm_gmtoff; /* Seconds east of UTC. */
const char *tm_zone; /* Timezone abbreviation. */
# else
long int __tm_gmtoff; /* Seconds east of UTC. */
const char *__tm_zone; /* Timezone abbreviation. */
# endif
};
It seems that struct tm
does store time zone information, at least in this implementation.
Upvotes: 4