From 0e723610f0cb23515f564bc87e6b0b2f16577f5b Mon Sep 17 00:00:00 2001 From: John Ralls Date: Fri, 2 Nov 2018 10:29:52 -0700 Subject: [PATCH] Bug 795080 - Some dates reset to 01/01/1970 The first fix for this bug handled structs tm with ambiguous times. This one fixes the GncDate constructor when the time is ambiguous because it's in the DST-change hour, using the same add 3 hours, construct the LDT, and subtract the 3 hours from the result. The string constructor handles only simple-offset HH:MM timezones and so is immune to the bug. --- libgnucash/engine/gnc-datetime.cpp | 59 +++++++++++++++---- libgnucash/engine/test/gtest-gnc-datetime.cpp | 22 +++++++ po/POTFILES.in | 1 + 3 files changed, 69 insertions(+), 13 deletions(-) diff --git a/libgnucash/engine/gnc-datetime.cpp b/libgnucash/engine/gnc-datetime.cpp index f8b52dd2dd..8561cb0ac6 100644 --- a/libgnucash/engine/gnc-datetime.cpp +++ b/libgnucash/engine/gnc-datetime.cpp @@ -52,12 +52,18 @@ using LDTBase = boost::local_time::local_date_time_baseget(temp.date().year()); return LDT(temp, tz); } catch(boost::gregorian::bad_year&) @@ -167,7 +173,7 @@ LDT_from_struct_tm(const struct tm tm) tdate = boost::gregorian::date_from_tm(tm); tdur = boost::posix_time::time_duration(tm.tm_hour, tm.tm_min, tm.tm_sec, 0); - tz = tzp.get(tdate.year()); + tz = tzp->get(tdate.year()); LDT ldt(tdate, tdur, tz, LDTBase::EXCEPTION_ON_ERROR); return ldt; } @@ -198,20 +204,32 @@ LDT_from_struct_tm(const struct tm tm) using TD = boost::posix_time::time_duration; +void +_set_tzp(TimeZoneProvider& new_tzp) +{ + tzp = &new_tzp; +} + +void +_reset_tzp() +{ + tzp = <zp; +} + class GncDateTimeImpl { public: - GncDateTimeImpl() : m_time(boost::local_time::local_sec_clock::local_time(tzp.get(boost::gregorian::day_clock::local_day().year()))) {} + GncDateTimeImpl() : m_time(boost::local_time::local_sec_clock::local_time(tzp->get(boost::gregorian::day_clock::local_day().year()))) {} GncDateTimeImpl(const time64 time) : m_time(LDT_from_unix_local(time)) {} GncDateTimeImpl(const struct tm tm) : m_time(LDT_from_struct_tm(tm)) {} GncDateTimeImpl(const GncDateImpl& date, DayPart part = DayPart::neutral); GncDateTimeImpl(std::string str); - GncDateTimeImpl(PTime&& pt) : m_time(pt, tzp.get(pt.date().year())) {} + GncDateTimeImpl(PTime&& pt) : m_time(pt, tzp->get(pt.date().year())) {} GncDateTimeImpl(LDT&& ldt) : m_time(ldt) {} operator time64() const; operator struct tm() const; - void now() { m_time = boost::local_time::local_sec_clock::local_time(tzp.get(boost::gregorian::day_clock::local_day().year())); } + void now() { m_time = boost::local_time::local_sec_clock::local_time(tzp->get(boost::gregorian::day_clock::local_day().year())); } long offset() const; struct tm utc_tm() const { return to_tm(m_time.utc_time()); } std::unique_ptr date() const; @@ -254,13 +272,28 @@ private: */ GncDateTimeImpl::GncDateTimeImpl(const GncDateImpl& date, DayPart part) : - m_time(date.m_greg, time_of_day[part], tzp.get(date.m_greg.year()), + m_time(date.m_greg, time_of_day[part], tzp->get(date.m_greg.year()), LDT::NOT_DATE_TIME_ON_ERROR) { using boost::posix_time::hours; - try + if (m_time.is_not_a_date_time()) + { + try + { + auto t_o_d = time_of_day[part] + hours(3); + LDT time(date.m_greg, t_o_d, tzp->get(date.m_greg.year()), + LDT::EXCEPTION_ON_ERROR); + m_time = time - hours(3); + } + catch(boost::gregorian::bad_year&) + { + throw(std::invalid_argument("Time value is outside the supported year range.")); + } + } + + if (part == DayPart::neutral) { - if (part == DayPart::neutral) + try { auto offset = m_time.local_time() - m_time.utc_time(); m_time = LDT(date.m_greg, time_of_day[part], utc_zone, @@ -270,10 +303,10 @@ GncDateTimeImpl::GncDateTimeImpl(const GncDateImpl& date, DayPart part) : if (offset > hours(13)) m_time -= hours(offset.hours() - 11); } - } - catch(boost::gregorian::bad_year&) - { - throw(std::invalid_argument("Time value is outside the supported year range.")); + catch(boost::gregorian::bad_year&) + { + throw(std::invalid_argument("Time value is outside the supported year range.")); + } } } diff --git a/libgnucash/engine/test/gtest-gnc-datetime.cpp b/libgnucash/engine/test/gtest-gnc-datetime.cpp index 65989bf4da..1d25933d76 100644 --- a/libgnucash/engine/test/gtest-gnc-datetime.cpp +++ b/libgnucash/engine/test/gtest-gnc-datetime.cpp @@ -23,8 +23,13 @@ \********************************************************************/ #include "../gnc-datetime.hpp" +#include "../gnc-timezone.hpp" #include +/* Backdoor to enable unittests to temporarily override the timezone: */ +void _set_tzp(TimeZoneProvider& tz); +void _reset_tzp(); + TEST(gnc_date_constructors, test_default_constructor) { GncDate date; @@ -341,6 +346,23 @@ TEST(gnc_datetime_constructors, test_gncdate_start_constructor) EXPECT_EQ(atime.format("%d-%m-%Y %H:%M:%S"), "20-04-2017 00:00:00"); } +/* Summertime transitions have been a recurring problem. At the time of adding + * this test the GncDateEditor was refusing to set the date to 28 October 2018 + * in the Europe/London timezone. + */ +TEST(gnc_datetime_constructors, test_gncdate_BST_transition) +{ + const ymd begins = {2018, 03, 25}; + const ymd ends = {2018, 10, 28}; + TimeZoneProvider tzp("Europe/London"); + _set_tzp(tzp); + GncDateTime btime(GncDate(begins.year, begins.month, begins.day), DayPart::start); + GncDateTime etime(GncDate(ends.year, ends.month, ends.day), DayPart::start); + _reset_tzp(); + EXPECT_EQ(btime.format("%d-%m-%Y %H:%M:%S"), "25-03-2018 00:00:00"); + EXPECT_EQ(etime.format("%d-%m-%Y %H:%M:%S"), "28-10-2018 00:00:00"); +} + TEST(gnc_datetime_constructors, test_gncdate_end_constructor) { const ymd aymd = { 2046, 11, 06 }; diff --git a/po/POTFILES.in b/po/POTFILES.in index 8e327ea055..892c211976 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -616,6 +616,7 @@ libgnucash/core-utils/gnc-locale-utils.c libgnucash/core-utils/gnc-path.c libgnucash/core-utils/gnc-prefs.c libgnucash/doc/doxygen_main_page.c +libgnucash/engine/.#gnc-datetime.cpp libgnucash/engine/Account.cpp libgnucash/engine/business-core.scm libgnucash/engine/cap-gains.c