Reputation: 3741
I am trying to replace a number of different time classes with a single consistent API. However I have recently run into a problem whereby I cannot serialise the timezone offset correctly. Note that I am attempting to replicate an existing format that is already in wide use in the system.
The format should be YYYY-mm-DD HH:MM:SS.xxxxxxx -HHMM
, where the x
represents the sub-second precision and the last -HHMM
is the TZ offset from UTC.
Code:
using namespace My::Time;
namespace chrn = std::chrono;
time_point now = clock::now();
time_point lclNow = getDefaultCalendarProvider()->toLocal(now);
duration diff{ lclNow - now };
std::wstring sign = diff > duration::zero() ? L" +" : L" -";
duration ms{ now.time_since_epoch().count() % duration::period::den };
int diffHrs = popDurationPart<chrn::hours>(diff).count();
int diffMins{ abs(chrn::duration_cast<chrn::minutes>(diff).count()) };
std::cout << Format{ lclNow, TimeZone::UTC, L" %Y-%m-%d %H:%M:%S." } << ms.count()
<< sign << std::setfill(L'0') << std::setw(2) << diffHrs
<< std::setfill(L'0') << std::setw(2) << diffMins << std::endl;
Problem:
Expected:<2016-05-25 09:45:18.1970000 +0100> Actual:< 2016-05-25 09:45:18.1964787 +0059>
The expected value is what you get when I use the old class to do the same operation. The problem appears to be at the point where I attempt to get the difference between lclNow
and now
.
Currently I am in UTC +1 (due to DST being in effect). However the diff
value is always 35999995635
. Being on Visual C++ in Windows the tick is 100 ns, so there are 10000000 ticks per second, meaning the diff
value is 3599.9995 seconds, which is just short of the 3600 seconds I would need to make an hour.
When I print the two time values using the same format then I can see that they are exactly one hour apart. So it appears that the time-zone translation is not the issue.
Upvotes: 2
Views: 201
Reputation: 219428
Consider using this free, open source time zone library which does exactly what you want with very simple syntax, and works on VS-2013 and later:
#include "tz.h"
#include <iostream>
int
main()
{
using namespace date;
using namespace std::chrono;
auto t = make_zoned(current_zone(), system_clock::now());
std::cout << format("%F %T %z", t) << '\n';
}
This should output for you:
2016-05-25 09:45:18.1970000 +0100
This library is now part of C++20 <chrono>
and ships with MSVC. The syntax is slightly changed from that shown above:
#include <chrono>
#include <format>
#include <iostream>
int
main()
{
std::chrono::zoned_time t{std::chrono::current_zone(),
std::chrono::system_clock::now()};
std::cout << std::format("{:%F %T %z}", t) << '\n';
}
Upvotes: 2
Reputation: 3741
The issue appears to have come from the time-zone conversions as I was attempting (as SamVarshavchik pointed out). Unfortunately I am unable to use Howard Hinnant's very complete date and tz libraries because they require a mechanism to update the IANA time-zone DB that is required for them to work, so I resorted to wrapping the Windows native calls for the time-zone conversions; namely the TzSpecificLocalTimeToSystemTime and SystemTimeToTzSpecificLocalTime functions.
However these only work with SYSTEMTIME and not time_point
. This meant I took the quick and easy option of converting the time_point
to a FILETIME
(just modify the "epoch") and the FILETIME
to a SYSTEMTIME
before passing it to one of the two above functions. This resulted in truncation of the time value when it was pushed into the SYSTEMTIME
struct (which only holds millisecond resolution). The outcome is that while I was accurate for dates, I was not entirely accurate when converting the date back into the original value.
The new solution does no calendar mapping for the basic time_point
to time_point
translations. It uses the following code to work out the offset in std::chrono::minutes
(where zoneInfo
is a TIME_ZONE_INFORMATION):
time_point WindowsTzDateProvider::doToUtc(const time_point& inLocal) const {
return inLocal + getBias(inLocal);
}
time_point WindowsTzDateProvider::doToLocal(const time_point& inUtc) const {
return inUtc - getBias(inUtc);
}
std::chrono::minutes WindowsTzDateProvider::doGetBias(const time_point& input) const {
bool isDst = CalendarDateProvider::isDstInEffect(input);
minutes baseBias{ zoneInfo.Bias };
minutes extraBias{ isDst ? zoneInfo.DaylightBias : zoneInfo.StandardBias };
return baseBias + extraBias;
}
bool CalendarDateProvider::isDstInEffect(const time_point& t) {
time_t epochTime = clock::to_time_t(t);
tm out;
#ifdef WIN32
localtime_s(&out, &epochTime);
#else
localtime_r(&out, &epochTime);
#endif
return out.tm_isdst > 0;
}
Note: I'm using the non-virtual interface idiom for the classes, hence the "do..." versions of the methods.
Upvotes: 3