gnucash maint: Multiple changes pushed

John Ralls jralls at code.gnucash.org
Sun Mar 21 18:55:17 EDT 2021


Updated	 via  https://github.com/Gnucash/gnucash/commit/1221d7eb (commit)
	 via  https://github.com/Gnucash/gnucash/commit/ebb5eb1f (commit)
	from  https://github.com/Gnucash/gnucash/commit/902561fa (commit)



commit 1221d7ebc174410a251592cd1471924e8f083fc3
Author: John Ralls <jralls at ceridwen.us>
Date:   Sat Mar 20 15:49:54 2021 -0700

    Bug 798150 - Error on report over time
    
    Extract functions LDT_from_date_time and LDT_from_date_daypart
    to avoid duplicate code. Handle date-times in start-of-DST transitions
    and better handle those in end-of-DST transitions. Test the results.

diff --git a/libgnucash/engine/gnc-datetime.cpp b/libgnucash/engine/gnc-datetime.cpp
index 2f6d83de1..9563393e8 100644
--- a/libgnucash/engine/gnc-datetime.cpp
+++ b/libgnucash/engine/gnc-datetime.cpp
@@ -52,6 +52,7 @@ static const char* log_module = "gnc.engine";
 
 #define N_(string) string //So that xgettext will find it
 
+using PTZ = boost::local_time::posix_time_zone;
 using Date = boost::gregorian::date;
 using Month = boost::gregorian::greg_month;
 using PTime = boost::posix_time::ptime;
@@ -169,49 +170,97 @@ LDT_from_unix_local(const time64 time)
         throw(std::invalid_argument("Time value is outside the supported year range."));
     }
 }
+/* If a date-time falls in a DST transition the LDT constructor will
+ * fail because either the date-time doesn't exist (when starting DST
+ * because the transition skips an hour) or is ambiguous (when ending
+ * because the transition hour is repeated). We try again an hour
+ * later to be outside the DST transition. When starting DST that's
+ * now the correct time but at the end of DST we need to set the
+ * returned time back an hour.
+ */
+static LDT
+LDT_with_pushup(const Date& tdate, const Duration& tdur, const TZ_Ptr tz,
+                 bool putback)
+{
+    static const boost::posix_time::hours pushup{1};
+    LDT ldt{tdate, tdur + pushup, tz, LDTBase::NOT_DATE_TIME_ON_ERROR};
+    if (ldt.is_special())
+    {
+        std::string error{"Couldn't create a valid datetime at "};
+        error += to_simple_string(tdate) + " ";
+        error += to_simple_string(tdur) + " TZ ";
+        error += tz->std_zone_abbrev();
+        throw(std::invalid_argument{error});
+    }
+    if (putback)
+        ldt -= pushup;
+    return ldt;
+}
 
 static LDT
-LDT_from_struct_tm(const struct tm tm)
+LDT_from_date_time(const Date& tdate, const Duration& tdur, const TZ_Ptr tz)
 {
-    Date tdate;
-    Duration tdur;
-    TZ_Ptr tz;
 
     try
     {
-        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());
         LDT ldt(tdate, tdur, tz, LDTBase::EXCEPTION_ON_ERROR);
         return ldt;
     }
-    catch(boost::gregorian::bad_year&)
+    catch (const boost::local_time::time_label_invalid& err)
     {
-        throw(std::invalid_argument("Time value is outside the supported year range."));
+        return LDT_with_pushup(tdate, tdur, tz, false);
     }
-    catch(boost::local_time::time_label_invalid&)
+
+    catch (const boost::local_time::ambiguous_result& err)
     {
-        throw(std::invalid_argument("Struct tm does not resolve to a valid time."));
+        return LDT_with_pushup(tdate, tdur, tz, true);
     }
-    catch(boost::local_time::ambiguous_result&)
+
+    catch(boost::gregorian::bad_year&)
     {
-        /* We plunked down in the middle of a DST change. Try constructing the
-         * LDT three hours later to get a valid result then back up those three
-         * hours to have the time we want.
-         */
-        using boost::posix_time::hours;
-        auto hour = tm.tm_hour;
-        tdur += hours(3);
-        LDT ldt(tdate, tdur, tz, LDTBase::NOT_DATE_TIME_ON_ERROR);
-        if (ldt.is_special())
-            throw(std::invalid_argument("Couldn't create a valid datetime."));
-        ldt -= hours(3);
-        return ldt;
+        throw(std::invalid_argument("Time value is outside the supported year range."));
     }
