mirror of
https://github.com/Gnucash/gnucash.git
synced 2025-02-25 18:55:30 -06:00
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.
This commit is contained in:
@@ -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 (const boost::local_time::time_label_invalid& err)
|
||||
{
|
||||
return LDT_with_pushup(tdate, tdur, tz, false);
|
||||
}
|
||||
|
||||
catch (const boost::local_time::ambiguous_result& err)
|
||||
{
|
||||
return LDT_with_pushup(tdate, tdur, tz, true);
|
||||
}
|
||||
|
||||
catch(boost::gregorian::bad_year&)
|
||||
{
|
||||
throw(std::invalid_argument("Time value is outside the supported year range."));
|
||||
}
|
||||
catch(boost::local_time::time_label_invalid&)
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
throw(std::invalid_argument("Struct tm does not resolve to a valid time."));
|
||||
}
|
||||
catch(boost::local_time::ambiguous_result&)
|
||||
{
|
||||
/* 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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
using TD = boost::posix_time::time_duration;
|
||||
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,50 +328,16 @@ private:
|
||||
friend bool operator!=(const GncDateImpl&, const GncDateImpl&);
|
||||
};
|
||||
|
||||
/* 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{LDT_from_date_daypart(date.m_greg, part,
|
||||
tzp->get(date.m_greg.year()))} {}
|
||||
|
||||
/* Member function definitions for GncDateTimeImpl.
|
||||
*/
|
||||
|
||||
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."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using PTZ = boost::local_time::posix_time_zone;
|
||||
|
||||
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&)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user