|
9 | 9 | import tempfile
|
10 | 10 |
|
11 | 11 | import dateutil
|
| 12 | +import pytz |
| 13 | + |
12 | 14 | try:
|
13 | 15 | # mock in python 3.3+
|
14 | 16 | from unittest import mock
|
@@ -355,6 +357,78 @@ def test_date_inverted_limit():
|
355 | 357 | fig.subplots_adjust(left=0.25)
|
356 | 358 |
|
357 | 359 |
|
| 360 | +def test_date2num_dst(): |
| 361 | + # Test for github issue #3896, but in date2num around DST transitions |
| 362 | + # with a timezone-aware pandas date_range object. |
| 363 | + |
| 364 | + class dt_tzaware(datetime.datetime): |
| 365 | + """ |
| 366 | + This bug specifically occurs because of the normalization behavior of |
| 367 | + pandas Timestamp objects, so in order to replicate it, we need a |
| 368 | + datetime-like object that applies timezone normalization after |
| 369 | + subtraction. |
| 370 | + """ |
| 371 | + def __sub__(self, other): |
| 372 | + r = super(dt_tzaware, self).__sub__(other) |
| 373 | + tzinfo = getattr(r, 'tzinfo', None) |
| 374 | + |
| 375 | + if tzinfo is not None: |
| 376 | + localizer = getattr(tzinfo, 'normalize', None) |
| 377 | + if localizer is not None: |
| 378 | + r = tzinfo.normalize(r) |
| 379 | + |
| 380 | + if isinstance(r, datetime.datetime): |
| 381 | + r = self.mk_tzaware(r) |
| 382 | + |
| 383 | + return r |
| 384 | + |
| 385 | + def __add__(self, other): |
| 386 | + return self.mk_tzaware(super(dt_tzaware, self).__add__(other)) |
| 387 | + |
| 388 | + def astimezone(self, tzinfo): |
| 389 | + dt = super(dt_tzaware, self).astimezone(tzinfo) |
| 390 | + return self.mk_tzaware(dt) |
| 391 | + |
| 392 | + @classmethod |
| 393 | + def mk_tzaware(cls, datetime_obj): |
| 394 | + kwargs = {} |
| 395 | + attrs = ('year', |
| 396 | + 'month', |
| 397 | + 'day', |
| 398 | + 'hour', |
| 399 | + 'minute', |
| 400 | + 'second', |
| 401 | + 'microsecond', |
| 402 | + 'tzinfo') |
| 403 | + |
| 404 | + for attr in attrs: |
| 405 | + val = getattr(datetime_obj, attr, None) |
| 406 | + if val is not None: |
| 407 | + kwargs[attr] = val |
| 408 | + |
| 409 | + return cls(**kwargs) |
| 410 | + |
| 411 | + # Timezones |
| 412 | + BRUSSELS = pytz.timezone('Europe/Brussels') |
| 413 | + UTC = pytz.UTC |
| 414 | + |
| 415 | + # Create a list of timezone-aware datetime objects in UTC |
| 416 | + # Interval is 0b0.0000011 days, to prevent float rounding issues |
| 417 | + dtstart = dt_tzaware(2014, 3, 30, 0, 0, tzinfo=UTC) |
| 418 | + interval = datetime.timedelta(minutes=33, seconds=45) |
| 419 | + interval_days = 0.0234375 # 2025 / 86400 seconds |
| 420 | + N = 8 |
| 421 | + |
| 422 | + dt_utc = [dtstart + i * interval for i in range(N)] |
| 423 | + dt_bxl = [d.astimezone(BRUSSELS) for d in dt_utc] |
| 424 | + |
| 425 | + expected_ordinalf = [735322.0 + (i * interval_days) for i in range(N)] |
| 426 | + |
| 427 | + actual_ordinalf = list(mdates.date2num(dt_bxl)) |
| 428 | + |
| 429 | + assert_equal(actual_ordinalf, expected_ordinalf) |
| 430 | + |
| 431 | + |
358 | 432 | if __name__ == '__main__':
|
359 | 433 | import nose
|
360 | 434 | nose.runmodule(argv=['-s', '--with-doctest'], exit=False)
|
0 commit comments