test_nat.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. from datetime import (
  2. datetime,
  3. timedelta,
  4. )
  5. import operator
  6. import numpy as np
  7. import pytest
  8. import pytz
  9. from pandas._libs.tslibs import iNaT
  10. from pandas.compat.numpy import np_version_gte1p24p3
  11. from pandas.core.dtypes.common import is_datetime64_any_dtype
  12. from pandas import (
  13. DatetimeIndex,
  14. DatetimeTZDtype,
  15. Index,
  16. NaT,
  17. Period,
  18. Series,
  19. Timedelta,
  20. TimedeltaIndex,
  21. Timestamp,
  22. isna,
  23. offsets,
  24. )
  25. import pandas._testing as tm
  26. from pandas.core.arrays import (
  27. DatetimeArray,
  28. PeriodArray,
  29. TimedeltaArray,
  30. )
  31. from pandas.core.ops import roperator
  32. @pytest.mark.parametrize(
  33. "nat,idx",
  34. [
  35. (Timestamp("NaT"), DatetimeArray),
  36. (Timedelta("NaT"), TimedeltaArray),
  37. (Period("NaT", freq="M"), PeriodArray),
  38. ],
  39. )
  40. def test_nat_fields(nat, idx):
  41. for field in idx._field_ops:
  42. # weekday is a property of DTI, but a method
  43. # on NaT/Timestamp for compat with datetime
  44. if field == "weekday":
  45. continue
  46. result = getattr(NaT, field)
  47. assert np.isnan(result)
  48. result = getattr(nat, field)
  49. assert np.isnan(result)
  50. for field in idx._bool_ops:
  51. result = getattr(NaT, field)
  52. assert result is False
  53. result = getattr(nat, field)
  54. assert result is False
  55. def test_nat_vector_field_access():
  56. idx = DatetimeIndex(["1/1/2000", None, None, "1/4/2000"])
  57. for field in DatetimeArray._field_ops:
  58. # weekday is a property of DTI, but a method
  59. # on NaT/Timestamp for compat with datetime
  60. if field == "weekday":
  61. continue
  62. result = getattr(idx, field)
  63. expected = Index([getattr(x, field) for x in idx])
  64. tm.assert_index_equal(result, expected)
  65. ser = Series(idx)
  66. for field in DatetimeArray._field_ops:
  67. # weekday is a property of DTI, but a method
  68. # on NaT/Timestamp for compat with datetime
  69. if field == "weekday":
  70. continue
  71. result = getattr(ser.dt, field)
  72. expected = [getattr(x, field) for x in idx]
  73. tm.assert_series_equal(result, Series(expected))
  74. for field in DatetimeArray._bool_ops:
  75. result = getattr(ser.dt, field)
  76. expected = [getattr(x, field) for x in idx]
  77. tm.assert_series_equal(result, Series(expected))
  78. @pytest.mark.parametrize("klass", [Timestamp, Timedelta, Period])
  79. @pytest.mark.parametrize(
  80. "value", [None, np.nan, iNaT, float("nan"), NaT, "NaT", "nat", "", "NAT"]
  81. )
  82. def test_identity(klass, value):
  83. assert klass(value) is NaT
  84. @pytest.mark.parametrize("klass", [Timestamp, Timedelta])
  85. @pytest.mark.parametrize("method", ["round", "floor", "ceil"])
  86. @pytest.mark.parametrize("freq", ["s", "5s", "min", "5min", "h", "5h"])
  87. def test_round_nat(klass, method, freq):
  88. # see gh-14940
  89. ts = klass("nat")
  90. round_method = getattr(ts, method)
  91. assert round_method(freq) is ts
  92. @pytest.mark.parametrize(
  93. "method",
  94. [
  95. "astimezone",
  96. "combine",
  97. "ctime",
  98. "dst",
  99. "fromordinal",
  100. "fromtimestamp",
  101. "fromisocalendar",
  102. "isocalendar",
  103. "strftime",
  104. "strptime",
  105. "time",
  106. "timestamp",
  107. "timetuple",
  108. "timetz",
  109. "toordinal",
  110. "tzname",
  111. "utcfromtimestamp",
  112. "utcnow",
  113. "utcoffset",
  114. "utctimetuple",
  115. "timestamp",
  116. ],
  117. )
  118. def test_nat_methods_raise(method):
  119. # see gh-9513, gh-17329
  120. msg = f"NaTType does not support {method}"
  121. with pytest.raises(ValueError, match=msg):
  122. getattr(NaT, method)()
  123. @pytest.mark.parametrize("method", ["weekday", "isoweekday"])
  124. def test_nat_methods_nan(method):
  125. # see gh-9513, gh-17329
  126. assert np.isnan(getattr(NaT, method)())
  127. @pytest.mark.parametrize(
  128. "method", ["date", "now", "replace", "today", "tz_convert", "tz_localize"]
  129. )
  130. def test_nat_methods_nat(method):
  131. # see gh-8254, gh-9513, gh-17329
  132. assert getattr(NaT, method)() is NaT
  133. @pytest.mark.parametrize(
  134. "get_nat", [lambda x: NaT, lambda x: Timedelta(x), lambda x: Timestamp(x)]
  135. )
  136. def test_nat_iso_format(get_nat):
  137. # see gh-12300
  138. assert get_nat("NaT").isoformat() == "NaT"
  139. assert get_nat("NaT").isoformat(timespec="nanoseconds") == "NaT"
  140. @pytest.mark.parametrize(
  141. "klass,expected",
  142. [
  143. (Timestamp, ["normalize", "to_julian_date", "to_period", "unit"]),
  144. (
  145. Timedelta,
  146. [
  147. "components",
  148. "resolution_string",
  149. "to_pytimedelta",
  150. "to_timedelta64",
  151. "unit",
  152. "view",
  153. ],
  154. ),
  155. ],
  156. )
  157. def test_missing_public_nat_methods(klass, expected):
  158. # see gh-17327
  159. #
  160. # NaT should have *most* of the Timestamp and Timedelta methods.
  161. # Here, we check which public methods NaT does not have. We
  162. # ignore any missing private methods.
  163. nat_names = dir(NaT)
  164. klass_names = dir(klass)
  165. missing = [x for x in klass_names if x not in nat_names and not x.startswith("_")]
  166. missing.sort()
  167. assert missing == expected
  168. def _get_overlap_public_nat_methods(klass, as_tuple=False):
  169. """
  170. Get overlapping public methods between NaT and another class.
  171. Parameters
  172. ----------
  173. klass : type
  174. The class to compare with NaT
  175. as_tuple : bool, default False
  176. Whether to return a list of tuples of the form (klass, method).
  177. Returns
  178. -------
  179. overlap : list
  180. """
  181. nat_names = dir(NaT)
  182. klass_names = dir(klass)
  183. overlap = [
  184. x
  185. for x in nat_names
  186. if x in klass_names and not x.startswith("_") and callable(getattr(klass, x))
  187. ]
  188. # Timestamp takes precedence over Timedelta in terms of overlap.
  189. if klass is Timedelta:
  190. ts_names = dir(Timestamp)
  191. overlap = [x for x in overlap if x not in ts_names]
  192. if as_tuple:
  193. overlap = [(klass, method) for method in overlap]
  194. overlap.sort()
  195. return overlap
  196. @pytest.mark.parametrize(
  197. "klass,expected",
  198. [
  199. (
  200. Timestamp,
  201. [
  202. "as_unit",
  203. "astimezone",
  204. "ceil",
  205. "combine",
  206. "ctime",
  207. "date",
  208. "day_name",
  209. "dst",
  210. "floor",
  211. "fromisocalendar",
  212. "fromisoformat",
  213. "fromordinal",
  214. "fromtimestamp",
  215. "isocalendar",
  216. "isoformat",
  217. "isoweekday",
  218. "month_name",
  219. "now",
  220. "replace",
  221. "round",
  222. "strftime",
  223. "strptime",
  224. "time",
  225. "timestamp",
  226. "timetuple",
  227. "timetz",
  228. "to_datetime64",
  229. "to_numpy",
  230. "to_pydatetime",
  231. "today",
  232. "toordinal",
  233. "tz_convert",
  234. "tz_localize",
  235. "tzname",
  236. "utcfromtimestamp",
  237. "utcnow",
  238. "utcoffset",
  239. "utctimetuple",
  240. "weekday",
  241. ],
  242. ),
  243. (Timedelta, ["total_seconds"]),
  244. ],
  245. )
  246. def test_overlap_public_nat_methods(klass, expected):
  247. # see gh-17327
  248. #
  249. # NaT should have *most* of the Timestamp and Timedelta methods.
  250. # In case when Timestamp, Timedelta, and NaT are overlap, the overlap
  251. # is considered to be with Timestamp and NaT, not Timedelta.
  252. assert _get_overlap_public_nat_methods(klass) == expected
  253. @pytest.mark.parametrize(
  254. "compare",
  255. (
  256. _get_overlap_public_nat_methods(Timestamp, True)
  257. + _get_overlap_public_nat_methods(Timedelta, True)
  258. ),
  259. ids=lambda x: f"{x[0].__name__}.{x[1]}",
  260. )
  261. def test_nat_doc_strings(compare):
  262. # see gh-17327
  263. #
  264. # The docstrings for overlapping methods should match.
  265. klass, method = compare
  266. klass_doc = getattr(klass, method).__doc__
  267. # Ignore differences with Timestamp.isoformat() as they're intentional
  268. if klass == Timestamp and method == "isoformat":
  269. return
  270. if method == "to_numpy":
  271. # GH#44460 can return either dt64 or td64 depending on dtype,
  272. # different docstring is intentional
  273. return
  274. nat_doc = getattr(NaT, method).__doc__
  275. assert klass_doc == nat_doc
  276. _ops = {
  277. "left_plus_right": lambda a, b: a + b,
  278. "right_plus_left": lambda a, b: b + a,
  279. "left_minus_right": lambda a, b: a - b,
  280. "right_minus_left": lambda a, b: b - a,
  281. "left_times_right": lambda a, b: a * b,
  282. "right_times_left": lambda a, b: b * a,
  283. "left_div_right": lambda a, b: a / b,
  284. "right_div_left": lambda a, b: b / a,
  285. }
  286. @pytest.mark.parametrize("op_name", list(_ops.keys()))
  287. @pytest.mark.parametrize(
  288. "value,val_type",
  289. [
  290. (2, "scalar"),
  291. (1.5, "floating"),
  292. (np.nan, "floating"),
  293. ("foo", "str"),
  294. (timedelta(3600), "timedelta"),
  295. (Timedelta("5s"), "timedelta"),
  296. (datetime(2014, 1, 1), "timestamp"),
  297. (Timestamp("2014-01-01"), "timestamp"),
  298. (Timestamp("2014-01-01", tz="UTC"), "timestamp"),
  299. (Timestamp("2014-01-01", tz="US/Eastern"), "timestamp"),
  300. (pytz.timezone("Asia/Tokyo").localize(datetime(2014, 1, 1)), "timestamp"),
  301. ],
  302. )
  303. def test_nat_arithmetic_scalar(op_name, value, val_type):
  304. # see gh-6873
  305. invalid_ops = {
  306. "scalar": {"right_div_left"},
  307. "floating": {
  308. "right_div_left",
  309. "left_minus_right",
  310. "right_minus_left",
  311. "left_plus_right",
  312. "right_plus_left",
  313. },
  314. "str": set(_ops.keys()),
  315. "timedelta": {"left_times_right", "right_times_left"},
  316. "timestamp": {
  317. "left_times_right",
  318. "right_times_left",
  319. "left_div_right",
  320. "right_div_left",
  321. },
  322. }
  323. op = _ops[op_name]
  324. if op_name in invalid_ops.get(val_type, set()):
  325. if (
  326. val_type == "timedelta"
  327. and "times" in op_name
  328. and isinstance(value, Timedelta)
  329. ):
  330. typs = "(Timedelta|NaTType)"
  331. msg = rf"unsupported operand type\(s\) for \*: '{typs}' and '{typs}'"
  332. elif val_type == "str":
  333. # un-specific check here because the message comes from str
  334. # and varies by method
  335. msg = "|".join(
  336. [
  337. "can only concatenate str",
  338. "unsupported operand type",
  339. "can't multiply sequence",
  340. "Can't convert 'NaTType'",
  341. "must be str, not NaTType",
  342. ]
  343. )
  344. else:
  345. msg = "unsupported operand type"
  346. with pytest.raises(TypeError, match=msg):
  347. op(NaT, value)
  348. else:
  349. if val_type == "timedelta" and "div" in op_name:
  350. expected = np.nan
  351. else:
  352. expected = NaT
  353. assert op(NaT, value) is expected
  354. @pytest.mark.parametrize(
  355. "val,expected", [(np.nan, NaT), (NaT, np.nan), (np.timedelta64("NaT"), np.nan)]
  356. )
  357. def test_nat_rfloordiv_timedelta(val, expected):
  358. # see gh-#18846
  359. #
  360. # See also test_timedelta.TestTimedeltaArithmetic.test_floordiv
  361. td = Timedelta(hours=3, minutes=4)
  362. assert td // val is expected
  363. @pytest.mark.parametrize(
  364. "op_name",
  365. ["left_plus_right", "right_plus_left", "left_minus_right", "right_minus_left"],
  366. )
  367. @pytest.mark.parametrize(
  368. "value",
  369. [
  370. DatetimeIndex(["2011-01-01", "2011-01-02"], name="x"),
  371. DatetimeIndex(["2011-01-01", "2011-01-02"], tz="US/Eastern", name="x"),
  372. DatetimeArray._from_sequence(["2011-01-01", "2011-01-02"]),
  373. DatetimeArray._from_sequence(
  374. ["2011-01-01", "2011-01-02"], dtype=DatetimeTZDtype(tz="US/Pacific")
  375. ),
  376. TimedeltaIndex(["1 day", "2 day"], name="x"),
  377. ],
  378. )
  379. def test_nat_arithmetic_index(op_name, value):
  380. # see gh-11718
  381. exp_name = "x"
  382. exp_data = [NaT] * 2
  383. if is_datetime64_any_dtype(value.dtype) and "plus" in op_name:
  384. expected = DatetimeIndex(exp_data, tz=value.tz, name=exp_name)
  385. else:
  386. expected = TimedeltaIndex(exp_data, name=exp_name)
  387. if not isinstance(value, Index):
  388. expected = expected.array
  389. op = _ops[op_name]
  390. result = op(NaT, value)
  391. tm.assert_equal(result, expected)
  392. @pytest.mark.parametrize(
  393. "op_name",
  394. ["left_plus_right", "right_plus_left", "left_minus_right", "right_minus_left"],
  395. )
  396. @pytest.mark.parametrize("box", [TimedeltaIndex, Series, TimedeltaArray._from_sequence])
  397. def test_nat_arithmetic_td64_vector(op_name, box):
  398. # see gh-19124
  399. vec = box(["1 day", "2 day"], dtype="timedelta64[ns]")
  400. box_nat = box([NaT, NaT], dtype="timedelta64[ns]")
  401. tm.assert_equal(_ops[op_name](vec, NaT), box_nat)
  402. @pytest.mark.parametrize(
  403. "dtype,op,out_dtype",
  404. [
  405. ("datetime64[ns]", operator.add, "datetime64[ns]"),
  406. ("datetime64[ns]", roperator.radd, "datetime64[ns]"),
  407. ("datetime64[ns]", operator.sub, "timedelta64[ns]"),
  408. ("datetime64[ns]", roperator.rsub, "timedelta64[ns]"),
  409. ("timedelta64[ns]", operator.add, "datetime64[ns]"),
  410. ("timedelta64[ns]", roperator.radd, "datetime64[ns]"),
  411. ("timedelta64[ns]", operator.sub, "datetime64[ns]"),
  412. ("timedelta64[ns]", roperator.rsub, "timedelta64[ns]"),
  413. ],
  414. )
  415. def test_nat_arithmetic_ndarray(dtype, op, out_dtype):
  416. other = np.arange(10).astype(dtype)
  417. result = op(NaT, other)
  418. expected = np.empty(other.shape, dtype=out_dtype)
  419. expected.fill("NaT")
  420. tm.assert_numpy_array_equal(result, expected)
  421. def test_nat_pinned_docstrings():
  422. # see gh-17327
  423. assert NaT.ctime.__doc__ == datetime.ctime.__doc__
  424. def test_to_numpy_alias():
  425. # GH 24653: alias .to_numpy() for scalars
  426. expected = NaT.to_datetime64()
  427. result = NaT.to_numpy()
  428. assert isna(expected) and isna(result)
  429. # GH#44460
  430. result = NaT.to_numpy("M8[s]")
  431. assert isinstance(result, np.datetime64)
  432. assert result.dtype == "M8[s]"
  433. result = NaT.to_numpy("m8[ns]")
  434. assert isinstance(result, np.timedelta64)
  435. assert result.dtype == "m8[ns]"
  436. result = NaT.to_numpy("m8[s]")
  437. assert isinstance(result, np.timedelta64)
  438. assert result.dtype == "m8[s]"
  439. with pytest.raises(ValueError, match="NaT.to_numpy dtype must be a "):
  440. NaT.to_numpy(np.int64)
  441. @pytest.mark.parametrize(
  442. "other",
  443. [
  444. Timedelta(0),
  445. Timedelta(0).to_pytimedelta(),
  446. pytest.param(
  447. Timedelta(0).to_timedelta64(),
  448. marks=pytest.mark.xfail(
  449. not np_version_gte1p24p3,
  450. reason="td64 doesn't return NotImplemented, see numpy#17017",
  451. ),
  452. ),
  453. Timestamp(0),
  454. Timestamp(0).to_pydatetime(),
  455. pytest.param(
  456. Timestamp(0).to_datetime64(),
  457. marks=pytest.mark.xfail(
  458. not np_version_gte1p24p3,
  459. reason="dt64 doesn't return NotImplemented, see numpy#17017",
  460. ),
  461. ),
  462. Timestamp(0).tz_localize("UTC"),
  463. NaT,
  464. ],
  465. )
  466. def test_nat_comparisons(compare_operators_no_eq_ne, other):
  467. # GH 26039
  468. opname = compare_operators_no_eq_ne
  469. assert getattr(NaT, opname)(other) is False
  470. op = getattr(operator, opname.strip("_"))
  471. assert op(NaT, other) is False
  472. assert op(other, NaT) is False
  473. @pytest.mark.parametrize("other", [np.timedelta64(0, "ns"), np.datetime64("now", "ns")])
  474. def test_nat_comparisons_numpy(other):
  475. # Once numpy#17017 is fixed and the xfailed cases in test_nat_comparisons
  476. # pass, this test can be removed
  477. assert not NaT == other
  478. assert NaT != other
  479. assert not NaT < other
  480. assert not NaT > other
  481. assert not NaT <= other
  482. assert not NaT >= other
  483. @pytest.mark.parametrize("other_and_type", [("foo", "str"), (2, "int"), (2.0, "float")])
  484. @pytest.mark.parametrize(
  485. "symbol_and_op",
  486. [("<=", operator.le), ("<", operator.lt), (">=", operator.ge), (">", operator.gt)],
  487. )
  488. def test_nat_comparisons_invalid(other_and_type, symbol_and_op):
  489. # GH#35585
  490. other, other_type = other_and_type
  491. symbol, op = symbol_and_op
  492. assert not NaT == other
  493. assert not other == NaT
  494. assert NaT != other
  495. assert other != NaT
  496. msg = f"'{symbol}' not supported between instances of 'NaTType' and '{other_type}'"
  497. with pytest.raises(TypeError, match=msg):
  498. op(NaT, other)
  499. msg = f"'{symbol}' not supported between instances of '{other_type}' and 'NaTType'"
  500. with pytest.raises(TypeError, match=msg):
  501. op(other, NaT)
  502. @pytest.mark.parametrize(
  503. "other",
  504. [
  505. np.array(["foo"] * 2, dtype=object),
  506. np.array([2, 3], dtype="int64"),
  507. np.array([2.0, 3.5], dtype="float64"),
  508. ],
  509. ids=["str", "int", "float"],
  510. )
  511. def test_nat_comparisons_invalid_ndarray(other):
  512. # GH#40722
  513. expected = np.array([False, False])
  514. result = NaT == other
  515. tm.assert_numpy_array_equal(result, expected)
  516. result = other == NaT
  517. tm.assert_numpy_array_equal(result, expected)
  518. expected = np.array([True, True])
  519. result = NaT != other
  520. tm.assert_numpy_array_equal(result, expected)
  521. result = other != NaT
  522. tm.assert_numpy_array_equal(result, expected)
  523. for symbol, op in [
  524. ("<=", operator.le),
  525. ("<", operator.lt),
  526. (">=", operator.ge),
  527. (">", operator.gt),
  528. ]:
  529. msg = f"'{symbol}' not supported between"
  530. with pytest.raises(TypeError, match=msg):
  531. op(NaT, other)
  532. if other.dtype == np.dtype("object"):
  533. # uses the reverse operator, so symbol changes
  534. msg = None
  535. with pytest.raises(TypeError, match=msg):
  536. op(other, NaT)
  537. def test_compare_date(fixed_now_ts):
  538. # GH#39151 comparing NaT with date object is deprecated
  539. # See also: tests.scalar.timestamps.test_comparisons::test_compare_date
  540. dt = fixed_now_ts.to_pydatetime().date()
  541. msg = "Cannot compare NaT with datetime.date object"
  542. for left, right in [(NaT, dt), (dt, NaT)]:
  543. assert not left == right
  544. assert left != right
  545. with pytest.raises(TypeError, match=msg):
  546. left < right
  547. with pytest.raises(TypeError, match=msg):
  548. left <= right
  549. with pytest.raises(TypeError, match=msg):
  550. left > right
  551. with pytest.raises(TypeError, match=msg):
  552. left >= right
  553. @pytest.mark.parametrize(
  554. "obj",
  555. [
  556. offsets.YearEnd(2),
  557. offsets.YearBegin(2),
  558. offsets.MonthBegin(1),
  559. offsets.MonthEnd(2),
  560. offsets.MonthEnd(12),
  561. offsets.Day(2),
  562. offsets.Day(5),
  563. offsets.Hour(24),
  564. offsets.Hour(3),
  565. offsets.Minute(),
  566. np.timedelta64(3, "h"),
  567. np.timedelta64(4, "h"),
  568. np.timedelta64(3200, "s"),
  569. np.timedelta64(3600, "s"),
  570. np.timedelta64(3600 * 24, "s"),
  571. np.timedelta64(2, "D"),
  572. np.timedelta64(365, "D"),
  573. timedelta(-2),
  574. timedelta(365),
  575. timedelta(minutes=120),
  576. timedelta(days=4, minutes=180),
  577. timedelta(hours=23),
  578. timedelta(hours=23, minutes=30),
  579. timedelta(hours=48),
  580. ],
  581. )
  582. def test_nat_addsub_tdlike_scalar(obj):
  583. assert NaT + obj is NaT
  584. assert obj + NaT is NaT
  585. assert NaT - obj is NaT
  586. def test_pickle():
  587. # GH#4606
  588. p = tm.round_trip_pickle(NaT)
  589. assert p is NaT