Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 0a31b36

Browse filesBrowse files
authored
Merge pull request #6262 from pganssle/fix_date2num_dst
FIX: Properly handle UTC conversion in date2num
2 parents 38a32d9 + 3e6e8d1 commit 0a31b36
Copy full SHA for 0a31b36

File tree

Expand file treeCollapse file tree

2 files changed

+119
-33
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+119
-33
lines changed

‎lib/matplotlib/dates.py

Copy file name to clipboardExpand all lines: lib/matplotlib/dates.py
+17-33Lines changed: 17 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -212,47 +212,31 @@ def _to_ordinalf(dt):
212212
days, preserving hours, minutes, seconds and microseconds. Return value
213213
is a :func:`float`.
214214
"""
215-
216-
if hasattr(dt, 'tzinfo') and dt.tzinfo is not None:
217-
delta = dt.tzinfo.utcoffset(dt)
218-
if delta is not None:
219-
dt -= delta
215+
# Convert to UTC
216+
tzi = getattr(dt, 'tzinfo', None)
217+
if tzi is not None:
218+
dt = dt.astimezone(UTC)
219+
tzi = UTC
220220

221221
base = float(dt.toordinal())
222-
if isinstance(dt, datetime.datetime):
223-
# Get a datetime object at midnight in the same time zone as dt.
224-
cdate = dt.date()
225-
midnight_time = datetime.time(0, 0, 0, tzinfo=dt.tzinfo)
222+
223+
# If it's sufficiently datetime-like, it will have a `date()` method
224+
cdate = getattr(dt, 'date', lambda: None)()
225+
if cdate is not None:
226+
# Get a datetime object at midnight UTC
227+
midnight_time = datetime.time(0, tzinfo=tzi)
226228

227229
rdt = datetime.datetime.combine(cdate, midnight_time)
228-
td_remainder = _total_seconds(dt - rdt)
229230

230-
if td_remainder > 0:
231-
base += td_remainder / SEC_PER_DAY
231+
# Append the seconds as a fraction of a day
232+
base += (dt - rdt).total_seconds() / SEC_PER_DAY
232233

233234
return base
234235

235236

236237
# a version of _to_ordinalf that can operate on numpy arrays
237238
_to_ordinalf_np_vectorized = np.vectorize(_to_ordinalf)
238239

239-
try:
240-
# Available as a native method in Python >= 2.7.
241-
_total_seconds = datetime.timedelta.total_seconds
242-
except AttributeError:
243-
def _total_seconds(tdelta):
244-
"""
245-
Alias providing support for datetime.timedelta.total_seconds() function
246-
calls even in Python < 2.7.
247-
248-
The input `tdelta` is a datetime.timedelta object, and returns a float
249-
containing the total number of seconds representing the `tdelta`
250-
duration. For large durations (> 270 on most platforms), this loses
251-
microsecond accuracy.
252-
"""
253-
return (tdelta.microseconds +
254-
(tdelta.seconds + tdelta.days * SEC_PER_DAY) * 1e6) * 1e-6
255-
256240

257241
def _from_ordinalf(x, tz=None):
258242
"""
@@ -432,7 +416,7 @@ def drange(dstart, dend, delta):
432416
"""
433417
f1 = _to_ordinalf(dstart)
434418
f2 = _to_ordinalf(dend)
435-
step = _total_seconds(delta) / SEC_PER_DAY
419+
step = delta.total_seconds() / SEC_PER_DAY
436420

437421
# calculate the difference between dend and dstart in times of delta
438422
num = int(np.ceil((f2 - f1) / step))
@@ -1065,8 +1049,8 @@ def get_locator(self, dmin, dmax):
10651049
numDays = tdelta.days # Avoids estimates of days/month, days/year
10661050
numHours = (numDays * HOURS_PER_DAY) + delta.hours
10671051
numMinutes = (numHours * MIN_PER_HOUR) + delta.minutes
1068-
numSeconds = np.floor(_total_seconds(tdelta))
1069-
numMicroseconds = np.floor(_total_seconds(tdelta) * 1e6)
1052+
numSeconds = np.floor(tdelta.total_seconds())
1053+
numMicroseconds = np.floor(tdelta.total_seconds() * 1e6)
10701054

10711055
nums = [numYears, numMonths, numDays, numHours, numMinutes,
10721056
numSeconds, numMicroseconds]
@@ -1406,7 +1390,7 @@ def _close_to_dt(d1, d2, epsilon=5):
14061390
Assert that datetimes *d1* and *d2* are within *epsilon* microseconds.
14071391
"""
14081392
delta = d2 - d1
1409-
mus = abs(_total_seconds(delta) * 1e6)
1393+
mus = abs(delta.total_seconds() * 1e6)
14101394
assert mus < epsilon
14111395

