Home > front end >  C vs. python: daylight saving time not recognized when extracting Unix time?
C vs. python: daylight saving time not recognized when extracting Unix time?

Time:02-03

I'm attempting to calculate the Unix time of a given date and time represented by two integers, e.g.

testdate1 = 20060711 (July 11th, 2006)

testdate2 = 4 (00:00:04, 4 seconds after midnight)

in a timezone other than my local timezone. To calculate the Unix time, I feed testdate1, testdate2 into a function I adapted from Convert date to unix time stamp in c

int unixtime (int testdate1, int testdate2) {

time_t rawtime; 
struct tm * timeinfo; 

//time1, ..., time6 are external functions that extract the 
//year, month, day, hour, minute, seconds digits from testdate1, testdate2

int year=time1(testdate1);
int month=time2(testdate1);
int day=time3(testdate1);
int hour=time4(testdate2);
int minute=time5(testdate2);
int second=time6(testdate2);

time ( &rawtime );
timeinfo = localtime ( &rawtime );  

timeinfo->tm_year   = year - 1900; 
timeinfo->tm_mon    = month - 1;   
timeinfo->tm_mday   = day;         
timeinfo->tm_hour   = hour;        
timeinfo->tm_min    = minute;          
timeinfo->tm_sec    = second;       

int date;
date = mktime(timeinfo);

return date;

}

Which I call from the main code

using namespace std;
int main(int argc, char* argv[])
{

int testdate1 = 20060711;
int testdate2 = 4;


//switch to CET time zone 
setenv("TZ","Europe/Berlin", 1); 
tzset();

cout << testdate1 << "\t" << testdate2 << "\t" << unixtime(testdate1,testdate2) << "\n";

    return 0;
}

With the given example, I get unixtime(testdate1,testdate2) = 1152572404, which according to

https://www.epochconverter.com/timezones?q=1152572404&tz=Europe/Berlin

is 1:00:04 am CEST, but I want this to be 0:00:04 CEST.

The code seems to work perfectly well if I choose a testdate1, testdate2 in which daylight saving time (DST) isn't being observed. For example, simply setting the month to February with all else unchanged is accomplished by setting testdate1 = 20060211. This gives unixtime(testdate1,testdate2) = 1139612404, corresponding to hh:mm:ss = 00:00:04 in CET, as desired.

My impression is that setenv("TZ","Europe/Berlin", 1) is supposed to account for DST when applicable, but perhaps I am mistaken. Can TZ interpret testdate1, testdate2 in such a way that it accounts for DST?

Interestingly, I have a python code that performs the same task by changing the local time via os.environ['TZ'] = 'Europe/Berlin'. Here I have no issues, as it seems to calculate the correct Unix time regardless of DST/non-DST.

CodePudding user response:

localtime sets timeinfo->tm_isdst to that of the current time - not of the date you parse.

Don't call localtime. Set timeinfo->tm_isdst to -1:

The value specified in the tm_isdst field informs mktime() whether or not daylight saving time (DST) is in effect for the time supplied in the tm structure: a positive value means DST is in effect; zero means that DST is not in effect; and a negative value means that mktime() should (use timezone information and system databases to) attempt to determine whether DST is in effect at the specified time.

See the code example in https://en.cppreference.com/w/cpp/chrono/c/mktime

CodePudding user response:

Maxim's answer is correct, and I've upvoted it. But I also thought it might be helpful to show how this can be done in C 20 using the newer <chrono> tools. This isn't implemented everywhere yet, but it is here in Visual Studio and will be coming elsewhere soon.

There's two main points I'd like to illustrate here:

  1. <chrono> is convenient for conversions like this, even if both the input and the output does not involve std::chrono types. One can convert the integral input to chrono, do the conversion, and then convert the chrono result back to integral.

  2. There's a thread safety weakness in using the TZ environment variable, as this is a type of global. If another thread is also doing some type of time computation, it may not get the correct answer if the computer's time zone unexpectedly changes out from under it. The <chrono> solution is thread-safe. It doesn't involve globals or environment variables.

The first job is to unpack the integral data. Here I show how to do this, and convert it into chrono types in one step:

std::chrono::year_month_day
get_ymd(int ymd)
{
    using namespace std::chrono;

    day d(ymd % 100);
    ymd /= 100;
    month m(ymd % 100);
    ymd /= 100;
    year y{ymd};
    return y/m/d;
}

get_ymd takes "testdate1", extracts the individual integral fields for day, month, and year, then converts each integral field into the std::chrono types day, month and year, and finally combines these three separate fields into a std::chrono::year_month_day to return it as one value. This return type is simple a {year, month, day} data structure -- like a tuple but with calendrical meaning.

The / syntax is simply a convenient factory function for constructing a year_month_day. And this construction can be done with any of these three orderings: y/m/d, d/m/y and m/d/y. This syntax, when combined with auto, also means that you often don't have to spell out the verbose name year_month_day:

auto
get_ymd(int ymd)
{
    // ...
    return y/m/d;
}

get_hms unpacks the hour, minute and second fields and returns that as a std::chrono::seconds:

std::chrono::seconds
get_hms(int hms)
{
    using namespace std::chrono;

    seconds s{hms % 100};
    hms /= 100;
    minutes m{hms % 100};
    hms /= 100;
    hours h{hms};
    return h   m   s;
}

The code is very similar to that for get_ymd except that the return is the sum of the hours, minutes and seconds. The chrono library does the job for you of converting hours and minutes to seconds while performing the summation.

Next is the function for doing the conversion, and returning the result back as an int.

int
unixtime(int testdate1, int testdate2)
{
    using namespace std::chrono;

    auto ymd = get_ymd(testdate1);
    auto hms = get_hms(testdate2);
    auto ut = locate_zone("Europe/Berlin")->to_sys(local_days{ymd}   hms);
    return ut.time_since_epoch().count();
}

std::chrono::locate_zone is called to get a pointer to the std::chrono::time_zone with the name "Europe/Berlin". The std::lib manages the lifetime of this object, so you don't have to worry about it. It is a const singleton, created on demand. And it has no impact on what time zone your computer considers its "local time zone".

The std::chrono::time_zone has a member function called to_sys that takes a local_time, and converts it to a sys_time, using the proper UTC offset for this time zone (taking into account daylight saving rules when applicable).

Both local_time and sys_time are std::chrono::time_point types. local_time is "some local time", not necessarily your computer's local time. You can associate a local time with a time zone in order to specify the locality of that time.

sys_time is a time_point based on system_clock. This tracks UTC (Unix time).

The expression local_days{ymd} hms converts ymd and hms to local_time with a precision of seconds. local_days is just another local_time time_point, but with a precision of days.

The type of ut is time_point<system_clock, seconds>, which has a convenience type alias called sys_seconds, though auto makes that name unnecessary in this code.

To unpack the sys_seconds into an integral type, the .time_since_epoch() member function is called which results in the duration seconds, and then the .count() member function is called to extract the integral value from that duration.

When int is 32 bits, this function is susceptible to the year 2038 overflow problem. To fix that, simply change the return type of unixtime to return a 64 bit integral type (or make the return auto). Nothing else needs to change as std::chrono::seconds is already required to be greater than 32 bits and will not overflow at 68 years. Indeed std::chrono::seconds is usually represented by a signed 64 bit integral type in practice, giving it a range greater than the age of the universe (even if the scientists are off by an order of magnitude).

  •  Tags:  
  • Related