test_timezones.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. """
  2. Tests for Timestamp timezone-related methods
  3. """
  4. from datetime import (
  5. date,
  6. datetime,
  7. timedelta,
  8. timezone,
  9. )
  10. import re
  11. import dateutil
  12. from dateutil.tz import (
  13. gettz,
  14. tzoffset,
  15. )
  16. import pytest
  17. import pytz
  18. from pytz.exceptions import (
  19. AmbiguousTimeError,
  20. NonExistentTimeError,
  21. )
  22. from pandas._libs.tslibs import timezones
  23. from pandas._libs.tslibs.dtypes import NpyDatetimeUnit
  24. from pandas.errors import OutOfBoundsDatetime
  25. import pandas.util._test_decorators as td
  26. from pandas import (
  27. NaT,
  28. Timestamp,
  29. )
  30. try:
  31. from zoneinfo import ZoneInfo
  32. except ImportError:
  33. ZoneInfo = None
  34. class TestTimestampTZOperations:
  35. # --------------------------------------------------------------
  36. # Timestamp.tz_localize
  37. def test_tz_localize_pushes_out_of_bounds(self):
  38. # GH#12677
  39. # tz_localize that pushes away from the boundary is OK
  40. msg = (
  41. f"Converting {Timestamp.min.strftime('%Y-%m-%d %H:%M:%S')} "
  42. f"underflows past {Timestamp.min}"
  43. )
  44. pac = Timestamp.min.tz_localize("US/Pacific")
  45. assert pac._value > Timestamp.min._value
  46. pac.tz_convert("Asia/Tokyo") # tz_convert doesn't change value
  47. with pytest.raises(OutOfBoundsDatetime, match=msg):
  48. Timestamp.min.tz_localize("Asia/Tokyo")
  49. # tz_localize that pushes away from the boundary is OK
  50. msg = (
  51. f"Converting {Timestamp.max.strftime('%Y-%m-%d %H:%M:%S')} "
  52. f"overflows past {Timestamp.max}"
  53. )
  54. tokyo = Timestamp.max.tz_localize("Asia/Tokyo")
  55. assert tokyo._value < Timestamp.max._value
  56. tokyo.tz_convert("US/Pacific") # tz_convert doesn't change value
  57. with pytest.raises(OutOfBoundsDatetime, match=msg):
  58. Timestamp.max.tz_localize("US/Pacific")
  59. @pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
  60. def test_tz_localize_ambiguous_bool(self, unit):
  61. # make sure that we are correctly accepting bool values as ambiguous
  62. # GH#14402
  63. ts = Timestamp("2015-11-01 01:00:03").as_unit(unit)
  64. expected0 = Timestamp("2015-11-01 01:00:03-0500", tz="US/Central")
  65. expected1 = Timestamp("2015-11-01 01:00:03-0600", tz="US/Central")
  66. msg = "Cannot infer dst time from 2015-11-01 01:00:03"
  67. with pytest.raises(pytz.AmbiguousTimeError, match=msg):
  68. ts.tz_localize("US/Central")
  69. with pytest.raises(pytz.AmbiguousTimeError, match=msg):
  70. ts.tz_localize("dateutil/US/Central")
  71. if ZoneInfo is not None:
  72. try:
  73. tz = ZoneInfo("US/Central")
  74. except KeyError:
  75. # no tzdata
  76. pass
  77. else:
  78. with pytest.raises(pytz.AmbiguousTimeError, match=msg):
  79. ts.tz_localize(tz)
  80. result = ts.tz_localize("US/Central", ambiguous=True)
  81. assert result == expected0
  82. assert result._creso == getattr(NpyDatetimeUnit, f"NPY_FR_{unit}").value
  83. result = ts.tz_localize("US/Central", ambiguous=False)
  84. assert result == expected1
  85. assert result._creso == getattr(NpyDatetimeUnit, f"NPY_FR_{unit}").value
  86. def test_tz_localize_ambiguous(self):
  87. ts = Timestamp("2014-11-02 01:00")
  88. ts_dst = ts.tz_localize("US/Eastern", ambiguous=True)
  89. ts_no_dst = ts.tz_localize("US/Eastern", ambiguous=False)
  90. assert ts_no_dst._value - ts_dst._value == 3600
  91. msg = re.escape(
  92. "'ambiguous' parameter must be one of: "
  93. "True, False, 'NaT', 'raise' (default)"
  94. )
  95. with pytest.raises(ValueError, match=msg):
  96. ts.tz_localize("US/Eastern", ambiguous="infer")
  97. # GH#8025
  98. msg = "Cannot localize tz-aware Timestamp, use tz_convert for conversions"
  99. with pytest.raises(TypeError, match=msg):
  100. Timestamp("2011-01-01", tz="US/Eastern").tz_localize("Asia/Tokyo")
  101. msg = "Cannot convert tz-naive Timestamp, use tz_localize to localize"
  102. with pytest.raises(TypeError, match=msg):
  103. Timestamp("2011-01-01").tz_convert("Asia/Tokyo")
  104. @pytest.mark.parametrize(
  105. "stamp, tz",
  106. [
  107. ("2015-03-08 02:00", "US/Eastern"),
  108. ("2015-03-08 02:30", "US/Pacific"),
  109. ("2015-03-29 02:00", "Europe/Paris"),
  110. ("2015-03-29 02:30", "Europe/Belgrade"),
  111. ],
  112. )
  113. def test_tz_localize_nonexistent(self, stamp, tz):
  114. # GH#13057
  115. ts = Timestamp(stamp)
  116. with pytest.raises(NonExistentTimeError, match=stamp):
  117. ts.tz_localize(tz)
  118. # GH 22644
  119. with pytest.raises(NonExistentTimeError, match=stamp):
  120. ts.tz_localize(tz, nonexistent="raise")
  121. assert ts.tz_localize(tz, nonexistent="NaT") is NaT
  122. def test_tz_localize_ambiguous_raise(self):
  123. # GH#13057
  124. ts = Timestamp("2015-11-1 01:00")
  125. msg = "Cannot infer dst time from 2015-11-01 01:00:00,"
  126. with pytest.raises(AmbiguousTimeError, match=msg):
  127. ts.tz_localize("US/Pacific", ambiguous="raise")
  128. def test_tz_localize_nonexistent_invalid_arg(self, warsaw):
  129. # GH 22644
  130. tz = warsaw
  131. ts = Timestamp("2015-03-29 02:00:00")
  132. msg = (
  133. "The nonexistent argument must be one of 'raise', 'NaT', "
  134. "'shift_forward', 'shift_backward' or a timedelta object"
  135. )
  136. with pytest.raises(ValueError, match=msg):
  137. ts.tz_localize(tz, nonexistent="foo")
  138. @pytest.mark.parametrize(
  139. "stamp",
  140. [
  141. "2014-02-01 09:00",
  142. "2014-07-08 09:00",
  143. "2014-11-01 17:00",
  144. "2014-11-05 00:00",
  145. ],
  146. )
  147. def test_tz_localize_roundtrip(self, stamp, tz_aware_fixture):
  148. tz = tz_aware_fixture
  149. ts = Timestamp(stamp)
  150. localized = ts.tz_localize(tz)
  151. assert localized == Timestamp(stamp, tz=tz)
  152. msg = "Cannot localize tz-aware Timestamp"
  153. with pytest.raises(TypeError, match=msg):
  154. localized.tz_localize(tz)
  155. reset = localized.tz_localize(None)
  156. assert reset == ts
  157. assert reset.tzinfo is None
  158. def test_tz_localize_ambiguous_compat(self):
  159. # validate that pytz and dateutil are compat for dst
  160. # when the transition happens
  161. naive = Timestamp("2013-10-27 01:00:00")
  162. pytz_zone = "Europe/London"
  163. dateutil_zone = "dateutil/Europe/London"
  164. result_pytz = naive.tz_localize(pytz_zone, ambiguous=False)
  165. result_dateutil = naive.tz_localize(dateutil_zone, ambiguous=False)
  166. assert result_pytz._value == result_dateutil._value
  167. assert result_pytz._value == 1382835600
  168. # fixed ambiguous behavior
  169. # see gh-14621, GH#45087
  170. assert result_pytz.to_pydatetime().tzname() == "GMT"
  171. assert result_dateutil.to_pydatetime().tzname() == "GMT"
  172. assert str(result_pytz) == str(result_dateutil)
  173. # 1 hour difference
  174. result_pytz = naive.tz_localize(pytz_zone, ambiguous=True)
  175. result_dateutil = naive.tz_localize(dateutil_zone, ambiguous=True)
  176. assert result_pytz._value == result_dateutil._value
  177. assert result_pytz._value == 1382832000
  178. # see gh-14621
  179. assert str(result_pytz) == str(result_dateutil)
  180. assert (
  181. result_pytz.to_pydatetime().tzname()
  182. == result_dateutil.to_pydatetime().tzname()
  183. )
  184. @pytest.mark.parametrize(
  185. "tz",
  186. [
  187. pytz.timezone("US/Eastern"),
  188. gettz("US/Eastern"),
  189. "US/Eastern",
  190. "dateutil/US/Eastern",
  191. ],
  192. )
  193. def test_timestamp_tz_localize(self, tz):
  194. stamp = Timestamp("3/11/2012 04:00")
  195. result = stamp.tz_localize(tz)
  196. expected = Timestamp("3/11/2012 04:00", tz=tz)
  197. assert result.hour == expected.hour
  198. assert result == expected
  199. @pytest.mark.parametrize(
  200. "start_ts, tz, end_ts, shift",
  201. [
  202. ["2015-03-29 02:20:00", "Europe/Warsaw", "2015-03-29 03:00:00", "forward"],
  203. [
  204. "2015-03-29 02:20:00",
  205. "Europe/Warsaw",
  206. "2015-03-29 01:59:59.999999999",
  207. "backward",
  208. ],
  209. [
  210. "2015-03-29 02:20:00",
  211. "Europe/Warsaw",
  212. "2015-03-29 03:20:00",
  213. timedelta(hours=1),
  214. ],
  215. [
  216. "2015-03-29 02:20:00",
  217. "Europe/Warsaw",
  218. "2015-03-29 01:20:00",
  219. timedelta(hours=-1),
  220. ],
  221. ["2018-03-11 02:33:00", "US/Pacific", "2018-03-11 03:00:00", "forward"],
  222. [
  223. "2018-03-11 02:33:00",
  224. "US/Pacific",
  225. "2018-03-11 01:59:59.999999999",
  226. "backward",
  227. ],
  228. [
  229. "2018-03-11 02:33:00",
  230. "US/Pacific",
  231. "2018-03-11 03:33:00",
  232. timedelta(hours=1),
  233. ],
  234. [
  235. "2018-03-11 02:33:00",
  236. "US/Pacific",
  237. "2018-03-11 01:33:00",
  238. timedelta(hours=-1),
  239. ],
  240. ],
  241. )
  242. @pytest.mark.parametrize("tz_type", ["", "dateutil/"])
  243. @pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
  244. def test_timestamp_tz_localize_nonexistent_shift(
  245. self, start_ts, tz, end_ts, shift, tz_type, unit
  246. ):
  247. # GH 8917, 24466
  248. tz = tz_type + tz
  249. if isinstance(shift, str):
  250. shift = "shift_" + shift
  251. ts = Timestamp(start_ts).as_unit(unit)
  252. result = ts.tz_localize(tz, nonexistent=shift)
  253. expected = Timestamp(end_ts).tz_localize(tz)
  254. if unit == "us":
  255. assert result == expected.replace(nanosecond=0)
  256. elif unit == "ms":
  257. micros = expected.microsecond - expected.microsecond % 1000
  258. assert result == expected.replace(microsecond=micros, nanosecond=0)
  259. elif unit == "s":
  260. assert result == expected.replace(microsecond=0, nanosecond=0)
  261. else:
  262. assert result == expected
  263. assert result._creso == getattr(NpyDatetimeUnit, f"NPY_FR_{unit}").value
  264. @pytest.mark.parametrize("offset", [-1, 1])
  265. def test_timestamp_tz_localize_nonexistent_shift_invalid(self, offset, warsaw):
  266. # GH 8917, 24466
  267. tz = warsaw
  268. ts = Timestamp("2015-03-29 02:20:00")
  269. msg = "The provided timedelta will relocalize on a nonexistent time"
  270. with pytest.raises(ValueError, match=msg):
  271. ts.tz_localize(tz, nonexistent=timedelta(seconds=offset))
  272. @pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
  273. def test_timestamp_tz_localize_nonexistent_NaT(self, warsaw, unit):
  274. # GH 8917
  275. tz = warsaw
  276. ts = Timestamp("2015-03-29 02:20:00").as_unit(unit)
  277. result = ts.tz_localize(tz, nonexistent="NaT")
  278. assert result is NaT
  279. @pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
  280. def test_timestamp_tz_localize_nonexistent_raise(self, warsaw, unit):
  281. # GH 8917
  282. tz = warsaw
  283. ts = Timestamp("2015-03-29 02:20:00").as_unit(unit)
  284. msg = "2015-03-29 02:20:00"
  285. with pytest.raises(pytz.NonExistentTimeError, match=msg):
  286. ts.tz_localize(tz, nonexistent="raise")
  287. msg = (
  288. "The nonexistent argument must be one of 'raise', 'NaT', "
  289. "'shift_forward', 'shift_backward' or a timedelta object"
  290. )
  291. with pytest.raises(ValueError, match=msg):
  292. ts.tz_localize(tz, nonexistent="foo")
  293. # ------------------------------------------------------------------
  294. # Timestamp.tz_convert
  295. @pytest.mark.parametrize(
  296. "stamp",
  297. [
  298. "2014-02-01 09:00",
  299. "2014-07-08 09:00",
  300. "2014-11-01 17:00",
  301. "2014-11-05 00:00",
  302. ],
  303. )
  304. def test_tz_convert_roundtrip(self, stamp, tz_aware_fixture):
  305. tz = tz_aware_fixture
  306. ts = Timestamp(stamp, tz="UTC")
  307. converted = ts.tz_convert(tz)
  308. reset = converted.tz_convert(None)
  309. assert reset == Timestamp(stamp)
  310. assert reset.tzinfo is None
  311. assert reset == converted.tz_convert("UTC").tz_localize(None)
  312. @pytest.mark.parametrize("tzstr", ["US/Eastern", "dateutil/US/Eastern"])
  313. def test_astimezone(self, tzstr):
  314. # astimezone is an alias for tz_convert, so keep it with
  315. # the tz_convert tests
  316. utcdate = Timestamp("3/11/2012 22:00", tz="UTC")
  317. expected = utcdate.tz_convert(tzstr)
  318. result = utcdate.astimezone(tzstr)
  319. assert expected == result
  320. assert isinstance(result, Timestamp)
  321. @td.skip_if_windows
  322. def test_tz_convert_utc_with_system_utc(self):
  323. # from system utc to real utc
  324. ts = Timestamp("2001-01-05 11:56", tz=timezones.maybe_get_tz("dateutil/UTC"))
  325. # check that the time hasn't changed.
  326. assert ts == ts.tz_convert(dateutil.tz.tzutc())
  327. # from system utc to real utc
  328. ts = Timestamp("2001-01-05 11:56", tz=timezones.maybe_get_tz("dateutil/UTC"))
  329. # check that the time hasn't changed.
  330. assert ts == ts.tz_convert(dateutil.tz.tzutc())
  331. # ------------------------------------------------------------------
  332. # Timestamp.__init__ with tz str or tzinfo
  333. def test_timestamp_constructor_tz_utc(self):
  334. utc_stamp = Timestamp("3/11/2012 05:00", tz="utc")
  335. assert utc_stamp.tzinfo is timezone.utc
  336. assert utc_stamp.hour == 5
  337. utc_stamp = Timestamp("3/11/2012 05:00").tz_localize("utc")
  338. assert utc_stamp.hour == 5
  339. def test_timestamp_to_datetime_tzoffset(self):
  340. tzinfo = tzoffset(None, 7200)
  341. expected = Timestamp("3/11/2012 04:00", tz=tzinfo)
  342. result = Timestamp(expected.to_pydatetime())
  343. assert expected == result
  344. def test_timestamp_constructor_near_dst_boundary(self):
  345. # GH#11481 & GH#15777
  346. # Naive string timestamps were being localized incorrectly
  347. # with tz_convert_from_utc_single instead of tz_localize_to_utc
  348. for tz in ["Europe/Brussels", "Europe/Prague"]:
  349. result = Timestamp("2015-10-25 01:00", tz=tz)
  350. expected = Timestamp("2015-10-25 01:00").tz_localize(tz)
  351. assert result == expected
  352. msg = "Cannot infer dst time from 2015-10-25 02:00:00"
  353. with pytest.raises(pytz.AmbiguousTimeError, match=msg):
  354. Timestamp("2015-10-25 02:00", tz=tz)
  355. result = Timestamp("2017-03-26 01:00", tz="Europe/Paris")
  356. expected = Timestamp("2017-03-26 01:00").tz_localize("Europe/Paris")
  357. assert result == expected
  358. msg = "2017-03-26 02:00"
  359. with pytest.raises(pytz.NonExistentTimeError, match=msg):
  360. Timestamp("2017-03-26 02:00", tz="Europe/Paris")
  361. # GH#11708
  362. naive = Timestamp("2015-11-18 10:00:00")
  363. result = naive.tz_localize("UTC").tz_convert("Asia/Kolkata")
  364. expected = Timestamp("2015-11-18 15:30:00+0530", tz="Asia/Kolkata")
  365. assert result == expected
  366. # GH#15823
  367. result = Timestamp("2017-03-26 00:00", tz="Europe/Paris")
  368. expected = Timestamp("2017-03-26 00:00:00+0100", tz="Europe/Paris")
  369. assert result == expected
  370. result = Timestamp("2017-03-26 01:00", tz="Europe/Paris")
  371. expected = Timestamp("2017-03-26 01:00:00+0100", tz="Europe/Paris")
  372. assert result == expected
  373. msg = "2017-03-26 02:00"
  374. with pytest.raises(pytz.NonExistentTimeError, match=msg):
  375. Timestamp("2017-03-26 02:00", tz="Europe/Paris")
  376. result = Timestamp("2017-03-26 02:00:00+0100", tz="Europe/Paris")
  377. naive = Timestamp(result.as_unit("ns")._value)
  378. expected = naive.tz_localize("UTC").tz_convert("Europe/Paris")
  379. assert result == expected
  380. result = Timestamp("2017-03-26 03:00", tz="Europe/Paris")
  381. expected = Timestamp("2017-03-26 03:00:00+0200", tz="Europe/Paris")
  382. assert result == expected
  383. @pytest.mark.parametrize(
  384. "tz",
  385. [
  386. pytz.timezone("US/Eastern"),
  387. gettz("US/Eastern"),
  388. "US/Eastern",
  389. "dateutil/US/Eastern",
  390. ],
  391. )
  392. def test_timestamp_constructed_by_date_and_tz(self, tz):
  393. # GH#2993, Timestamp cannot be constructed by datetime.date
  394. # and tz correctly
  395. result = Timestamp(date(2012, 3, 11), tz=tz)
  396. expected = Timestamp("3/11/2012", tz=tz)
  397. assert result.hour == expected.hour
  398. assert result == expected
  399. @pytest.mark.parametrize(
  400. "tz",
  401. [
  402. pytz.timezone("US/Eastern"),
  403. gettz("US/Eastern"),
  404. "US/Eastern",
  405. "dateutil/US/Eastern",
  406. ],
  407. )
  408. def test_timestamp_add_timedelta_push_over_dst_boundary(self, tz):
  409. # GH#1389
  410. # 4 hours before DST transition
  411. stamp = Timestamp("3/10/2012 22:00", tz=tz)
  412. result = stamp + timedelta(hours=6)
  413. # spring forward, + "7" hours
  414. expected = Timestamp("3/11/2012 05:00", tz=tz)
  415. assert result == expected
  416. def test_timestamp_timetz_equivalent_with_datetime_tz(self, tz_naive_fixture):
  417. # GH21358
  418. tz = timezones.maybe_get_tz(tz_naive_fixture)
  419. stamp = Timestamp("2018-06-04 10:20:30", tz=tz)
  420. _datetime = datetime(2018, 6, 4, hour=10, minute=20, second=30, tzinfo=tz)
  421. result = stamp.timetz()
  422. expected = _datetime.timetz()
  423. assert result == expected