14121396

‎lib/matplotlib/tests/test_dates.py

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_dates.py
+102Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99
import tempfile
1010

1111
import dateutil
12+
import pytz
13+
1214
try:
1315
# mock in python 3.3+
1416
from unittest import mock
1517
except ImportError:
1618
import mock
1719
from nose.tools import assert_raises, assert_equal
20+
from nose.plugins.skip import SkipTest
1821

1922
from matplotlib.testing.decorators import image_comparison, cleanup
2023
import matplotlib.pyplot as plt
@@ -355,6 +358,105 @@ def test_date_inverted_limit():
355358
fig.subplots_adjust(left=0.25)
356359

357360

361+
def _test_date2num_dst(date_range, tz_convert):
362+
# Timezones
363+
BRUSSELS = pytz.timezone('Europe/Brussels')
364+
UTC = pytz.UTC
365+
366+
# Create a list of timezone-aware datetime objects in UTC
367+
# Interval is 0b0.0000011 days, to prevent float rounding issues
368+
dtstart = datetime.datetime(2014, 3, 30, 0, 0, tzinfo=UTC)
369+
interval = datetime.timedelta(minutes=33, seconds=45)
370+
interval_days = 0.0234375 # 2025 / 86400 seconds
371+
N = 8
372+
373+
dt_utc = date_range(start=dtstart, freq=interval, periods=N)
374+
dt_bxl = tz_convert(dt_utc, BRUSSELS)
375+
376+
expected_ordinalf = [735322.0 + (i * interval_days) for i in range(N)]
377+
actual_ordinalf = list(mdates.date2num(dt_bxl))
378+
379+
assert_equal(actual_ordinalf, expected_ordinalf)
380+
381+
382+
def test_date2num_dst():
383+
# Test for github issue #3896, but in date2num around DST transitions
384+
# with a timezone-aware pandas date_range object.
385+
386+
class dt_tzaware(datetime.datetime):
387+
"""
388+
This bug specifically occurs because of the normalization behavior of
389+
pandas Timestamp objects, so in order to replicate it, we need a
390+
datetime-like object that applies timezone normalization after
391+
subtraction.
392+
"""
393+
def __sub__(self, other):
394+
r = super(dt_tzaware, self).__sub__(other)
395+
tzinfo = getattr(r, 'tzinfo', None)
396+
397+
if tzinfo is not None:
398+
localizer = getattr(tzinfo, 'normalize', None)
399+
if localizer is not None:
400+
r = tzinfo.normalize(r)
401+
402+
if isinstance(r, datetime.datetime):
403+
r = self.mk_tzaware(r)
404+
405+
return r
406+
407+
def __add__(self, other):
408+
return self.mk_tzaware(super(dt_tzaware, self).__add__(other))
409+
410+
def astimezone(self, tzinfo):
411+
dt = super(dt_tzaware, self).astimezone(tzinfo)
412+
return self.mk_tzaware(dt)
413+
414+
@classmethod
415+
def mk_tzaware(cls, datetime_obj):
416+
kwargs = {}
417+
attrs = ('year',
418+
'month',
419+
'day',
420+
'hour',
421+
'minute',
422+
'second',
423+
'microsecond',
424+
'tzinfo')
425+
426+
for attr in attrs:
427+
val = getattr(datetime_obj, attr, None)
428+
if val is not None:
429+
kwargs[attr] = val
430+
431+
return cls(**kwargs)
432+
433+
# Define a date_range function similar to pandas.date_range
434+
def date_range(start, freq, periods):
435+
dtstart = dt_tzaware.mk_tzaware(start)
436+
437+
return [dtstart + (i * freq) for i in range(periods)]
438+
439+
# Define a tz_convert function that converts a list to a new time zone.
440+
def tz_convert(dt_list, tzinfo):
441+
return [d.astimezone(tzinfo) for d in dt_list]
442+
443+
_test_date2num_dst(date_range, tz_convert)
444+
445+
446+
def test_date2num_dst_pandas():
447+
# Test for github issue #3896, but in date2num around DST transitions
448+
# with a timezone-aware pandas date_range object.
449+
try:
450+
import pandas as pd
451+
except ImportError:
452+
raise SkipTest('pandas not installed')
453+
454+
def tz_convert(*args):
455+
return pd.DatetimeIndex.tz_convert(*args).astype(datetime.datetime)
456+
457+
_test_date2num_dst(pd.date_range, tz_convert)
458+
459+
358460
if __name__ == '__main__':
359461
import nose
360462
nose.runmodule(argv=['-s', '--with-doctest'], exit=False)

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.