test_arithmetic.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. from datetime import (
  2. datetime,
  3. timedelta,
  4. timezone,
  5. )
  6. from dateutil.tz import gettz
  7. import numpy as np
  8. import pytest
  9. import pytz
  10. from pandas._libs.tslibs import (
  11. OutOfBoundsDatetime,
  12. OutOfBoundsTimedelta,
  13. Timedelta,
  14. Timestamp,
  15. offsets,
  16. to_offset,
  17. )
  18. import pandas._testing as tm
  19. class TestTimestampArithmetic:
  20. def test_overflow_offset(self):
  21. # no overflow expected
  22. stamp = Timestamp("2000/1/1")
  23. offset_no_overflow = to_offset("D") * 100
  24. expected = Timestamp("2000/04/10")
  25. assert stamp + offset_no_overflow == expected
  26. assert offset_no_overflow + stamp == expected
  27. expected = Timestamp("1999/09/23")
  28. assert stamp - offset_no_overflow == expected
  29. def test_overflow_offset_raises(self):
  30. # xref https://github.com/statsmodels/statsmodels/issues/3374
  31. # ends up multiplying really large numbers which overflow
  32. stamp = Timestamp("2017-01-13 00:00:00").as_unit("ns")
  33. offset_overflow = 20169940 * offsets.Day(1)
  34. lmsg2 = r"Cannot cast -?20169940 days \+?00:00:00 to unit='ns' without overflow"
  35. with pytest.raises(OutOfBoundsTimedelta, match=lmsg2):
  36. stamp + offset_overflow
  37. with pytest.raises(OutOfBoundsTimedelta, match=lmsg2):
  38. offset_overflow + stamp
  39. with pytest.raises(OutOfBoundsTimedelta, match=lmsg2):
  40. stamp - offset_overflow
  41. # xref https://github.com/pandas-dev/pandas/issues/14080
  42. # used to crash, so check for proper overflow exception
  43. stamp = Timestamp("2000/1/1").as_unit("ns")
  44. offset_overflow = to_offset("D") * 100**5
  45. lmsg3 = (
  46. r"Cannot cast -?10000000000 days \+?00:00:00 to unit='ns' without overflow"
  47. )
  48. with pytest.raises(OutOfBoundsTimedelta, match=lmsg3):
  49. stamp + offset_overflow
  50. with pytest.raises(OutOfBoundsTimedelta, match=lmsg3):
  51. offset_overflow + stamp
  52. with pytest.raises(OutOfBoundsTimedelta, match=lmsg3):
  53. stamp - offset_overflow
  54. def test_overflow_timestamp_raises(self):
  55. # https://github.com/pandas-dev/pandas/issues/31774
  56. msg = "Result is too large"
  57. a = Timestamp("2101-01-01 00:00:00").as_unit("ns")
  58. b = Timestamp("1688-01-01 00:00:00").as_unit("ns")
  59. with pytest.raises(OutOfBoundsDatetime, match=msg):
  60. a - b
  61. # but we're OK for timestamp and datetime.datetime
  62. assert (a - b.to_pydatetime()) == (a.to_pydatetime() - b)
  63. def test_delta_preserve_nanos(self):
  64. val = Timestamp(1337299200000000123)
  65. result = val + timedelta(1)
  66. assert result.nanosecond == val.nanosecond
  67. def test_rsub_dtscalars(self, tz_naive_fixture):
  68. # In particular, check that datetime64 - Timestamp works GH#28286
  69. td = Timedelta(1235345642000)
  70. ts = Timestamp("2021-01-01", tz=tz_naive_fixture)
  71. other = ts + td
  72. assert other - ts == td
  73. assert other.to_pydatetime() - ts == td
  74. if tz_naive_fixture is None:
  75. assert other.to_datetime64() - ts == td
  76. else:
  77. msg = "Cannot subtract tz-naive and tz-aware datetime-like objects"
  78. with pytest.raises(TypeError, match=msg):
  79. other.to_datetime64() - ts
  80. def test_timestamp_sub_datetime(self):
  81. dt = datetime(2013, 10, 12)
  82. ts = Timestamp(datetime(2013, 10, 13))
  83. assert (ts - dt).days == 1
  84. assert (dt - ts).days == -1
  85. def test_subtract_tzaware_datetime(self):
  86. t1 = Timestamp("2020-10-22T22:00:00+00:00")
  87. t2 = datetime(2020, 10, 22, 22, tzinfo=timezone.utc)
  88. result = t1 - t2
  89. assert isinstance(result, Timedelta)
  90. assert result == Timedelta("0 days")
  91. def test_subtract_timestamp_from_different_timezone(self):
  92. t1 = Timestamp("20130101").tz_localize("US/Eastern")
  93. t2 = Timestamp("20130101").tz_localize("CET")
  94. result = t1 - t2
  95. assert isinstance(result, Timedelta)
  96. assert result == Timedelta("0 days 06:00:00")
  97. def test_subtracting_involving_datetime_with_different_tz(self):
  98. t1 = datetime(2013, 1, 1, tzinfo=timezone(timedelta(hours=-5)))
  99. t2 = Timestamp("20130101").tz_localize("CET")
  100. result = t1 - t2
  101. assert isinstance(result, Timedelta)
  102. assert result == Timedelta("0 days 06:00:00")
  103. result = t2 - t1
  104. assert isinstance(result, Timedelta)
  105. assert result == Timedelta("-1 days +18:00:00")
  106. def test_subtracting_different_timezones(self, tz_aware_fixture):
  107. t_raw = Timestamp("20130101")
  108. t_UTC = t_raw.tz_localize("UTC")
  109. t_diff = t_UTC.tz_convert(tz_aware_fixture) + Timedelta("0 days 05:00:00")
  110. result = t_diff - t_UTC
  111. assert isinstance(result, Timedelta)
  112. assert result == Timedelta("0 days 05:00:00")
  113. def test_addition_subtraction_types(self):
  114. # Assert on the types resulting from Timestamp +/- various date/time
  115. # objects
  116. dt = datetime(2014, 3, 4)
  117. td = timedelta(seconds=1)
  118. ts = Timestamp(dt)
  119. msg = "Addition/subtraction of integers"
  120. with pytest.raises(TypeError, match=msg):
  121. # GH#22535 add/sub with integers is deprecated
  122. ts + 1
  123. with pytest.raises(TypeError, match=msg):
  124. ts - 1
  125. # Timestamp + datetime not supported, though subtraction is supported
  126. # and yields timedelta more tests in tseries/base/tests/test_base.py
  127. assert type(ts - dt) == Timedelta
  128. assert type(ts + td) == Timestamp
  129. assert type(ts - td) == Timestamp
  130. # Timestamp +/- datetime64 not supported, so not tested (could possibly
  131. # assert error raised?)
  132. td64 = np.timedelta64(1, "D")
  133. assert type(ts + td64) == Timestamp
  134. assert type(ts - td64) == Timestamp
  135. @pytest.mark.parametrize(
  136. "td", [Timedelta(hours=3), np.timedelta64(3, "h"), timedelta(hours=3)]
  137. )
  138. def test_radd_tdscalar(self, td, fixed_now_ts):
  139. # GH#24775 timedelta64+Timestamp should not raise
  140. ts = fixed_now_ts
  141. assert td + ts == ts + td
  142. @pytest.mark.parametrize(
  143. "other,expected_difference",
  144. [
  145. (np.timedelta64(-123, "ns"), -123),
  146. (np.timedelta64(1234567898, "ns"), 1234567898),
  147. (np.timedelta64(-123, "us"), -123000),
  148. (np.timedelta64(-123, "ms"), -123000000),
  149. ],
  150. )
  151. def test_timestamp_add_timedelta64_unit(self, other, expected_difference):
  152. now = datetime.now(timezone.utc)
  153. ts = Timestamp(now).as_unit("ns")
  154. result = ts + other
  155. valdiff = result._value - ts._value
  156. assert valdiff == expected_difference
  157. ts2 = Timestamp(now)
  158. assert ts2 + other == result
  159. @pytest.mark.parametrize(
  160. "ts",
  161. [
  162. Timestamp("1776-07-04"),
  163. Timestamp("1776-07-04", tz="UTC"),
  164. ],
  165. )
  166. @pytest.mark.parametrize(
  167. "other",
  168. [
  169. 1,
  170. np.int64(1),
  171. np.array([1, 2], dtype=np.int32),
  172. np.array([3, 4], dtype=np.uint64),
  173. ],
  174. )
  175. def test_add_int_with_freq(self, ts, other):
  176. msg = "Addition/subtraction of integers and integer-arrays"
  177. with pytest.raises(TypeError, match=msg):
  178. ts + other
  179. with pytest.raises(TypeError, match=msg):
  180. other + ts
  181. with pytest.raises(TypeError, match=msg):
  182. ts - other
  183. msg = "unsupported operand type"
  184. with pytest.raises(TypeError, match=msg):
  185. other - ts
  186. @pytest.mark.parametrize("shape", [(6,), (2, 3)])
  187. def test_addsub_m8ndarray(self, shape):
  188. # GH#33296
  189. ts = Timestamp("2020-04-04 15:45").as_unit("ns")
  190. other = np.arange(6).astype("m8[h]").reshape(shape)
  191. result = ts + other
  192. ex_stamps = [ts + Timedelta(hours=n) for n in range(6)]
  193. expected = np.array([x.asm8 for x in ex_stamps], dtype="M8[ns]").reshape(shape)
  194. tm.assert_numpy_array_equal(result, expected)
  195. result = other + ts
  196. tm.assert_numpy_array_equal(result, expected)
  197. result = ts - other
  198. ex_stamps = [ts - Timedelta(hours=n) for n in range(6)]
  199. expected = np.array([x.asm8 for x in ex_stamps], dtype="M8[ns]").reshape(shape)
  200. tm.assert_numpy_array_equal(result, expected)
  201. msg = r"unsupported operand type\(s\) for -: 'numpy.ndarray' and 'Timestamp'"
  202. with pytest.raises(TypeError, match=msg):
  203. other - ts
  204. @pytest.mark.parametrize("shape", [(6,), (2, 3)])
  205. def test_addsub_m8ndarray_tzaware(self, shape):
  206. # GH#33296
  207. ts = Timestamp("2020-04-04 15:45", tz="US/Pacific")
  208. other = np.arange(6).astype("m8[h]").reshape(shape)
  209. result = ts + other
  210. ex_stamps = [ts + Timedelta(hours=n) for n in range(6)]
  211. expected = np.array(ex_stamps).reshape(shape)
  212. tm.assert_numpy_array_equal(result, expected)
  213. result = other + ts
  214. tm.assert_numpy_array_equal(result, expected)
  215. result = ts - other
  216. ex_stamps = [ts - Timedelta(hours=n) for n in range(6)]
  217. expected = np.array(ex_stamps).reshape(shape)
  218. tm.assert_numpy_array_equal(result, expected)
  219. msg = r"unsupported operand type\(s\) for -: 'numpy.ndarray' and 'Timestamp'"
  220. with pytest.raises(TypeError, match=msg):
  221. other - ts
  222. def test_subtract_different_utc_objects(self, utc_fixture, utc_fixture2):
  223. # GH 32619
  224. dt = datetime(2021, 1, 1)
  225. ts1 = Timestamp(dt, tz=utc_fixture)
  226. ts2 = Timestamp(dt, tz=utc_fixture2)
  227. result = ts1 - ts2
  228. expected = Timedelta(0)
  229. assert result == expected
  230. @pytest.mark.parametrize(
  231. "tz",
  232. [
  233. pytz.timezone("US/Eastern"),
  234. gettz("US/Eastern"),
  235. "US/Eastern",
  236. "dateutil/US/Eastern",
  237. ],
  238. )
  239. def test_timestamp_add_timedelta_push_over_dst_boundary(self, tz):
  240. # GH#1389
  241. # 4 hours before DST transition
  242. stamp = Timestamp("3/10/2012 22:00", tz=tz)
  243. result = stamp + timedelta(hours=6)
  244. # spring forward, + "7" hours
  245. expected = Timestamp("3/11/2012 05:00", tz=tz)
  246. assert result == expected
  247. class SubDatetime(datetime):
  248. pass
  249. @pytest.mark.parametrize(
  250. "lh,rh",
  251. [
  252. (SubDatetime(2000, 1, 1), Timedelta(hours=1)),
  253. (Timedelta(hours=1), SubDatetime(2000, 1, 1)),
  254. ],
  255. )
  256. def test_dt_subclass_add_timedelta(lh, rh):
  257. # GH#25851
  258. # ensure that subclassed datetime works for
  259. # Timedelta operations
  260. result = lh + rh
  261. expected = SubDatetime(2000, 1, 1, 1)
  262. assert result == expected