Fix timezone transition times.

This is responsible for test failures on DST transition days.

See the comments in gnc-timezone.cpp for an explanation of why this is
correct. The rubric was tested on macOS, Arch Linux, Debian Unstable,
Fedora 33, and Ubuntu 18.04 to confirm universal applicability.
This commit is contained in:
John Ralls 2020-11-06 16:54:22 -08:00
parent b6c0a62bbd
commit 3bcf57e7f2
2 changed files with 79 additions and 4 deletions

View File

@ -549,10 +549,34 @@ namespace DSTRule
std::swap(to_std_time, to_dst_time);
std::swap(std_info, dst_info);
}
if (dst_info->isgmt)
to_dst_time += boost::posix_time::seconds(dst_info->info.gmtoff);
if (std_info->isgmt)
to_std_time += boost::posix_time::seconds(std_info->info.gmtoff);
/* Documentation notwithstanding, the date-time rules are
* looking for local time (wall clock to use the RFC 8538
* definition) values.
*
* The TZ Info contains two fields, isstd and isgmt (renamed
* to isut in newer versions of tzinfo). In theory if both are
* 0 the transition times represent wall-clock times,
* i.e. time stamps in the respective time zone's local time
* at the moment of the transition. If isstd is 1 then the
* representation is always in standard time instead of
* daylight time; this is significant for dst->std
* transitions. If isgmt/isut is one then isstd must also be
* set and the transition time is in UTC.
*
* In practice it seems that the timestamps are always in UTC
* so the isgmt/isut flag isn't meaningful. The times always
* need to have the utc offset added to them to make the
* transition occur at the right time; the isstd flag
* determines whether that should be the standard offset or
* the daylight offset for the daylight->standard transition.
*/
to_dst_time += boost::posix_time::seconds(std_info->info.gmtoff);
if (std_info->isstd) //if isstd always use standard time
to_std_time += boost::posix_time::seconds(std_info->info.gmtoff);
else
to_std_time += boost::posix_time::seconds(dst_info->info.gmtoff);
}

View File

@ -381,6 +381,57 @@ TEST(gnc_datetime_constructors, test_gncdate_end_constructor)
EXPECT_EQ(atime.format("%d-%m-%Y %H:%M:%S"), "06-11-2046 23:59:59");
}
static ::testing::AssertionResult
test_offset(time64 start_time, int hour, int offset1, int offset2,
const char* zone)
{
GncDateTime gdt{start_time + hour * 3600};
if ((hour < 2 && gdt.offset() == offset1) ||
(hour >= 2 && gdt.offset() == offset2))
return ::testing::AssertionSuccess();
else
return ::testing::AssertionFailure() << zone << ": " << gdt.format("%D %T %z %q") << " hour " << hour;
}
TEST(gnc_datetime_constructors, test_DST_start_transition_time)
{
#ifdef __MINGW32__
TimeZoneProvider tzp_can{"A.U.S Eastern Standard Time"};
TimeZoneProvider tzp_la{"Pacific Standard Time"};
#else
TimeZoneProvider tzp_can("Australia/Canberra");
TimeZoneProvider tzp_la("America/Los_Angeles");
#endif
_set_tzp(tzp_la);
for (auto hours = 0; hours < 23; ++hours)
EXPECT_TRUE(test_offset(1583657940, hours, -28800, -25200, "Los Angeles"));
_reset_tzp();
_set_tzp(tzp_can);
for (auto hours = 0; hours < 23; ++hours)
EXPECT_TRUE(test_offset(1601737140, hours, 36000, 39600, "Canberra"));
_reset_tzp();
}
TEST(gnc_datetime_constructors, test_DST_end_transition_time)
{
#ifdef __MINGW32__
TimeZoneProvider tzp_can{"A.U.S Eastern Standard Time"};
TimeZoneProvider tzp_la{"Pacific Standard Time"};
#else
TimeZoneProvider tzp_can("Australia/Canberra");
TimeZoneProvider tzp_la("America/Los_Angeles");
#endif
_set_tzp(tzp_la);
for (auto hours = 0; hours < 23; ++hours)
EXPECT_TRUE(test_offset(1604217540, hours, -25200, -28800, "Los Angeles"));
_reset_tzp();
_set_tzp(tzp_can);
for (auto hours = 0; hours < 23; ++hours)
EXPECT_TRUE(test_offset(1586008740, hours, 39600, 36000, "Canberra"));
_reset_tzp();
}
TEST(gnc_datetime_constructors, test_gncdate_neutral_constructor)
{
const ymd aymd = { 2017, 04, 20 };