Merge branch 'tz-fixes'

Miscellaneous fixes for time zones and DST handling in wxDateTime.

This still leaves 2 big problems:

1. We have no support for using the correct time zone offset at the
   given date and always use the current time zone offset, which may,
   and often is, wrong.

2. Our code for converting to/from broken down representation doesn't
   handle DST at all, so support for DST is non-existent for the dates
   before 1970-01-01 or after 2038-01-01 (i.e. roughly outside of the
   32 bit time_t range).

See #10445 and the other tickets linked from there.
This commit is contained in:
Vadim Zeitlin
2017-12-02 16:28:05 +01:00
8 changed files with 215 additions and 133 deletions

View File

@@ -306,7 +306,9 @@ public:
return tz;
}
long GetOffset() const { return m_offset; }
bool IsLocal() const { return m_offset == -1; }
long GetOffset() const;
private:
// offset for this timezone from GMT in seconds

View File

@@ -158,7 +158,11 @@ public:
line = line_;
component = component_;
timestamp = time(NULL);
// don't initialize the timestamp yet, we might not need it at all if
// the message doesn't end up being logged and otherwise we'll fill it
// just before logging it, which won't change it by much and definitely
// less than a second resolution of the timestamp
timestamp = 0;
#if wxUSE_THREADS
threadId = wxThread::GetCurrentId();
@@ -1162,6 +1166,11 @@ private:
void DoCallOnLog(wxLogLevel level, const wxString& format, va_list argptr)
{
// As explained in wxLogRecordInfo ctor, we don't initialize its
// timestamp to avoid calling time() unnecessary, but now that we are
// about to log the message, we do need to do it.
m_info.timestamp = time(NULL);
wxLog::OnLog(level, wxString::FormatV(format, argptr), m_info);
}

View File

@@ -254,6 +254,17 @@ public:
/// Create a time zone with the given offset in seconds.
static TimeZone Make(long offset);
/**
Return true if this is the local time zone.
This method can be useful for distinguishing between UTC time zone
and local time zone in Great Britain, which use the same offset as
UTC (i.e. 0), but do use DST.
@since 3.1.1
*/
bool IsLocal() const;
/// Return the offset of this time zone from UTC, in seconds.
long GetOffset() const;
};
@@ -1237,6 +1248,10 @@ public:
for more information about time zones. Normally, these functions should
be rarely used.
Note that all functions in this section always use the current offset
for the specified time zone and don't take into account its possibly
different historical value at the given date.
Related functions in other groups: GetBeginDST(), GetEndDST()
*/
//@{
@@ -1246,10 +1261,7 @@ public:
If @a noDST is @true, no DST adjustments will be made.
Notice using wxDateTime::Local for @a tz parameter doesn't really make
sense and may result in unexpected results as it will return a
different object when DST is in use and @a noDST has its default value
of @false.
If @a tz parameter is wxDateTime::Local, no adjustment is performed.
@return The date adjusted by the different between the given and the
local time zones.
@@ -1286,9 +1298,7 @@ public:
If @a noDST is @true, no DST adjustments will be made.
Notice that, as with FromTimezone(), using wxDateTime::Local as @a tz
doesn't really make sense and may return a different object when DST is
in effect and @a noDST is @false.
If @a tz parameter is wxDateTime::Local, no adjustment is performed.
@return The date adjusted by the different between the local and the
given time zones.

View File