+
 }
 
-using TD = boost::posix_time::time_duration;
+static LDT
+LDT_from_date_daypart(const Date& date, DayPart part, const TZ_Ptr tz)
+{
+    using hours = boost::posix_time::hours;
+
+    static const Duration day_begin{0, 0, 0};
+    static const Duration day_neutral{10, 59, 0};
+    static const Duration day_end{23, 59, 59};
+
+
+    switch (part)
+    {
+    case DayPart::start:
+        return LDT_from_date_time(date, day_begin, tz);
+    case DayPart::end:
+        return LDT_from_date_time(date, day_end, tz);
+    default: // To stop gcc from emitting a control reaches end of non-void function.
+    case DayPart::neutral:
+        PTime pt{date, day_neutral};
+        LDT lt{pt, tz};
+        auto offset = lt.local_time() - lt.utc_time();
+        if (offset < hours(-10))
+            lt -= hours(offset.hours() + 10);
+        if (offset > hours(13))
+            lt += hours(13 - offset.hours());
+        return lt;
+    }
+}
+
+static LDT
+LDT_from_struct_tm(const struct tm tm)
+{
+    Date tdate{boost::gregorian::date_from_tm(tm)};
+    Duration tdur{boost::posix_time::time_duration(tm.tm_hour, tm.tm_min,
+                                                  tm.tm_sec, 0)};
+    TZ_Ptr tz{tzp->get(tdate.year())};
+    return LDT_from_date_time(tdate, tdur, tz);
+}
 
 void
 _set_tzp(TimeZoneProvider& new_tzp)
@@ -248,10 +297,8 @@ public:
     static std::string timestamp();
 private:
     LDT m_time;
-    static const TD time_of_day[3];
 };
 
-const TD GncDateTimeImpl::time_of_day[3] = {TD(0, 0, 0), TD(10, 59, 0), TD(23, 59, 59)};
 /** Private implementation of GncDate. See the documentation for that class.
  */
 class GncDateImpl
@@ -281,49 +328,15 @@ private:
     friend bool operator!=(const GncDateImpl&, const GncDateImpl&);
 };
 
-/* Member function definitions for GncDateTimeImpl.
+/* Needs to be separately defined so that the friend decl can grant
+ * access to date.m_greg.
  */
-
 GncDateTimeImpl::GncDateTimeImpl(const GncDateImpl& date, DayPart part) :
-    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;
-    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)
-    {
-        try
-        {
-            auto offset = m_time.local_time() - m_time.utc_time();
-            m_time = LDT(date.m_greg, time_of_day[part], utc_zone,
-                         LDT::EXCEPTION_ON_ERROR);
-            if (offset < hours(-10))
-                m_time -= hours(offset.hours() + 10);
-            if (offset > hours(13))
-                m_time += hours(13 - offset.hours());
-        }
-        catch(boost::gregorian::bad_year&)
-        {
-            throw(std::invalid_argument("Time value is outside the supported year range."));
-        }
-    }
-}
+    m_time{LDT_from_date_daypart(date.m_greg, part,
+                                 tzp->get(date.m_greg.year()))} {}
 
-using PTZ = boost::local_time::posix_time_zone;
+/* Member function definitions for GncDateTimeImpl.
+ */
 
 static TZ_Ptr
 tz_from_string(std::string str)
@@ -368,8 +381,7 @@ GncDateTimeImpl::GncDateTimeImpl(std::string str) :
         if (sm[2].matched)
             tzstr += sm[2];
         tzptr = tz_from_string(tzstr);
