Dennis
Dennis

Reputation: 3741

Time offset calculation is off by one minute

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

Answers (2)

Howard Hinnant
Howard Hinnant

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

C++20 update

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';
}

Demo

Upvotes: 2

Dennis
Dennis

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

Related Questions