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.
This commit is contained in:
John Ralls 2018-11-02 10:29:52 -07:00
parent 19b3643abf
commit 0e723610f0
3 changed files with 69 additions and 13 deletions

View File

@ -52,12 +52,18 @@ using LDTBase = boost::local_time::local_date_time_base<PTime, boost::date_time:
using boost::date_time::not_a_date_time;
using time64 = int64_t;
static const TimeZoneProvider tzp;
static const TimeZoneProvider ltzp;
static const TimeZoneProvider* tzp = &ltzp;
// For converting to/from POSIX time.
static const PTime unix_epoch (Date(1970, boost::gregorian::Jan, 1),
boost::posix_time::seconds(0));
static const TZ_Ptr utc_zone(new boost::local_time::posix_time_zone("UTC-0"));
/* Backdoor to enable unittests to temporarily override the timezone: */
void _set_tzp(TimeZoneProvider& tz);
void _reset_tzp();
/* To ensure things aren't overly screwed up by setting the nanosecond clock for boost::date_time. Don't do it, though, it doesn't get us anything and slows down the date/time library. */
#ifndef BOOST_DATE_TIME_HAS_NANOSECONDS
static constexpr auto ticks_per_second = INT64_C(1000000);
@ -146,7 +152,7 @@ LDT_from_unix_local(const time64 time)
PTime temp(unix_epoch.date(),
boost::posix_time::hours(time / 3600) +
boost::posix_time::seconds(time % 3600));
auto tz = tzp.get(temp.date().year());
auto tz = tzp->get(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 = &ltzp;
}
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<GncDateImpl> 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())
{
if (part == DayPart::neutral)
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,
@ -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."));
}
}
}

View File

@ -23,8 +23,13 @@
\********************************************************************/
#include "../gnc-datetime.hpp"
#include "../gnc-timezone.hpp"
#include <gtest/gtest.h>
/* 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 };

View File

@ -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