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