494 lines
17 KiB
Python
494 lines
17 KiB
Python
"""
|
|
Tests for Timestamp timezone-related methods
|
|
"""
|
|
from datetime import (
|
|
date,
|
|
datetime,
|
|
timedelta,
|
|
timezone,
|
|
)
|
|
import re
|
|
|
|
import dateutil
|
|
from dateutil.tz import (
|
|
gettz,
|
|
tzoffset,
|
|
)
|
|
import pytest
|
|
import pytz
|
|
from pytz.exceptions import (
|
|
AmbiguousTimeError,
|
|
NonExistentTimeError,
|
|
)
|
|
|
|
from pandas._libs.tslibs import timezones
|
|
from pandas._libs.tslibs.dtypes import NpyDatetimeUnit
|
|
from pandas.errors import OutOfBoundsDatetime
|
|
import pandas.util._test_decorators as td
|
|
|
|
from pandas import (
|
|
NaT,
|
|
Timestamp,
|
|
)
|
|
|
|
try:
|
|
from zoneinfo import ZoneInfo
|
|
except ImportError:
|
|
ZoneInfo = None
|
|
|
|
|
|
class TestTimestampTZOperations:
|
|
# --------------------------------------------------------------
|
|
# Timestamp.tz_localize
|
|
|
|
def test_tz_localize_pushes_out_of_bounds(self):
|
|
# GH#12677
|
|
# tz_localize that pushes away from the boundary is OK
|
|
msg = (
|
|
f"Converting {Timestamp.min.strftime('%Y-%m-%d %H:%M:%S')} "
|
|
f"underflows past {Timestamp.min}"
|
|
)
|
|
pac = Timestamp.min.tz_localize("US/Pacific")
|
|
assert pac._value > Timestamp.min._value
|
|
pac.tz_convert("Asia/Tokyo") # tz_convert doesn't change value
|
|
with pytest.raises(OutOfBoundsDatetime, match=msg):
|
|
Timestamp.min.tz_localize("Asia/Tokyo")
|
|
|
|
# tz_localize that pushes away from the boundary is OK
|
|
msg = (
|
|
f"Converting {Timestamp.max.strftime('%Y-%m-%d %H:%M:%S')} "
|
|
f"overflows past {Timestamp.max}"
|
|
)
|
|
tokyo = Timestamp.max.tz_localize("Asia/Tokyo")
|
|
assert tokyo._value < Timestamp.max._value
|
|
tokyo.tz_convert("US/Pacific") # tz_convert doesn't change value
|
|
with pytest.raises(OutOfBoundsDatetime, match=msg):
|
|
Timestamp.max.tz_localize("US/Pacific")
|
|
|
|
@pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
|
|
def test_tz_localize_ambiguous_bool(self, unit):
|
|
# make sure that we are correctly accepting bool values as ambiguous
|
|
# GH#14402
|
|
ts = Timestamp("2015-11-01 01:00:03").as_unit(unit)
|
|
expected0 = Timestamp("2015-11-01 01:00:03-0500", tz="US/Central")
|
|
expected1 = Timestamp("2015-11-01 01:00:03-0600", tz="US/Central")
|
|
|
|
msg = "Cannot infer dst time from 2015-11-01 01:00:03"
|
|
with pytest.raises(pytz.AmbiguousTimeError, match=msg):
|
|
ts.tz_localize("US/Central")
|
|
|
|
with pytest.raises(pytz.AmbiguousTimeError, match=msg):
|
|
ts.tz_localize("dateutil/US/Central")
|
|
|
|
if ZoneInfo is not None:
|
|
try:
|
|
tz = ZoneInfo("US/Central")
|
|
except KeyError:
|
|
# no tzdata
|
|
pass
|
|
else:
|
|
with pytest.raises(pytz.AmbiguousTimeError, match=msg):
|
|
ts.tz_localize(tz)
|
|
|
|
result = ts.tz_localize("US/Central", ambiguous=True)
|
|
assert result == expected0
|
|
assert result._creso == getattr(NpyDatetimeUnit, f"NPY_FR_{unit}").value
|
|
|
|
result = ts.tz_localize("US/Central", ambiguous=False)
|
|
assert result == expected1
|
|
assert result._creso == getattr(NpyDatetimeUnit, f"NPY_FR_{unit}").value
|
|
|
|
def test_tz_localize_ambiguous(self):
|
|
ts = Timestamp("2014-11-02 01:00")
|
|
ts_dst = ts.tz_localize("US/Eastern", ambiguous=True)
|
|
ts_no_dst = ts.tz_localize("US/Eastern", ambiguous=False)
|
|
|
|
assert ts_no_dst._value - ts_dst._value == 3600
|
|
msg = re.escape(
|
|
"'ambiguous' parameter must be one of: "
|
|
"True, False, 'NaT', 'raise' (default)"
|
|
)
|
|
with pytest.raises(ValueError, match=msg):
|
|
ts.tz_localize("US/Eastern", ambiguous="infer")
|
|
|
|
# GH#8025
|
|
msg = "Cannot localize tz-aware Timestamp, use tz_convert for conversions"
|
|
with pytest.raises(TypeError, match=msg):
|
|
Timestamp("2011-01-01", tz="US/Eastern").tz_localize("Asia/Tokyo")
|
|
|
|
msg = "Cannot convert tz-naive Timestamp, use tz_localize to localize"
|
|
with pytest.raises(TypeError, match=msg):
|
|
Timestamp("2011-01-01").tz_convert("Asia/Tokyo")
|
|
|
|
@pytest.mark.parametrize(
|
|
"stamp, tz",
|
|
[
|
|
("2015-03-08 02:00", "US/Eastern"),
|
|
("2015-03-08 02:30", "US/Pacific"),
|
|
("2015-03-29 02:00", "Europe/Paris"),
|
|
("2015-03-29 02:30", "Europe/Belgrade"),
|
|
],
|
|
)
|
|
def test_tz_localize_nonexistent(self, stamp, tz):
|
|
# GH#13057
|
|
ts = Timestamp(stamp)
|
|
with pytest.raises(NonExistentTimeError, match=stamp):
|
|
ts.tz_localize(tz)
|
|
# GH 22644
|
|
with pytest.raises(NonExistentTimeError, match=stamp):
|
|
ts.tz_localize(tz, nonexistent="raise")
|
|
assert ts.tz_localize(tz, nonexistent="NaT") is NaT
|
|
|
|
def test_tz_localize_ambiguous_raise(self):
|
|
# GH#13057
|
|
ts = Timestamp("2015-11-1 01:00")
|
|
msg = "Cannot infer dst time from 2015-11-01 01:00:00,"
|
|
with pytest.raises(AmbiguousTimeError, match=msg):
|
|
ts.tz_localize("US/Pacific", ambiguous="raise")
|
|
|
|
def test_tz_localize_nonexistent_invalid_arg(self, warsaw):
|
|
# GH 22644
|
|
tz = warsaw
|
|
ts = Timestamp("2015-03-29 02:00:00")
|
|
msg = (
|
|
"The nonexistent argument must be one of 'raise', 'NaT', "
|
|
"'shift_forward', 'shift_backward' or a timedelta object"
|
|
)
|
|
with pytest.raises(ValueError, match=msg):
|
|
ts.tz_localize(tz, nonexistent="foo")
|
|
|
|
@pytest.mark.parametrize(
|
|
"stamp",
|
|
[
|
|
"2014-02-01 09:00",
|
|
"2014-07-08 09:00",
|
|
"2014-11-01 17:00",
|
|
"2014-11-05 00:00",
|
|
],
|
|
)
|
|
def test_tz_localize_roundtrip(self, stamp, tz_aware_fixture):
|
|
tz = tz_aware_fixture
|
|
ts = Timestamp(stamp)
|
|
localized = ts.tz_localize(tz)
|
|
assert localized == Timestamp(stamp, tz=tz)
|
|
|
|
msg = "Cannot localize tz-aware Timestamp"
|
|
with pytest.raises(TypeError, match=msg):
|
|
localized.tz_localize(tz)
|
|
|
|
reset = localized.tz_localize(None)
|
|
assert reset == ts
|
|
assert reset.tzinfo is None
|
|
|
|
def test_tz_localize_ambiguous_compat(self):
|
|
# validate that pytz and dateutil are compat for dst
|
|
# when the transition happens
|
|
naive = Timestamp("2013-10-27 01:00:00")
|
|
|
|
pytz_zone = "Europe/London"
|
|
dateutil_zone = "dateutil/Europe/London"
|
|
result_pytz = naive.tz_localize(pytz_zone, ambiguous=False)
|
|
result_dateutil = naive.tz_localize(dateutil_zone, ambiguous=False)
|
|
assert result_pytz._value == result_dateutil._value
|
|
assert result_pytz._value == 1382835600
|
|
|
|
# fixed ambiguous behavior
|
|
# see gh-14621, GH#45087
|
|
assert result_pytz.to_pydatetime().tzname() == "GMT"
|
|
assert result_dateutil.to_pydatetime().tzname() == "GMT"
|
|
assert str(result_pytz) == str(result_dateutil)
|
|
|
|
# 1 hour difference
|
|
result_pytz = naive.tz_localize(pytz_zone, ambiguous=True)
|
|
result_dateutil = naive.tz_localize(dateutil_zone, ambiguous=True)
|
|
assert result_pytz._value == result_dateutil._value
|
|
assert result_pytz._value == 1382832000
|
|
|
|
# see gh-14621
|
|
assert str(result_pytz) == str(result_dateutil)
|
|
assert (
|
|
result_pytz.to_pydatetime().tzname()
|
|
== result_dateutil.to_pydatetime().tzname()
|
|
)
|
|
|
|
@pytest.mark.parametrize(
|
|
"tz",
|
|
[
|
|
pytz.timezone("US/Eastern"),
|
|
gettz("US/Eastern"),
|
|
"US/Eastern",
|
|
"dateutil/US/Eastern",
|
|
],
|
|
)
|
|
def test_timestamp_tz_localize(self, tz):
|
|
stamp = Timestamp("3/11/2012 04:00")
|
|
|
|
result = stamp.tz_localize(tz)
|
|
expected = Timestamp("3/11/2012 04:00", tz=tz)
|
|
assert result.hour == expected.hour
|
|
assert result == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
"start_ts, tz, end_ts, shift",
|
|
[
|
|
["2015-03-29 02:20:00", "Europe/Warsaw", "2015-03-29 03:00:00", "forward"],
|
|
[
|
|
"2015-03-29 02:20:00",
|
|
"Europe/Warsaw",
|
|
"2015-03-29 01:59:59.999999999",
|
|
"backward",
|
|
],
|
|
[
|
|
"2015-03-29 02:20:00",
|
|
"Europe/Warsaw",
|
|
"2015-03-29 03:20:00",
|
|
timedelta(hours=1),
|
|
],
|
|
[
|
|
"2015-03-29 02:20:00",
|
|
"Europe/Warsaw",
|
|
"2015-03-29 01:20:00",
|
|
timedelta(hours=-1),
|
|
],
|
|
["2018-03-11 02:33:00", "US/Pacific", "2018-03-11 03:00:00", "forward"],
|
|
[
|
|
"2018-03-11 02:33:00",
|
|
"US/Pacific",
|
|
"2018-03-11 01:59:59.999999999",
|
|
"backward",
|
|
],
|
|
[
|
|
"2018-03-11 02:33:00",
|
|
"US/Pacific",
|
|
"2018-03-11 03:33:00",
|
|
timedelta(hours=1),
|
|
],
|
|
[
|
|
"2018-03-11 02:33:00",
|
|
"US/Pacific",
|
|
"2018-03-11 01:33:00",
|
|
timedelta(hours=-1),
|
|
],
|
|
],
|
|
)
|
|
@pytest.mark.parametrize("tz_type", ["", "dateutil/"])
|
|
@pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
|
|
def test_timestamp_tz_localize_nonexistent_shift(
|
|
self, start_ts, tz, end_ts, shift, tz_type, unit
|
|
):
|
|
# GH 8917, 24466
|
|
tz = tz_type + tz
|
|
if isinstance(shift, str):
|
|
shift = "shift_" + shift
|
|
ts = Timestamp(start_ts).as_unit(unit)
|
|
result = ts.tz_localize(tz, nonexistent=shift)
|
|
expected = Timestamp(end_ts).tz_localize(tz)
|
|
|
|
if unit == "us":
|
|
assert result == expected.replace(nanosecond=0)
|
|
elif unit == "ms":
|
|
micros = expected.microsecond - expected.microsecond % 1000
|
|
assert result == expected.replace(microsecond=micros, nanosecond=0)
|
|
elif unit == "s":
|
|
assert result == expected.replace(microsecond=0, nanosecond=0)
|
|
else:
|
|
assert result == expected
|
|
assert result._creso == getattr(NpyDatetimeUnit, f"NPY_FR_{unit}").value
|
|
|
|
@pytest.mark.parametrize("offset", [-1, 1])
|
|
def test_timestamp_tz_localize_nonexistent_shift_invalid(self, offset, warsaw):
|
|
# GH 8917, 24466
|
|
tz = warsaw
|
|
ts = Timestamp("2015-03-29 02:20:00")
|
|
msg = "The provided timedelta will relocalize on a nonexistent time"
|
|
with pytest.raises(ValueError, match=msg):
|
|
ts.tz_localize(tz, nonexistent=timedelta(seconds=offset))
|
|
|
|
@pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
|
|
def test_timestamp_tz_localize_nonexistent_NaT(self, warsaw, unit):
|
|
# GH 8917
|
|
tz = warsaw
|
|
ts = Timestamp("2015-03-29 02:20:00").as_unit(unit)
|
|
result = ts.tz_localize(tz, nonexistent="NaT")
|
|
assert result is NaT
|
|
|
|
@pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
|
|
def test_timestamp_tz_localize_nonexistent_raise(self, warsaw, unit):
|
|
# GH 8917
|
|
tz = warsaw
|
|
ts = Timestamp("2015-03-29 02:20:00").as_unit(unit)
|
|
msg = "2015-03-29 02:20:00"
|
|
with pytest.raises(pytz.NonExistentTimeError, match=msg):
|
|
ts.tz_localize(tz, nonexistent="raise")
|
|
msg = (
|
|
"The nonexistent argument must be one of 'raise', 'NaT', "
|
|
"'shift_forward', 'shift_backward' or a timedelta object"
|
|
)
|
|
with pytest.raises(ValueError, match=msg):
|
|
ts.tz_localize(tz, nonexistent="foo")
|
|
|
|
# ------------------------------------------------------------------
|
|
# Timestamp.tz_convert
|
|
|
|
@pytest.mark.parametrize(
|
|
"stamp",
|
|
[
|
|
"2014-02-01 09:00",
|
|
"2014-07-08 09:00",
|
|
"2014-11-01 17:00",
|
|
"2014-11-05 00:00",
|
|
],
|
|
)
|
|
def test_tz_convert_roundtrip(self, stamp, tz_aware_fixture):
|
|
tz = tz_aware_fixture
|
|
|
|
ts = Timestamp(stamp, tz="UTC")
|
|
converted = ts.tz_convert(tz)
|
|
|
|
reset = converted.tz_convert(None)
|
|
assert reset == Timestamp(stamp)
|
|
assert reset.tzinfo is None
|
|
assert reset == converted.tz_convert("UTC").tz_localize(None)
|
|
|
|
@pytest.mark.parametrize("tzstr", ["US/Eastern", "dateutil/US/Eastern"])
|
|
def test_astimezone(self, tzstr):
|
|
# astimezone is an alias for tz_convert, so keep it with
|
|
# the tz_convert tests
|
|
utcdate = Timestamp("3/11/2012 22:00", tz="UTC")
|
|
expected = utcdate.tz_convert(tzstr)
|
|
result = utcdate.astimezone(tzstr)
|
|
assert expected == result
|
|
assert isinstance(result, Timestamp)
|
|
|
|
@td.skip_if_windows
|
|
def test_tz_convert_utc_with_system_utc(self):
|
|
# from system utc to real utc
|
|
ts = Timestamp("2001-01-05 11:56", tz=timezones.maybe_get_tz("dateutil/UTC"))
|
|
# check that the time hasn't changed.
|
|
assert ts == ts.tz_convert(dateutil.tz.tzutc())
|
|
|
|
# from system utc to real utc
|
|
ts = Timestamp("2001-01-05 11:56", tz=timezones.maybe_get_tz("dateutil/UTC"))
|
|
# check that the time hasn't changed.
|
|
assert ts == ts.tz_convert(dateutil.tz.tzutc())
|
|
|
|
# ------------------------------------------------------------------
|
|
# Timestamp.__init__ with tz str or tzinfo
|
|
|
|
def test_timestamp_constructor_tz_utc(self):
|
|
utc_stamp = Timestamp("3/11/2012 05:00", tz="utc")
|
|
assert utc_stamp.tzinfo is timezone.utc
|
|
assert utc_stamp.hour == 5
|
|
|
|
utc_stamp = Timestamp("3/11/2012 05:00").tz_localize("utc")
|
|
assert utc_stamp.hour == 5
|
|
|
|
def test_timestamp_to_datetime_tzoffset(self):
|
|
tzinfo = tzoffset(None, 7200)
|
|
expected = Timestamp("3/11/2012 04:00", tz=tzinfo)
|
|
result = Timestamp(expected.to_pydatetime())
|
|
assert expected == result
|
|
|
|
def test_timestamp_constructor_near_dst_boundary(self):
|
|
# GH#11481 & GH#15777
|
|
# Naive string timestamps were being localized incorrectly
|
|
# with tz_convert_from_utc_single instead of tz_localize_to_utc
|
|
|
|
for tz in ["Europe/Brussels", "Europe/Prague"]:
|
|
result = Timestamp("2015-10-25 01:00", tz=tz)
|
|
expected = Timestamp("2015-10-25 01:00").tz_localize(tz)
|
|
assert result == expected
|
|
|
|
msg = "Cannot infer dst time from 2015-10-25 02:00:00"
|
|
with pytest.raises(pytz.AmbiguousTimeError, match=msg):
|
|
Timestamp("2015-10-25 02:00", tz=tz)
|
|
|
|
result = Timestamp("2017-03-26 01:00", tz="Europe/Paris")
|
|
expected = Timestamp("2017-03-26 01:00").tz_localize("Europe/Paris")
|
|
assert result == expected
|
|
|
|
msg = "2017-03-26 02:00"
|
|
with pytest.raises(pytz.NonExistentTimeError, match=msg):
|
|
Timestamp("2017-03-26 02:00", tz="Europe/Paris")
|
|
|
|
# GH#11708
|
|
naive = Timestamp("2015-11-18 10:00:00")
|
|
result = naive.tz_localize("UTC").tz_convert("Asia/Kolkata")
|
|
expected = Timestamp("2015-11-18 15:30:00+0530", tz="Asia/Kolkata")
|
|
assert result == expected
|
|
|
|
# GH#15823
|
|
result = Timestamp("2017-03-26 00:00", tz="Europe/Paris")
|
|
expected = Timestamp("2017-03-26 00:00:00+0100", tz="Europe/Paris")
|
|
assert result == expected
|
|
|
|
result = Timestamp("2017-03-26 01:00", tz="Europe/Paris")
|
|
expected = Timestamp("2017-03-26 01:00:00+0100", tz="Europe/Paris")
|
|
assert result == expected
|
|
|
|
msg = "2017-03-26 02:00"
|
|
with pytest.raises(pytz.NonExistentTimeError, match=msg):
|
|
Timestamp("2017-03-26 02:00", tz="Europe/Paris")
|
|
|
|
result = Timestamp("2017-03-26 02:00:00+0100", tz="Europe/Paris")
|
|
naive = Timestamp(result.as_unit("ns")._value)
|
|
expected = naive.tz_localize("UTC").tz_convert("Europe/Paris")
|
|
assert result == expected
|
|
|
|
result = Timestamp("2017-03-26 03:00", tz="Europe/Paris")
|
|
expected = Timestamp("2017-03-26 03:00:00+0200", tz="Europe/Paris")
|
|
assert result == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
"tz",
|
|
[
|
|
pytz.timezone("US/Eastern"),
|
|
gettz("US/Eastern"),
|
|
"US/Eastern",
|
|
"dateutil/US/Eastern",
|
|
],
|
|
)
|
|
def test_timestamp_constructed_by_date_and_tz(self, tz):
|
|
# GH#2993, Timestamp cannot be constructed by datetime.date
|
|
# and tz correctly
|
|
|
|
result = Timestamp(date(2012, 3, 11), tz=tz)
|
|
|
|
expected = Timestamp("3/11/2012", tz=tz)
|
|
assert result.hour == expected.hour
|
|
assert result == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
"tz",
|
|
[
|
|
pytz.timezone("US/Eastern"),
|
|
gettz("US/Eastern"),
|
|
"US/Eastern",
|
|
"dateutil/US/Eastern",
|
|
],
|
|
)
|
|
def test_timestamp_add_timedelta_push_over_dst_boundary(self, tz):
|
|
# GH#1389
|
|
|
|
# 4 hours before DST transition
|
|
stamp = Timestamp("3/10/2012 22:00", tz=tz)
|
|
|
|
result = stamp + timedelta(hours=6)
|
|
|
|
# spring forward, + "7" hours
|
|
expected = Timestamp("3/11/2012 05:00", tz=tz)
|
|
|
|
assert result == expected
|
|
|
|
def test_timestamp_timetz_equivalent_with_datetime_tz(self, tz_naive_fixture):
|
|
# GH21358
|
|
tz = timezones.maybe_get_tz(tz_naive_fixture)
|
|
|
|
stamp = Timestamp("2018-06-04 10:20:30", tz=tz)
|
|
_datetime = datetime(2018, 6, 4, hour=10, minute=20, second=30, tzinfo=tz)
|
|
|
|
result = stamp.timetz()
|
|
expected = _datetime.timetz()
|
|
|
|
assert result == expected
|