test_timezones.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests for Timestamp timezone-related methods
  4. """
  5. from datetime import date, datetime, timedelta
  6. from distutils.version import LooseVersion
  7. import dateutil
  8. from dateutil.tz import gettz, tzoffset
  9. import pytest
  10. import pytz
  11. from pytz.exceptions import AmbiguousTimeError, NonExistentTimeError
  12. from pandas._libs.tslibs import timezones
  13. from pandas.errors import OutOfBoundsDatetime
  14. import pandas.util._test_decorators as td
  15. from pandas import NaT, Timestamp
  16. import pandas.util.testing as tm
  17. class TestTimestampTZOperations(object):
  18. # --------------------------------------------------------------
  19. # Timestamp.tz_localize
  20. def test_tz_localize_pushes_out_of_bounds(self):
  21. # GH#12677
  22. # tz_localize that pushes away from the boundary is OK
  23. pac = Timestamp.min.tz_localize('US/Pacific')
  24. assert pac.value > Timestamp.min.value
  25. pac.tz_convert('Asia/Tokyo') # tz_convert doesn't change value
  26. with pytest.raises(OutOfBoundsDatetime):
  27. Timestamp.min.tz_localize('Asia/Tokyo')
  28. # tz_localize that pushes away from the boundary is OK
  29. tokyo = Timestamp.max.tz_localize('Asia/Tokyo')
  30. assert tokyo.value < Timestamp.max.value
  31. tokyo.tz_convert('US/Pacific') # tz_convert doesn't change value
  32. with pytest.raises(OutOfBoundsDatetime):
  33. Timestamp.max.tz_localize('US/Pacific')
  34. def test_tz_localize_ambiguous_bool(self):
  35. # make sure that we are correctly accepting bool values as ambiguous
  36. # GH#14402
  37. ts = Timestamp('2015-11-01 01:00:03')
  38. expected0 = Timestamp('2015-11-01 01:00:03-0500', tz='US/Central')
  39. expected1 = Timestamp('2015-11-01 01:00:03-0600', tz='US/Central')
  40. with pytest.raises(pytz.AmbiguousTimeError):
  41. ts.tz_localize('US/Central')
  42. result = ts.tz_localize('US/Central', ambiguous=True)
  43. assert result == expected0
  44. result = ts.tz_localize('US/Central', ambiguous=False)
  45. assert result == expected1
  46. def test_tz_localize_ambiguous(self):
  47. ts = Timestamp('2014-11-02 01:00')
  48. ts_dst = ts.tz_localize('US/Eastern', ambiguous=True)
  49. ts_no_dst = ts.tz_localize('US/Eastern', ambiguous=False)
  50. assert (ts_no_dst.value - ts_dst.value) / 1e9 == 3600
  51. with pytest.raises(ValueError):
  52. ts.tz_localize('US/Eastern', ambiguous='infer')
  53. # GH#8025
  54. msg = ('Cannot localize tz-aware Timestamp, '
  55. 'use tz_convert for conversions')
  56. with pytest.raises(TypeError, match=msg):
  57. Timestamp('2011-01-01', tz='US/Eastern').tz_localize('Asia/Tokyo')
  58. msg = ('Cannot convert tz-naive Timestamp, '
  59. 'use tz_localize to localize')
  60. with pytest.raises(TypeError, match=msg):
  61. Timestamp('2011-01-01').tz_convert('Asia/Tokyo')
  62. @pytest.mark.parametrize('stamp, tz', [
  63. ('2015-03-08 02:00', 'US/Eastern'),
  64. ('2015-03-08 02:30', 'US/Pacific'),
  65. ('2015-03-29 02:00', 'Europe/Paris'),
  66. ('2015-03-29 02:30', 'Europe/Belgrade')])
  67. @pytest.mark.filterwarnings('ignore::FutureWarning')
  68. def test_tz_localize_nonexistent(self, stamp, tz):
  69. # GH#13057
  70. ts = Timestamp(stamp)
  71. with pytest.raises(NonExistentTimeError):
  72. ts.tz_localize(tz)
  73. # GH 22644
  74. with pytest.raises(NonExistentTimeError):
  75. with tm.assert_produces_warning(FutureWarning):
  76. ts.tz_localize(tz, errors='raise')
  77. with tm.assert_produces_warning(FutureWarning):
  78. assert ts.tz_localize(tz, errors='coerce') is NaT
  79. def test_tz_localize_errors_ambiguous(self):
  80. # GH#13057
  81. ts = Timestamp('2015-11-1 01:00')
  82. with pytest.raises(AmbiguousTimeError):
  83. with tm.assert_produces_warning(FutureWarning):
  84. ts.tz_localize('US/Pacific', errors='coerce')
  85. @pytest.mark.filterwarnings('ignore::FutureWarning')
  86. def test_tz_localize_errors_invalid_arg(self):
  87. # GH 22644
  88. tz = 'Europe/Warsaw'
  89. ts = Timestamp('2015-03-29 02:00:00')
  90. with pytest.raises(ValueError):
  91. with tm.assert_produces_warning(FutureWarning):
  92. ts.tz_localize(tz, errors='foo')
  93. def test_tz_localize_errors_coerce(self):
  94. # GH 22644
  95. # make sure errors='coerce' gets mapped correctly to nonexistent
  96. tz = 'Europe/Warsaw'
  97. ts = Timestamp('2015-03-29 02:00:00')
  98. with tm.assert_produces_warning(FutureWarning):
  99. result = ts.tz_localize(tz, errors='coerce')
  100. expected = ts.tz_localize(tz, nonexistent='NaT')
  101. assert result is expected
  102. @pytest.mark.parametrize('stamp', ['2014-02-01 09:00', '2014-07-08 09:00',
  103. '2014-11-01 17:00', '2014-11-05 00:00'])
  104. def test_tz_localize_roundtrip(self, stamp, tz_aware_fixture):
  105. tz = tz_aware_fixture
  106. ts = Timestamp(stamp)
  107. localized = ts.tz_localize(tz)
  108. assert localized == Timestamp(stamp, tz=tz)
  109. with pytest.raises(TypeError):
  110. localized.tz_localize(tz)
  111. reset = localized.tz_localize(None)
  112. assert reset == ts
  113. assert reset.tzinfo is None
  114. def test_tz_localize_ambiguous_compat(self):
  115. # validate that pytz and dateutil are compat for dst
  116. # when the transition happens
  117. naive = Timestamp('2013-10-27 01:00:00')
  118. pytz_zone = 'Europe/London'
  119. dateutil_zone = 'dateutil/Europe/London'
  120. result_pytz = naive.tz_localize(pytz_zone, ambiguous=0)
  121. result_dateutil = naive.tz_localize(dateutil_zone, ambiguous=0)
  122. assert result_pytz.value == result_dateutil.value
  123. assert result_pytz.value == 1382835600000000000
  124. if LooseVersion(dateutil.__version__) < LooseVersion('2.6.0'):
  125. # dateutil 2.6 buggy w.r.t. ambiguous=0
  126. # see gh-14621
  127. # see https://github.com/dateutil/dateutil/issues/321
  128. assert (result_pytz.to_pydatetime().tzname() ==
  129. result_dateutil.to_pydatetime().tzname())
  130. assert str(result_pytz) == str(result_dateutil)
  131. elif LooseVersion(dateutil.__version__) > LooseVersion('2.6.0'):
  132. # fixed ambiguous behavior
  133. assert result_pytz.to_pydatetime().tzname() == 'GMT'
  134. assert result_dateutil.to_pydatetime().tzname() == 'BST'
  135. assert str(result_pytz) != str(result_dateutil)
  136. # 1 hour difference
  137. result_pytz = naive.tz_localize(pytz_zone, ambiguous=1)
  138. result_dateutil = naive.tz_localize(dateutil_zone, ambiguous=1)
  139. assert result_pytz.value == result_dateutil.value
  140. assert result_pytz.value == 1382832000000000000
  141. # dateutil < 2.6 is buggy w.r.t. ambiguous timezones
  142. if LooseVersion(dateutil.__version__) > LooseVersion('2.5.3'):
  143. # see gh-14621
  144. assert str(result_pytz) == str(result_dateutil)
  145. assert (result_pytz.to_pydatetime().tzname() ==
  146. result_dateutil.to_pydatetime().tzname())
  147. @pytest.mark.parametrize('tz', [pytz.timezone('US/Eastern'),
  148. gettz('US/Eastern'),
  149. 'US/Eastern', 'dateutil/US/Eastern'])
  150. def test_timestamp_tz_localize(self, tz):
  151. stamp = Timestamp('3/11/2012 04:00')
  152. result = stamp.tz_localize(tz)
  153. expected = Timestamp('3/11/2012 04:00', tz=tz)
  154. assert result.hour == expected.hour
  155. assert result == expected
  156. @pytest.mark.parametrize('start_ts, tz, end_ts, shift', [
  157. ['2015-03-29 02:20:00', 'Europe/Warsaw', '2015-03-29 03:00:00',
  158. 'forward'],
  159. ['2015-03-29 02:20:00', 'Europe/Warsaw',
  160. '2015-03-29 01:59:59.999999999', 'backward'],
  161. ['2015-03-29 02:20:00', 'Europe/Warsaw',
  162. '2015-03-29 03:20:00', timedelta(hours=1)],
  163. ['2015-03-29 02:20:00', 'Europe/Warsaw',
  164. '2015-03-29 01:20:00', timedelta(hours=-1)],
  165. ['2018-03-11 02:33:00', 'US/Pacific', '2018-03-11 03:00:00',
  166. 'forward'],
  167. ['2018-03-11 02:33:00', 'US/Pacific', '2018-03-11 01:59:59.999999999',
  168. 'backward'],
  169. ['2018-03-11 02:33:00', 'US/Pacific', '2018-03-11 03:33:00',
  170. timedelta(hours=1)],
  171. ['2018-03-11 02:33:00', 'US/Pacific', '2018-03-11 01:33:00',
  172. timedelta(hours=-1)]
  173. ])
  174. @pytest.mark.parametrize('tz_type', ['', 'dateutil/'])
  175. def test_timestamp_tz_localize_nonexistent_shift(self, start_ts, tz,
  176. end_ts, shift,
  177. tz_type):
  178. # GH 8917, 24466
  179. tz = tz_type + tz
  180. if isinstance(shift, str):
  181. shift = 'shift_' + shift
  182. ts = Timestamp(start_ts)
  183. result = ts.tz_localize(tz, nonexistent=shift)
  184. expected = Timestamp(end_ts).tz_localize(tz)
  185. assert result == expected
  186. @pytest.mark.parametrize('offset', [-1, 1])
  187. @pytest.mark.parametrize('tz_type', ['', 'dateutil/'])
  188. def test_timestamp_tz_localize_nonexistent_shift_invalid(self, offset,
  189. tz_type):
  190. # GH 8917, 24466
  191. tz = tz_type + 'Europe/Warsaw'
  192. ts = Timestamp('2015-03-29 02:20:00')
  193. msg = "The provided timedelta will relocalize on a nonexistent time"
  194. with pytest.raises(ValueError, match=msg):
  195. ts.tz_localize(tz, nonexistent=timedelta(seconds=offset))
  196. @pytest.mark.parametrize('tz', ['Europe/Warsaw', 'dateutil/Europe/Warsaw'])
  197. def test_timestamp_tz_localize_nonexistent_NaT(self, tz):
  198. # GH 8917
  199. ts = Timestamp('2015-03-29 02:20:00')
  200. result = ts.tz_localize(tz, nonexistent='NaT')
  201. assert result is NaT
  202. @pytest.mark.parametrize('tz', ['Europe/Warsaw', 'dateutil/Europe/Warsaw'])
  203. def test_timestamp_tz_localize_nonexistent_raise(self, tz):
  204. # GH 8917
  205. ts = Timestamp('2015-03-29 02:20:00')
  206. with pytest.raises(pytz.NonExistentTimeError):
  207. ts.tz_localize(tz, nonexistent='raise')
  208. with pytest.raises(ValueError):
  209. ts.tz_localize(tz, nonexistent='foo')
  210. # ------------------------------------------------------------------
  211. # Timestamp.tz_convert
  212. @pytest.mark.parametrize('stamp', ['2014-02-01 09:00', '2014-07-08 09:00',
  213. '2014-11-01 17:00', '2014-11-05 00:00'])
  214. def test_tz_convert_roundtrip(self, stamp, tz_aware_fixture):
  215. tz = tz_aware_fixture
  216. ts = Timestamp(stamp, tz='UTC')
  217. converted = ts.tz_convert(tz)
  218. reset = converted.tz_convert(None)
  219. assert reset == Timestamp(stamp)
  220. assert reset.tzinfo is None
  221. assert reset == converted.tz_convert('UTC').tz_localize(None)
  222. @pytest.mark.parametrize('tzstr', ['US/Eastern', 'dateutil/US/Eastern'])
  223. def test_astimezone(self, tzstr):
  224. # astimezone is an alias for tz_convert, so keep it with
  225. # the tz_convert tests
  226. utcdate = Timestamp('3/11/2012 22:00', tz='UTC')
  227. expected = utcdate.tz_convert(tzstr)
  228. result = utcdate.astimezone(tzstr)
  229. assert expected == result
  230. assert isinstance(result, Timestamp)
  231. @td.skip_if_windows
  232. def test_tz_convert_utc_with_system_utc(self):
  233. from pandas._libs.tslibs.timezones import maybe_get_tz
  234. # from system utc to real utc
  235. ts = Timestamp('2001-01-05 11:56', tz=maybe_get_tz('dateutil/UTC'))
  236. # check that the time hasn't changed.
  237. assert ts == ts.tz_convert(dateutil.tz.tzutc())
  238. # from system utc to real utc
  239. ts = Timestamp('2001-01-05 11:56', tz=maybe_get_tz('dateutil/UTC'))
  240. # check that the time hasn't changed.
  241. assert ts == ts.tz_convert(dateutil.tz.tzutc())
  242. # ------------------------------------------------------------------
  243. # Timestamp.__init__ with tz str or tzinfo
  244. def test_timestamp_constructor_tz_utc(self):
  245. utc_stamp = Timestamp('3/11/2012 05:00', tz='utc')
  246. assert utc_stamp.tzinfo is pytz.utc
  247. assert utc_stamp.hour == 5
  248. utc_stamp = Timestamp('3/11/2012 05:00').tz_localize('utc')
  249. assert utc_stamp.hour == 5
  250. def test_timestamp_to_datetime_tzoffset(self):
  251. tzinfo = tzoffset(None, 7200)
  252. expected = Timestamp('3/11/2012 04:00', tz=tzinfo)
  253. result = Timestamp(expected.to_pydatetime())
  254. assert expected == result
  255. def test_timestamp_constructor_near_dst_boundary(self):
  256. # GH#11481 & GH#15777
  257. # Naive string timestamps were being localized incorrectly
  258. # with tz_convert_single instead of tz_localize_to_utc
  259. for tz in ['Europe/Brussels', 'Europe/Prague']:
  260. result = Timestamp('2015-10-25 01:00', tz=tz)
  261. expected = Timestamp('2015-10-25 01:00').tz_localize(tz)
  262. assert result == expected
  263. with pytest.raises(pytz.AmbiguousTimeError):
  264. Timestamp('2015-10-25 02:00', tz=tz)
  265. result = Timestamp('2017-03-26 01:00', tz='Europe/Paris')
  266. expected = Timestamp('2017-03-26 01:00').tz_localize('Europe/Paris')
  267. assert result == expected
  268. with pytest.raises(pytz.NonExistentTimeError):
  269. Timestamp('2017-03-26 02:00', tz='Europe/Paris')
  270. # GH#11708
  271. naive = Timestamp('2015-11-18 10:00:00')
  272. result = naive.tz_localize('UTC').tz_convert('Asia/Kolkata')
  273. expected = Timestamp('2015-11-18 15:30:00+0530', tz='Asia/Kolkata')
  274. assert result == expected
  275. # GH#15823
  276. result = Timestamp('2017-03-26 00:00', tz='Europe/Paris')
  277. expected = Timestamp('2017-03-26 00:00:00+0100', tz='Europe/Paris')
  278. assert result == expected
  279. result = Timestamp('2017-03-26 01:00', tz='Europe/Paris')
  280. expected = Timestamp('2017-03-26 01:00:00+0100', tz='Europe/Paris')
  281. assert result == expected
  282. with pytest.raises(pytz.NonExistentTimeError):
  283. Timestamp('2017-03-26 02:00', tz='Europe/Paris')
  284. result = Timestamp('2017-03-26 02:00:00+0100', tz='Europe/Paris')
  285. naive = Timestamp(result.value)
  286. expected = naive.tz_localize('UTC').tz_convert('Europe/Paris')
  287. assert result == expected
  288. result = Timestamp('2017-03-26 03:00', tz='Europe/Paris')
  289. expected = Timestamp('2017-03-26 03:00:00+0200', tz='Europe/Paris')
  290. assert result == expected
  291. @pytest.mark.parametrize('tz', [pytz.timezone('US/Eastern'),
  292. gettz('US/Eastern'),
  293. 'US/Eastern', 'dateutil/US/Eastern'])
  294. def test_timestamp_constructed_by_date_and_tz(self, tz):
  295. # GH#2993, Timestamp cannot be constructed by datetime.date
  296. # and tz correctly
  297. result = Timestamp(date(2012, 3, 11), tz=tz)
  298. expected = Timestamp('3/11/2012', tz=tz)
  299. assert result.hour == expected.hour
  300. assert result == expected
  301. @pytest.mark.parametrize('tz', [pytz.timezone('US/Eastern'),
  302. gettz('US/Eastern'),
  303. 'US/Eastern', 'dateutil/US/Eastern'])
  304. def test_timestamp_add_timedelta_push_over_dst_boundary(self, tz):
  305. # GH#1389
  306. # 4 hours before DST transition
  307. stamp = Timestamp('3/10/2012 22:00', tz=tz)
  308. result = stamp + timedelta(hours=6)
  309. # spring forward, + "7" hours
  310. expected = Timestamp('3/11/2012 05:00', tz=tz)
  311. assert result == expected
  312. def test_timestamp_timetz_equivalent_with_datetime_tz(self,
  313. tz_naive_fixture):
  314. # GH21358
  315. tz = timezones.maybe_get_tz(tz_naive_fixture)
  316. stamp = Timestamp('2018-06-04 10:20:30', tz=tz)
  317. _datetime = datetime(2018, 6, 4, hour=10,
  318. minute=20, second=30, tzinfo=tz)
  319. result = stamp.timetz()
  320. expected = _datetime.timetz()
  321. assert result == expected