123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- """
- Tests for DateOffset additions over Daylight Savings Time
- """
- from datetime import timedelta
- import pytest
- import pytz
- from pandas._libs.tslibs import Timestamp
- from pandas._libs.tslibs.offsets import (
- BMonthBegin,
- BMonthEnd,
- BQuarterBegin,
- BQuarterEnd,
- BYearBegin,
- BYearEnd,
- CBMonthBegin,
- CBMonthEnd,
- CustomBusinessDay,
- DateOffset,
- Day,
- MonthBegin,
- MonthEnd,
- QuarterBegin,
- QuarterEnd,
- SemiMonthBegin,
- SemiMonthEnd,
- Week,
- YearBegin,
- YearEnd,
- )
- from pandas.util.version import Version
- # error: Module has no attribute "__version__"
- pytz_version = Version(pytz.__version__) # type: ignore[attr-defined]
- def get_utc_offset_hours(ts):
- # take a Timestamp and compute total hours of utc offset
- o = ts.utcoffset()
- return (o.days * 24 * 3600 + o.seconds) / 3600.0
- class TestDST:
- # one microsecond before the DST transition
- ts_pre_fallback = "2013-11-03 01:59:59.999999"
- ts_pre_springfwd = "2013-03-10 01:59:59.999999"
- # test both basic names and dateutil timezones
- timezone_utc_offsets = {
- "US/Eastern": {"utc_offset_daylight": -4, "utc_offset_standard": -5},
- "dateutil/US/Pacific": {"utc_offset_daylight": -7, "utc_offset_standard": -8},
- }
- valid_date_offsets_singular = [
- "weekday",
- "day",
- "hour",
- "minute",
- "second",
- "microsecond",
- ]
- valid_date_offsets_plural = [
- "weeks",
- "days",
- "hours",
- "minutes",
- "seconds",
- "milliseconds",
- "microseconds",
- ]
- def _test_all_offsets(self, n, **kwds):
- valid_offsets = (
- self.valid_date_offsets_plural
- if n > 1
- else self.valid_date_offsets_singular
- )
- for name in valid_offsets:
- self._test_offset(offset_name=name, offset_n=n, **kwds)
- def _test_offset(self, offset_name, offset_n, tstart, expected_utc_offset):
- offset = DateOffset(**{offset_name: offset_n})
- t = tstart + offset
- if expected_utc_offset is not None:
- assert get_utc_offset_hours(t) == expected_utc_offset
- if offset_name == "weeks":
- # dates should match
- assert t.date() == timedelta(days=7 * offset.kwds["weeks"]) + tstart.date()
- # expect the same day of week, hour of day, minute, second, ...
- assert (
- t.dayofweek == tstart.dayofweek
- and t.hour == tstart.hour
- and t.minute == tstart.minute
- and t.second == tstart.second
- )
- elif offset_name == "days":
- # dates should match
- assert timedelta(offset.kwds["days"]) + tstart.date() == t.date()
- # expect the same hour of day, minute, second, ...
- assert (
- t.hour == tstart.hour
- and t.minute == tstart.minute
- and t.second == tstart.second
- )
- elif offset_name in self.valid_date_offsets_singular:
- # expect the singular offset value to match between tstart and t
- datepart_offset = getattr(
- t, offset_name if offset_name != "weekday" else "dayofweek"
- )
- assert datepart_offset == offset.kwds[offset_name]
- else:
- # the offset should be the same as if it was done in UTC
- assert t == (tstart.tz_convert("UTC") + offset).tz_convert("US/Pacific")
- def _make_timestamp(self, string, hrs_offset, tz):
- if hrs_offset >= 0:
- offset_string = f"{hrs_offset:02d}00"
- else:
- offset_string = f"-{(hrs_offset * -1):02}00"
- return Timestamp(string + offset_string).tz_convert(tz)
- def test_springforward_plural(self):
- # test moving from standard to daylight savings
- for tz, utc_offsets in self.timezone_utc_offsets.items():
- hrs_pre = utc_offsets["utc_offset_standard"]
- hrs_post = utc_offsets["utc_offset_daylight"]
- self._test_all_offsets(
- n=3,
- tstart=self._make_timestamp(self.ts_pre_springfwd, hrs_pre, tz),
- expected_utc_offset=hrs_post,
- )
- def test_fallback_singular(self):
- # in the case of singular offsets, we don't necessarily know which utc
- # offset the new Timestamp will wind up in (the tz for 1 month may be
- # different from 1 second) so we don't specify an expected_utc_offset
- for tz, utc_offsets in self.timezone_utc_offsets.items():
- hrs_pre = utc_offsets["utc_offset_standard"]
- self._test_all_offsets(
- n=1,
- tstart=self._make_timestamp(self.ts_pre_fallback, hrs_pre, tz),
- expected_utc_offset=None,
- )
- def test_springforward_singular(self):
- for tz, utc_offsets in self.timezone_utc_offsets.items():
- hrs_pre = utc_offsets["utc_offset_standard"]
- self._test_all_offsets(
- n=1,
- tstart=self._make_timestamp(self.ts_pre_springfwd, hrs_pre, tz),
- expected_utc_offset=None,
- )
- offset_classes = {
- MonthBegin: ["11/2/2012", "12/1/2012"],
- MonthEnd: ["11/2/2012", "11/30/2012"],
- BMonthBegin: ["11/2/2012", "12/3/2012"],
- BMonthEnd: ["11/2/2012", "11/30/2012"],
- CBMonthBegin: ["11/2/2012", "12/3/2012"],
- CBMonthEnd: ["11/2/2012", "11/30/2012"],
- SemiMonthBegin: ["11/2/2012", "11/15/2012"],
- SemiMonthEnd: ["11/2/2012", "11/15/2012"],
- Week: ["11/2/2012", "11/9/2012"],
- YearBegin: ["11/2/2012", "1/1/2013"],
- YearEnd: ["11/2/2012", "12/31/2012"],
- BYearBegin: ["11/2/2012", "1/1/2013"],
- BYearEnd: ["11/2/2012", "12/31/2012"],
- QuarterBegin: ["11/2/2012", "12/1/2012"],
- QuarterEnd: ["11/2/2012", "12/31/2012"],
- BQuarterBegin: ["11/2/2012", "12/3/2012"],
- BQuarterEnd: ["11/2/2012", "12/31/2012"],
- Day: ["11/4/2012", "11/4/2012 23:00"],
- }.items()
- @pytest.mark.parametrize("tup", offset_classes)
- def test_all_offset_classes(self, tup):
- offset, test_values = tup
- first = Timestamp(test_values[0], tz="US/Eastern") + offset()
- second = Timestamp(test_values[1], tz="US/Eastern")
- assert first == second
- @pytest.mark.parametrize(
- "original_dt, target_dt, offset, tz",
- [
- pytest.param(
- Timestamp("1900-01-01"),
- Timestamp("1905-07-01"),
- MonthBegin(66),
- "Africa/Kinshasa",
- marks=pytest.mark.xfail(
- pytz_version < Version("2020.5") or pytz_version == Version("2022.2"),
- reason="GH#41906: pytz utc transition dates changed",
- ),
- ),
- (
- Timestamp("2021-10-01 01:15"),
- Timestamp("2021-10-31 01:15"),
- MonthEnd(1),
- "Europe/London",
- ),
- (
- Timestamp("2010-12-05 02:59"),
- Timestamp("2010-10-31 02:59"),
- SemiMonthEnd(-3),
- "Europe/Paris",
- ),
- (
- Timestamp("2021-10-31 01:20"),
- Timestamp("2021-11-07 01:20"),
- CustomBusinessDay(2, weekmask="Sun Mon"),
- "US/Eastern",
- ),
- (
- Timestamp("2020-04-03 01:30"),
- Timestamp("2020-11-01 01:30"),
- YearBegin(1, month=11),
- "America/Chicago",
- ),
- ],
- )
- def test_nontick_offset_with_ambiguous_time_error(original_dt, target_dt, offset, tz):
- # .apply for non-Tick offsets throws AmbiguousTimeError when the target dt
- # is dst-ambiguous
- localized_dt = original_dt.tz_localize(tz)
- msg = f"Cannot infer dst time from {target_dt}, try using the 'ambiguous' argument"
- with pytest.raises(pytz.AmbiguousTimeError, match=msg):
- localized_dt + offset
|