From 3bcf57e7f21033a73102ad986bcf669644fa9979 Mon Sep 17 00:00:00 2001 From: John Ralls Date: Fri, 6 Nov 2020 16:54:22 -0800 Subject: [PATCH] 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. --- libgnucash/engine/gnc-timezone.cpp | 32 ++++++++++-- libgnucash/engine/test/gtest-gnc-datetime.cpp | 51 +++++++++++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/libgnucash/engine/gnc-timezone.cpp b/libgnucash/engine/gnc-timezone.cpp index 3bb157b5a0..6c03481d42 100644 --- a/libgnucash/engine/gnc-timezone.cpp +++ b/libgnucash/engine/gnc-timezone.cpp @@ -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); } diff --git a/libgnucash/engine/test/gtest-gnc-datetime.cpp b/libgnucash/engine/test/gtest-gnc-datetime.cpp index 3fcbb6d20a..446f810e89 100644 --- a/libgnucash/engine/test/gtest-gnc-datetime.cpp +++ b/libgnucash/engine/test/gtest-gnc-datetime.cpp @@ -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 };