-        m_time = LDT(pdt.date(), pdt.time_of_day(), tzptr,
-                         LDTBase::NOT_DATE_TIME_ON_ERROR);
+        m_time = LDT_from_date_time(pdt.date(), pdt.time_of_day(), tzptr);
     }
     catch(boost::gregorian::bad_year&)
     {
diff --git a/libgnucash/engine/gnc-datetime.hpp b/libgnucash/engine/gnc-datetime.hpp
index 58d1f32fb..cd19628fa 100644
--- a/libgnucash/engine/gnc-datetime.hpp
+++ b/libgnucash/engine/gnc-datetime.hpp
@@ -37,10 +37,10 @@ typedef struct
     int day; //1-31
 } ymd;
 
-enum DayPart : int {
-    start,  // 00:00
-    neutral,  // 10:59
-    end,  // 23:59
+enum class DayPart {
+    start,    // 00:00 local
+    neutral,  // 10:59 UTC
+    end,      // 23:59 local
 };
 
 class GncDateTimeImpl;
diff --git a/libgnucash/engine/test/gtest-gnc-datetime.cpp b/libgnucash/engine/test/gtest-gnc-datetime.cpp
index a3ceb6222..01fbbf584 100644
--- a/libgnucash/engine/test/gtest-gnc-datetime.cpp
+++ b/libgnucash/engine/test/gtest-gnc-datetime.cpp
@@ -432,8 +432,56 @@ TEST(gnc_datetime_constructors, test_DST_end_transition_time)
     _reset_tzp();
 }
 
+TEST(gnc_datetime_constructors, test_create_in_transition)
+{
+#ifdef __MINGW32__
+    TimeZoneProvider tzp_br{"E. South America Standard Time"};
+#else
+    TimeZoneProvider tzp_br("America/Sao_Paulo");
+#endif
+    _set_tzp(tzp_br);
+    /* Test Daylight Savings start: When Sao Paolo had daylight
+     * savings time it ended at 23:59:59 and the next second was
+     * 01:00:00 so that's when the day starts.
+     */
+    GncDate date0{"2018-11-03", "y-m-d"};
+    GncDateTime gncdt0{date0, DayPart::end};
+    EXPECT_EQ(gncdt0.format_zulu("%Y-%m-%d %H:%M:%S %Z"), "2018-11-04 02:59:59 UTC");
+    EXPECT_EQ(gncdt0.format("%Y-%m-%d %H:%M:%S %Z"), "2018-11-03 23:59:59 -03");
+    GncDate date1{"2018-11-04", "y-m-d"};
+    GncDateTime gncdt1{date1, DayPart::start};
+    EXPECT_EQ(gncdt1.format_zulu("%Y-%m-%d %H:%M:%S %Z"), "2018-11-04 03:00:00 UTC");
+    EXPECT_EQ(gncdt1.format("%Y-%m-%d %H:%M:%S %Z"), "2018-11-04 01:00:00 -02");
+    /* End of day, end of DST. We want one second before midnight in
+     * std time, i.e. -03. Unfortunately clang yields the still-in-DST time.
+     */
+    GncDate date2{"2018-02-17", "y-m-d"};
+    GncDateTime gncdt2{date2, DayPart::end};
+#ifdef __clang__
+    EXPECT_EQ(gncdt2.format_zulu("%Y-%m-%d %H:%M:%S %Z"), "2018-02-18 01:59:59 UTC");
+    EXPECT_EQ(gncdt2.format("%Y-%m-%d %H:%M:%S %Z"), "2018-02-17 23:59:59 -02");
+#else
+    EXPECT_EQ(gncdt2.format_zulu("%Y-%m-%d %H:%M:%S %Z"), "2018-02-18 02:59:59 UTC");
+    EXPECT_EQ(gncdt2.format("%Y-%m-%d %H:%M:%S %Z"), "2018-02-17 23:59:59 -03");
+#endif
+   /* After February 2019 Sao Paulo discontinued Daylight
+     * Savings. This test checks to ensure that GncTimeZone doesn't
+     * try to project 2018's rule forward.
+     */
+    GncDate date3{"2019-11-01", "y-m-d"};
+    GncDateTime gncdt3{date3, DayPart::start};
+    EXPECT_EQ(gncdt3.format_zulu("%Y-%m-%d %H:%M:%S %Z"), "2019-11-01 03:00:00 UTC");
+    EXPECT_EQ(gncdt3.format("%Y-%m-%d %H:%M:%S %Z"), "2019-11-01 00:00:00 -03");
+}
+
 TEST(gnc_datetime_constructors, test_gncdate_neutral_constructor)
 {
+#ifdef __MINGW32__
+    TimeZoneProvider tzp_la{"Pacific Standard Time"};
+#else
+    TimeZoneProvider tzp_la("America/Los_Angeles");
+#endif
+    _set_tzp(tzp_la);
     const ymd aymd = { 2017, 04, 20 };
     GncDateTime atime(GncDate(aymd.year, aymd.month, aymd.day), DayPart::neutral);
     time64 date{1492685940};
@@ -448,8 +496,8 @@ TEST(gnc_datetime_constructors, test_gncdate_neutral_constructor)
     if (gncdt.offset() >= max_western_offset &&
         gncdt.offset() <= max_eastern_offset)
     {
-        EXPECT_EQ(atime.format("%d-%m-%Y %H:%M:%S %Z"), "20-04-2017 10:59:00 UTC");
-//        EXPECT_EQ(atime, gncdt);
+        EXPECT_EQ(atime.format_zulu("%d-%m-%Y %H:%M:%S %Z"),
+                  "20-04-2017 10:59:00 UTC");
         EXPECT_EQ(date, static_cast<time64>(gncdt));
         EXPECT_EQ(date, static_cast<time64>(atime));
     }
diff --git a/libgnucash/engine/test/gtest-gnc-timezone.cpp b/libgnucash/engine/test/gtest-gnc-timezone.cpp
index 25d9d9d45..096950e25 100644
--- a/libgnucash/engine/test/gtest-gnc-timezone.cpp
+++ b/libgnucash/engine/test/gtest-gnc-timezone.cpp
@@ -61,6 +61,11 @@ TEST(gnc_timezone_constructors, test_pacific_time_constructor)
     EXPECT_EQ(3, tz->dst_local_start_time (2017).date().month());
     EXPECT_EQ(5, tz->dst_local_end_time (2017).date().day());
     EXPECT_EQ(11, tz->dst_local_end_time (2017).date().month());
+//Check some post-2038 dates to make sure that it works even on macOS.
+    EXPECT_EQ(10, tz->dst_local_start_time (2052).date().day());
+    EXPECT_EQ(3, tz->dst_local_start_time (2052).date().month());
+    EXPECT_EQ(3, tz->dst_local_end_time (2052).date().day());
+    EXPECT_EQ(11, tz->dst_local_end_time (2052).date().month());
 }
 
 #if !PLATFORM(WINDOWS)

