Does struct tm store time zone information as its data member
Asked Answered
S

3

11

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.

Schorl answered 18/10, 2019 at 4:28 Comment(9)
tm doesn't contain timezone information. strftime gets the timezone by behind-the-scenes voodoo. If you want to get the timezone in general, that's a bit of a mess. There is (currently) no standard way to get a time zone. Fortunately Howard Hinnant is on that job....Tinner
Thank you @Tinner This partially answers my question. But I still have follow-up questions: given all the information stored in tm, how does strftime know to respond in different ways to two struct tm objects? Unless tm contains some information like this tm is created by localtime, that tm is created by gmtime.Schorl
The tm struct doesn't store timezone infos, what makes you think it does? The difference is rather in the calls to gmtime() and localtime().Speight
Man page covers how timezone info is acquired on a POSIX system. Still looking for how strftime tells the suckers apart. Should add that POSIX leaves what happens undefined.Tinner
@UlrichEckhardt because strftime() can be passed a struct tm and a %Z format specifier and provide the timezone as an output. I'd like to know how it can manage that too,Speiss
Fails outright with gcc 9.2.0 from MSYS2 under Windows. Seeing that reminded me that I'd seen non-standard tms with extra info. Here's one. Note the const char *tm_zone member. What platform are you compiling for? Take a look at the tm implementation to see if they've extended the structure.Tinner
Here's an example of the extensions in action: ideone.com/Tx6FEDTinner
It seems to me that this was a mistake that got locked in. pubs.opengroup.org/onlinepubs/009695399/functions/strftime.html contains notes that suggest to me that this can't really be done right or relied on. I'd classify the %Z specifier as dubious and best not used.Speiss
I won't go as far as do not use, but the lack of portability should be carefully considered before using. Especially considering there are better options coming soon to a chrono library near you.Tinner
E
6

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)

Eicher answered 18/10, 2019 at 14:19 Comment(0)
S
4

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.

Schorl answered 18/10, 2019 at 6:3 Comment(0)
P
1

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.

Persse answered 18/10, 2019 at 19:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.