@@ -341,6 +341,33 @@ void wxInitTm(struct tm& tm)
tm.tm_isdst = -1; // auto determine
}
// Internal helper function called only for times outside of standard time_t
// range.
//
// It is just a hack to work around the fact that we can't call IsDST() and
// related methods from GetTm() for the reasons explained there.
static int GetDSTOffset(wxLongLong t)
{
bool isDST = false;
switch ( wxDateTime::GetCountry() )
{
case wxDateTime::UK:
// We don't need to check for the end value in 1971 as this is
// inside the standard range, so check just for beginning of the
// permanent BST period in UK, see IsDST().
if ( t < 0 &&
t >= wxDateTime(27, wxDateTime::Oct, 1968).GetValue() )
isDST = true;
break;
default:
break;
}
return isDST ? wxDateTime::DST_OFFSET : 0;
}
// ============================================================================
// implementation of wxDateTime
// ============================================================================
@@ -456,9 +483,8 @@ wxDateTime::TimeZone::TimeZone(wxDateTime::TZ tz)
switch ( tz )
{
case wxDateTime::Local:
// get the offset from C RTL: it returns the difference GMT-local
// while we want to have the offset _from_ GMT, hence the '-'
m_offset = -wxGetTimeZone();
// Use a special value for local time zone.
m_offset = -1;
break;
case wxDateTime::GMT_12:
@@ -503,6 +529,13 @@ wxDateTime::TimeZone::TimeZone(wxDateTime::TZ tz)
}
}
long wxDateTime::TimeZone::GetOffset() const
{
// get the offset from C RTL: it returns the difference GMT-local
// while we want to have the offset _from_ GMT, hence the '-'
return m_offset == -1 ? -wxGetTimeZone() : m_offset;
}
// ----------------------------------------------------------------------------
// static functions
// ----------------------------------------------------------------------------
@@ -855,7 +888,8 @@ wxDateTime::Country wxDateTime::GetCountry()
struct tm *tm = wxLocaltime_r(&t, &tmstruct);
wxString tz = wxCallStrftime(wxS("%Z"), tm);
if ( tz == wxT("WET") || tz == wxT("WEST") )
if ( tz == wxT("WET") || tz == wxT("WEST") ||
tz == wxT("BST") || tz == wxT("GMT") )
{
ms_country = UK;
}
@@ -1430,6 +1464,24 @@ unsigned long wxDateTime::GetAsDOS() const
// time_t <-> broken down time conversions
// ----------------------------------------------------------------------------
const tm* wxTryGetTm(tm& tmstruct, time_t t, const wxDateTime::TimeZone& tz)
{
if ( tz.IsLocal() )
{
// we are working with local time
return wxLocaltime_r(&t, &tmstruct);
}
else
{
t += (time_t)tz.GetOffset();
#if !defined(__VMS__) // time is unsigned so avoid warning
if ( t < 0 )
return NULL;
#endif
return wxGmtime_r(&t, &tmstruct);
}
}
wxDateTime::Tm wxDateTime::GetTm(const TimeZone& tz) const
{
wxASSERT_MSG( IsValid(), wxT("invalid wxDateTime") );
@@ -1437,39 +1489,9 @@ wxDateTime::Tm wxDateTime::GetTm(const TimeZone& tz) const
time_t time = GetTicks();
if ( time != (time_t)-1 )
{
// use C RTL functions
// Try to use the RTL.
struct tm tmstruct;
tm *tm;
if ( tz.GetOffset() == -wxGetTimeZone() )
{
// we are working with local time
tm = wxLocaltime_r(&time, &tmstruct);
// should never happen
wxCHECK_MSG( tm, Tm(), wxT("wxLocaltime_r() failed") );
}
else
{
time += (time_t)tz.GetOffset();
#if defined(__VMS__) // time is unsigned so avoid warning
int time2 = (int) time;
if ( time2 >= 0 )
#else
if ( time >= 0 )
#endif
{
tm = wxGmtime_r(&time, &tmstruct);
// should never happen
wxCHECK_MSG( tm, Tm(), wxT("wxGmtime_r() failed") );
}
else
{
tm = (struct tm *)NULL;
}
}
if ( tm )
if ( const tm* tm = wxTryGetTm(tmstruct, time, tz) )
{
// adjust the milliseconds
Tm tm2(*tm, tz);
@@ -1480,11 +1502,21 @@ wxDateTime::Tm wxDateTime::GetTm(const TimeZone& tz) const
//else: use generic code below
}
long secDiff = tz.GetOffset();
// We need to account for DST as always when converting to broken down time
// components, but we can't call IsDST() from here because this would
// result in infinite recursion as IsDST() starts by calling GetYear()
// which just calls back to this function. So call a special function which
// is used just here to determine the DST offset to add.
if ( tz.IsLocal() )
secDiff += GetDSTOffset(m_time);
wxLongLong timeMidnight = m_time + secDiff * 1000;
// remember the time and do the calculations with the date only - this
// eliminates rounding errors of the floating point arithmetics
wxLongLong timeMidnight = m_time + tz.GetOffset() * 1000;
long timeOnly = (timeMidnight % MILLISECONDS_PER_DAY).ToLong();
// we want to always have positive time and timeMidnight to be really
@@ -2086,10 +2118,28 @@ int wxDateTime::IsDST(wxDateTime::Country country) const
{
int year = GetYear();
if ( !IsDSTApplicable(year, country) )
country = GetCountry();
switch ( country )
{
// no DST time in this year in this country
return -1;
case UK:
// There is a special, but important, case of UK which was
// permanently on BST, i.e. using DST, during this period. It
// is important because it covers Unix epoch and without
// accounting for the DST during it, various tests done around
// the epoch time would fail in BST time zone (only!).
if ( IsEarlierThan(wxDateTime(31, Oct, 1971)) &&
IsLaterThan(wxDateTime(27, Oct, 1968)) )
{
return true;
}
wxFALLTHROUGH;
default:
if ( !IsDSTApplicable(year, country) )
{
// no DST time in this year in this country
return -1;
}
}
return IsBetween(GetBeginDST(year, country), GetEndDST(year, country));
@@ -2100,11 +2150,11 @@ wxDateTime& wxDateTime::MakeTimezone(const TimeZone& tz, bool noDST)
{
long secDiff = wxGetTimeZone() + tz.GetOffset();
// We are converting from the local time, but local time zone does not
// include the DST offset (as it varies depending on the date), so we have
// to handle DST manually, unless a special flag inhibiting this was
// specified.
if ( !noDST && (IsDST() == 1) )
// We are converting from the local time to some other time zone, but local
// time zone does not include the DST offset (as it varies depending on the
// date), so we have to handle DST manually, unless a special flag
// inhibiting this was specified.
if ( !noDST && (IsDST() == 1) && !tz.IsLocal() )
{
secDiff -= DST_OFFSET;
}
@@ -2117,7 +2167,7 @@ wxDateTime& wxDateTime::MakeFromTimezone(const TimeZone& tz, bool noDST)
long secDiff = wxGetTimeZone() + tz.GetOffset();
// See comment in MakeTimezone() above, the logic here is exactly the same.
if ( !noDST && (IsDST() == 1) )
if ( !noDST && (IsDST() == 1) && !tz.IsLocal() )
{
secDiff -= DST_OFFSET;
}

View File

@@ -65,6 +65,7 @@
// ----------------------------------------------------------------------------
extern void wxInitTm(struct tm& tm);
extern const tm* wxTryGetTm(tm& tmstruct, time_t t, const wxDateTime::TimeZone& tz);
extern wxString wxCallStrftime(const wxString& format, const tm* tm);
@@ -367,40 +368,9 @@ wxString wxDateTime::Format(const wxString& formatp, const TimeZone& tz) const
if ( canUseStrftime )
{
// use strftime()
// Try using strftime()
struct tm tmstruct;
struct tm *tm;
if ( tz.GetOffset() == -wxGetTimeZone() )
{
// we are working with local time
tm = wxLocaltime_r(&time, &tmstruct);
// should never happen
wxCHECK_MSG( tm, wxEmptyString, wxT("wxLocaltime_r() failed") );
}
else
{
time += (int)tz.GetOffset();
#if defined(__VMS__) // time is unsigned so avoid warning
int time2 = (int) time;
if ( time2 >= 0 )
#else
if ( time >= 0 )
#endif
{
tm = wxGmtime_r(&time, &tmstruct);
// should never happen
wxCHECK_MSG( tm, wxEmptyString, wxT("wxGmtime_r() failed") );
}
else
{
tm = (struct tm *)NULL;
}
}
if ( tm )
if ( const tm* tm = wxTryGetTm(tmstruct, time, tz) )
{
return wxCallStrftime(format, tm);
}

View File

@@ -24,8 +24,6 @@
#include "wx/wxcrt.h" // for wxStrstr()
#include "testdate.h"
// to test Today() meaningfully we must be able to change the system date which
// is not usually the case, but if we're under Win32 we can try it -- define
// the macro below to do it
@@ -301,15 +299,13 @@ void DateTimeTestCase::TestTimeSet()
for ( size_t n = 0; n < WXSIZEOF(testDates); n++ )
{
const Date& d1 = testDates[n];
wxDateTime dt = d1.DT();
const wxDateTime dt = d1.DT();
Date d2;
d2.Init(dt.GetTm());
wxString s1 = d1.Format(),
s2 = d2.Format();
CPPUNIT_ASSERT_EQUAL( s1, s2 );
INFO("n=" << n);
CHECK( d1.Format() == d2.Format() );
}
}
@@ -324,10 +320,11 @@ void DateTimeTestCase::TestTimeJDN()
// JDNs must be computed for UTC times
double jdn = dt.FromUTC().GetJulianDayNumber();
CPPUNIT_ASSERT_EQUAL( d.jdn, jdn );
INFO("n=" << n);
CHECK( d.jdn == jdn );
dt.Set(jdn);
CPPUNIT_ASSERT_EQUAL( jdn, dt.GetJulianDayNumber() );
CHECK( jdn == dt.GetJulianDayNumber() );
}
}
@@ -341,8 +338,10 @@ void DateTimeTestCase::TestTimeWDays()
const Date& d = testDates[n];
wxDateTime dt(d.day, d.month, d.year, d.hour, d.min, d.sec);
INFO("n=" << n);
wxDateTime::WeekDay wday = dt.GetWeekDay();
CPPUNIT_ASSERT_EQUAL( d.wday, wday );
CHECK( d.wday == wday );
}
// test SetToWeekDay()
@@ -665,6 +664,15 @@ void DateTimeTestCase::TestTimeFormat()
CompareTime // time only
};
const char* const compareKindStrings[] =
{
"nothing",
"both date and time",
"both date and time but without century",
"only dates",
"only times",
};
static const struct
{
CompareKind compareKind;
@@ -683,7 +691,7 @@ void DateTimeTestCase::TestTimeFormat()
const long timeZonesOffsets[] =
{
wxDateTime::TimeZone(wxDateTime::Local).GetOffset(),
-1, // This is pseudo-offset used for local time zone
// Fictitious TimeZone offsets to ensure time zone formating and
// interpretation works
@@ -712,7 +720,7 @@ void DateTimeTestCase::TestTimeFormat()
for ( unsigned idxtz = 0; idxtz < WXSIZEOF(timeZonesOffsets); ++idxtz )
{
wxDateTime::TimeZone tz(timeZonesOffsets[idxtz]);
const bool isLocalTz = tz.GetOffset() == -wxGetTimeZone();
const bool isLocalTz = tz.IsLocal();
for ( size_t d = 0; d < WXSIZEOF(formatTestDates); d++ )
{
@@ -753,6 +761,21 @@ void DateTimeTestCase::TestTimeFormat()
// do convert date to string
wxString s = dt.Format(fmt, tz);
// Normally, passing time zone to Format() should have exactly
// the same effect as converting to this time zone before
// calling it, however the former may use standard library date
// handling in strftime() implementation while the latter
// always uses our own code and they may disagree if the offset
// for this time zone has changed since the given date, as the
// standard library handles it correctly (at least under Unix),
// while our code doesn't handle time zone changes at all.
//
// Short of implementing full support for time zone database,
// we can't really do anything about this other than skipping
// the test in this case.
if ( s != dt.ToTimezone(tz).Format(fmt) )
continue;
// convert back
wxDateTime dt2;
const char *result = dt2.ParseFormat(s, fmt);
@@ -785,13 +808,16 @@ void DateTimeTestCase::TestTimeFormat()
if ( !strstr(fmt, "%z") && !isLocalTz )
dt2.MakeFromTimezone(tz);
INFO("Comparing " << compareKindStrings[kind] << " for "
<< dt << " with " << dt2
<< " (format result=\"" << s << "\")");
switch ( kind )
{
case CompareYear:
if ( dt2.GetCentury() != dt.GetCentury() )
{
CPPUNIT_ASSERT_EQUAL(dt.GetYear() % 100,
dt2.GetYear() % 100);
CHECK( dt.GetYear() % 100 == dt2.GetYear() % 100);
dt2.SetYear(dt.GetYear());
}
@@ -799,15 +825,15 @@ void DateTimeTestCase::TestTimeFormat()
wxFALLTHROUGH;
case CompareBoth:
CPPUNIT_ASSERT_EQUAL( dt, dt2 );
CHECK( dt == dt2 );
break;
case CompareDate:
CPPUNIT_ASSERT( dt.IsSameDate(dt2) );
CHECK( dt.IsSameDate(dt2) );
break;
case CompareTime:
CPPUNIT_ASSERT( dt.IsSameTime(dt2) );
CHECK( dt.IsSameTime(dt2) );
break;
case CompareNone:
@@ -993,31 +1019,18 @@ void DateTimeTestCase::TestTimeSpanFormat()
void DateTimeTestCase::TestTimeTicks()
{
static const wxDateTime::TimeZone TZ_LOCAL(wxDateTime::Local);
static const wxDateTime::TimeZone TZ_TEST(wxDateTime::NZST);
// this offset is needed to make the test work in any time zone when we
// only have expected test results in UTC in testDates
static const long tzOffset = TZ_LOCAL.GetOffset() - TZ_TEST.GetOffset();
for ( size_t n = 0; n < WXSIZEOF(testDates); n++ )
{
const Date& d = testDates[n];
if ( d.gmticks == -1 )
continue;
wxDateTime dt = d.DT().MakeTimezone(TZ_TEST, true /* no DST */);
const wxDateTime dt = d.DT().FromTimezone(wxDateTime::UTC);
// GetValue() returns internal UTC-based representation, we need to
// convert it to local TZ before comparing
time_t ticks = (dt.GetValue() / 1000).ToLong() + TZ_LOCAL.GetOffset();
if ( dt.IsDST() )
ticks += 3600;
CPPUNIT_ASSERT_EQUAL( d.gmticks, ticks + tzOffset );
INFO("n=" << n);
dt = d.DT().FromTimezone(wxDateTime::UTC);
ticks = (dt.GetValue() / 1000).ToLong();
CPPUNIT_ASSERT_EQUAL( d.gmticks, ticks );
time_t ticks = (dt.GetValue() / 1000).ToLong();
CHECK( d.gmticks == ticks );
}
}
@@ -1565,21 +1578,20 @@ void DateTimeTestCase::TestTranslateFromUnicodeFormat()
void DateTimeTestCase::TestConvToFromLocalTZ()
{
// Choose a date when the DST is on in many time zones: in this case,
// converting to/from local TZ does modify the object because it
// adds/subtracts DST to/from it, so to get the expected results we need to
// explicitly disable DST support in these functions.
// Choose a date when the DST is on in many time zones and verify that
// converting from/to local time zone still doesn't modify time in this
// case as this used to be broken.
wxDateTime dt(18, wxDateTime::Apr, 2017, 19);
CPPUNIT_ASSERT_EQUAL( dt.FromTimezone(wxDateTime::Local, true), dt );
CPPUNIT_ASSERT_EQUAL( dt.ToTimezone(wxDateTime::Local, true), dt );
CHECK( dt.FromTimezone(wxDateTime::Local) == dt );
CHECK( dt.ToTimezone(wxDateTime::Local) == dt );
// And another one when it is off: in this case, there is no need to pass
// "true" as "noDST" argument to these functions.
// For a date when the DST is not used, this always worked, but still
// verify that it continues to.
dt = wxDateTime(18, wxDateTime::Jan, 2018, 19);
CPPUNIT_ASSERT_EQUAL( dt.FromTimezone(wxDateTime::Local), dt );
CPPUNIT_ASSERT_EQUAL( dt.ToTimezone(wxDateTime::Local), dt );
CHECK( dt.FromTimezone(wxDateTime::Local) == dt );
CHECK( dt.ToTimezone(wxDateTime::Local) == dt );
}
static void DoTestSetFunctionsOnDST(const wxDateTime &orig)
@@ -1638,4 +1650,27 @@ TEST_CASE("wxDateTime::SetOnDST", "[datetime][dst]")
}
}
// Tests random problems that used to appear in BST time zone during DST.
// This test is disabled by default as it only passes in BST time zone, due to
// the times hard-coded in it.
TEST_CASE("wxDateTime-BST-bugs", "[datetime][dst][BST][.]")
{
SECTION("bug-17220")
{
wxDateTime dt;
dt.Set(22, wxDateTime::Oct, 2015, 10, 10, 10, 10);
REQUIRE( dt.IsDST() );
CHECK( dt.GetTm().hour == 10 );
CHECK( dt.GetTm(wxDateTime::UTC).hour == 9 );
CHECK( dt.Format("%Y-%m-%d %H:%M:%S", wxDateTime::Local ) == "2015-10-22 10:10:10" );
CHECK( dt.Format("%Y-%m-%d %H:%M:%S", wxDateTime::UTC ) == "2015-10-22 09:10:10" );
dt.MakeFromUTC();
CHECK( dt.Format("%Y-%m-%d %H:%M:%S", wxDateTime::Local ) == "2015-10-22 11:10:10" );
CHECK( dt.Format("%Y-%m-%d %H:%M:%S", wxDateTime::UTC ) == "2015-10-22 10:10:10" );
}
}
#endif // wxUSE_DATETIME

View File

@@ -11,6 +11,8 @@
#include "wx/datetime.h"
#include <ostream>
// need this to be able to use CPPUNIT_ASSERT_EQUAL with wxDateTime objects
inline std::ostream& operator<<(std::ostream& ostr, const wxDateTime& dt)
{

View File

@@ -4,6 +4,10 @@
#include "wx/wxprec.h"
#include "wx/stopwatch.h"
#include "wx/evtloop.h"
// This needs to be included before catch.hpp to be taken into account.
#include "testdate.h"
#include "wx/catch_cppunit.h"
// Custom test macro that is only defined when wxUIActionSimulator is available