commit ebb5eb1f1760b40174f6be99c025f4475f681407
Author: John Ralls <jralls at ceridwen.us>
Date:   Sun Mar 21 13:53:48 2021 -0700

    Fix GncDateTime::format_zulu to emit the UTC timezone.
    
    Instead of the GncDateTime's timezone with the UTC timestamp.

diff --git a/libgnucash/engine/gnc-datetime.cpp b/libgnucash/engine/gnc-datetime.cpp
index bd9a8cd31..2f6d83de1 100644
--- a/libgnucash/engine/gnc-datetime.cpp
+++ b/libgnucash/engine/gnc-datetime.cpp
@@ -517,8 +517,7 @@ GncDateTimeImpl::format_zulu(const char* format) const
     return win_date_format(sformat, utc_tm());
 #else
     using Facet = boost::local_time::local_time_facet;
-    auto offset = m_time.local_time() - m_time.utc_time();
-    auto zulu_time = m_time - offset;
+    auto zulu_time = LDT{m_time.utc_time(), utc_zone};
     auto output_facet(new Facet(normalize_format(format).c_str()));
     std::stringstream ss;
     ss.imbue(std::locale(gnc_get_locale(), output_facet));



Summary of changes:
 libgnucash/engine/gnc-datetime.cpp            | 155 ++++++++++++++------------
 libgnucash/engine/gnc-datetime.hpp            |   8 +-
 libgnucash/engine/test/gtest-gnc-datetime.cpp |  52 ++++++++-
 libgnucash/engine/test/gtest-gnc-timezone.cpp |   5 +
 4 files changed, 142 insertions(+), 78 deletions(-)



More information about the gnucash-changes mailing list