diff --git a/pandas/tests/scalar/test_nat.py b/pandas/tests/scalar/test_nat.py index d2a31de5c09388..2f6ad8fb103947 100644 --- a/pandas/tests/scalar/test_nat.py +++ b/pandas/tests/scalar/test_nat.py @@ -8,21 +8,20 @@ from pandas import ( DatetimeIndex, Index, NaT, Period, Series, Timedelta, TimedeltaIndex, - Timestamp, isna) + Timestamp) from pandas.core.arrays import PeriodArray from pandas.util import testing as tm -@pytest.mark.parametrize('nat, idx', [(Timestamp('NaT'), DatetimeIndex), - (Timedelta('NaT'), TimedeltaIndex), - (Period('NaT', freq='M'), PeriodArray)]) +@pytest.mark.parametrize("nat,idx", [(Timestamp("NaT"), DatetimeIndex), + (Timedelta("NaT"), TimedeltaIndex), + (Period("NaT", freq="M"), PeriodArray)]) def test_nat_fields(nat, idx): for field in idx._field_ops: - # weekday is a property of DTI, but a method # on NaT/Timestamp for compat with datetime - if field == 'weekday': + if field == "weekday": continue result = getattr(NaT, field) @@ -41,289 +40,296 @@ def test_nat_fields(nat, idx): def test_nat_vector_field_access(): - idx = DatetimeIndex(['1/1/2000', None, None, '1/4/2000']) + idx = DatetimeIndex(["1/1/2000", None, None, "1/4/2000"]) for field in DatetimeIndex._field_ops: # weekday is a property of DTI, but a method # on NaT/Timestamp for compat with datetime - if field == 'weekday': + if field == "weekday": continue result = getattr(idx, field) expected = Index([getattr(x, field) for x in idx]) tm.assert_index_equal(result, expected) - s = Series(idx) + ser = Series(idx) for field in DatetimeIndex._field_ops: - # weekday is a property of DTI, but a method # on NaT/Timestamp for compat with datetime - if field == 'weekday': + if field == "weekday": continue - result = getattr(s.dt, field) + result = getattr(ser.dt, field) expected = [getattr(x, field) for x in idx] tm.assert_series_equal(result, Series(expected)) for field in DatetimeIndex._bool_ops: - result = getattr(s.dt, field) + result = getattr(ser.dt, field) expected = [getattr(x, field) for x in idx] tm.assert_series_equal(result, Series(expected)) -@pytest.mark.parametrize('klass', [Timestamp, Timedelta, Period]) -def test_identity(klass): - assert klass(None) is NaT - - result = klass(np.nan) - assert result is NaT - - result = klass(None) - assert result is NaT - - result = klass(iNaT) - assert result is NaT - - result = klass(np.nan) - assert result is NaT - - result = klass(float('nan')) - assert result is NaT - - result = klass(NaT) - assert result is NaT - - result = klass('NaT') - assert result is NaT - - assert isna(klass('nat')) - - -@pytest.mark.parametrize('klass', [Timestamp, Timedelta, Period]) -def test_equality(klass): - - # nat - if klass is not Period: - klass('').value == iNaT - klass('nat').value == iNaT - klass('NAT').value == iNaT - klass(None).value == iNaT - klass(np.nan).value == iNaT - assert isna(klass('nat')) - - -@pytest.mark.parametrize('klass', [Timestamp, Timedelta]) -def test_round_nat(klass): - # GH14940 - ts = klass('nat') - for method in ["round", "floor", "ceil"]: - round_method = getattr(ts, method) - for freq in ["s", "5s", "min", "5min", "h", "5h"]: - assert round_method(freq) is ts - - -def test_NaT_methods(): - # GH 9513 - # GH 17329 for `timestamp` - raise_methods = ['astimezone', 'combine', 'ctime', 'dst', - 'fromordinal', 'fromtimestamp', 'isocalendar', - 'strftime', 'strptime', 'time', 'timestamp', - 'timetuple', 'timetz', 'toordinal', 'tzname', - 'utcfromtimestamp', 'utcnow', 'utcoffset', - 'utctimetuple', 'timestamp'] - nat_methods = ['date', 'now', 'replace', 'to_datetime', 'today', - 'tz_convert', 'tz_localize'] - nan_methods = ['weekday', 'isoweekday'] +@pytest.mark.parametrize("klass", [Timestamp, Timedelta, Period]) +@pytest.mark.parametrize("value", [None, np.nan, iNaT, float("nan"), + NaT, "NaT", "nat"]) +def test_identity(klass, value): + assert klass(value) is NaT + + +@pytest.mark.parametrize("klass", [Timestamp, Timedelta, Period]) +@pytest.mark.parametrize("value", ["", "nat", "NAT", None, np.nan]) +def test_equality(klass, value): + if klass is Period and value == "": + pytest.skip("Period cannot parse empty string") + + assert klass(value).value == iNaT + + +@pytest.mark.parametrize("klass", [Timestamp, Timedelta]) +@pytest.mark.parametrize("method", ["round", "floor", "ceil"]) +@pytest.mark.parametrize("freq", ["s", "5s", "min", "5min", "h", "5h"]) +def test_round_nat(klass, method, freq): + # see gh-14940 + ts = klass("nat") + + round_method = getattr(ts, method) + assert round_method(freq) is ts + + +@pytest.mark.parametrize("method", [ + "astimezone", "combine", "ctime", "dst", "fromordinal", + "fromtimestamp", "isocalendar", "strftime", "strptime", + "time", "timestamp", "timetuple", "timetz", "toordinal", + "tzname", "utcfromtimestamp", "utcnow", "utcoffset", + "utctimetuple", "timestamp" +]) +def test_nat_methods_raise(method): + # see gh-9513, gh-17329 + msg = "NaTType does not support {method}".format(method=method) + + with pytest.raises(ValueError, match=msg): + getattr(NaT, method)() + + +@pytest.mark.parametrize("method", [ + "weekday", "isoweekday" +]) +def test_nat_methods_nan(method): + # see gh-9513, gh-17329 + assert np.isnan(getattr(NaT, method)()) + + +@pytest.mark.parametrize("method", [ + "date", "now", "replace", "today", + "tz_convert", "tz_localize" +]) +def test_nat_methods_nat(method): + # see gh-8254, gh-9513, gh-17329 + assert getattr(NaT, method)() is NaT + + +@pytest.mark.parametrize("get_nat", [ + lambda x: NaT, + lambda x: Timedelta(x), + lambda x: Timestamp(x) +]) +def test_nat_iso_format(get_nat): + # see gh-12300 + assert get_nat("NaT").isoformat() == "NaT" + + +@pytest.mark.parametrize("klass,expected", [ + (Timestamp, ["freqstr", "normalize", "to_julian_date", "to_period", "tz"]), + (Timedelta, ["components", "delta", "is_populated", "to_pytimedelta", + "to_timedelta64", "view"]) +]) +def test_missing_public_nat_methods(klass, expected): + # see gh-17327 + # + # NaT should have *most* of the Timestamp and Timedelta methods. + # Here, we check which public methods NaT does not have. We + # ignore any missing private methods. + nat_names = dir(NaT) + klass_names = dir(klass) - for method in raise_methods: - if hasattr(NaT, method): - with pytest.raises(ValueError): - getattr(NaT, method)() + missing = [x for x in klass_names if x not in nat_names and + not x.startswith("_")] + missing.sort() - for method in nan_methods: - if hasattr(NaT, method): - assert np.isnan(getattr(NaT, method)()) + assert missing == expected - for method in nat_methods: - if hasattr(NaT, method): - # see gh-8254 - exp_warning = None - if method == 'to_datetime': - exp_warning = FutureWarning - with tm.assert_produces_warning( - exp_warning, check_stacklevel=False): - assert getattr(NaT, method)() is NaT - # GH 12300 - assert NaT.isoformat() == 'NaT' +def _get_overlap_public_nat_methods(klass, as_tuple=False): + """ + Get overlapping public methods between NaT and another class. + Parameters + ---------- + klass : type + The class to compare with NaT + as_tuple : bool, default False + Whether to return a list of tuples of the form (klass, method). -def test_NaT_docstrings(): - # GH#17327 + Returns + ------- + overlap : list + """ nat_names = dir(NaT) - - # NaT should have *most* of the Timestamp methods, with matching - # docstrings. The attributes that are not expected to be present in NaT - # are private methods plus `ts_expected` below. - ts_names = dir(Timestamp) - ts_missing = [x for x in ts_names if x not in nat_names and - not x.startswith('_')] - ts_missing.sort() - ts_expected = ['freqstr', 'normalize', - 'to_julian_date', - 'to_period', 'tz'] - assert ts_missing == ts_expected - - ts_overlap = [x for x in nat_names if x in ts_names and - not x.startswith('_') and - callable(getattr(Timestamp, x))] - for name in ts_overlap: - tsdoc = getattr(Timestamp, name).__doc__ - natdoc = getattr(NaT, name).__doc__ - assert tsdoc == natdoc - - # NaT should have *most* of the Timedelta methods, with matching - # docstrings. The attributes that are not expected to be present in NaT - # are private methods plus `td_expected` below. - # For methods that are both Timestamp and Timedelta methods, the - # Timestamp docstring takes priority. - td_names = dir(Timedelta) - td_missing = [x for x in td_names if x not in nat_names and - not x.startswith('_')] - td_missing.sort() - td_expected = ['components', 'delta', 'is_populated', - 'to_pytimedelta', 'to_timedelta64', 'view'] - assert td_missing == td_expected - - td_overlap = [x for x in nat_names if x in td_names and - x not in ts_names and # Timestamp __doc__ takes priority - not x.startswith('_') and - callable(getattr(Timedelta, x))] - assert td_overlap == ['total_seconds'] - for name in td_overlap: - tddoc = getattr(Timedelta, name).__doc__ - natdoc = getattr(NaT, name).__doc__ - assert tddoc == natdoc - - -@pytest.mark.parametrize('klass', [Timestamp, Timedelta]) -def test_isoformat(klass): - - result = klass('NaT').isoformat() - expected = 'NaT' - assert result == expected - - -def test_nat_arithmetic(): - # GH 6873 - i = 2 - f = 1.5 - - for (left, right) in [(NaT, i), (NaT, f), (NaT, np.nan)]: - assert left / right is NaT - assert left * right is NaT - assert right * left is NaT - with pytest.raises(TypeError): - right / left - - # Timestamp / datetime - t = Timestamp('2014-01-01') - dt = datetime(2014, 1, 1) - for (left, right) in [(NaT, NaT), (NaT, t), (NaT, dt)]: - # NaT __add__ or __sub__ Timestamp-like (or inverse) returns NaT - assert right + left is NaT - assert left + right is NaT - assert left - right is NaT - assert right - left is NaT - - # timedelta-like - # offsets are tested in test_offsets.py - - delta = timedelta(3600) - td = Timedelta('5s') - - for (left, right) in [(NaT, delta), (NaT, td)]: - # NaT + timedelta-like returns NaT - assert right + left is NaT - assert left + right is NaT - assert right - left is NaT - assert left - right is NaT - assert np.isnan(left / right) - assert np.isnan(right / left) - - # GH 11718 - t_utc = Timestamp('2014-01-01', tz='UTC') - t_tz = Timestamp('2014-01-01', tz='US/Eastern') - dt_tz = pytz.timezone('Asia/Tokyo').localize(dt) - - for (left, right) in [(NaT, t_utc), (NaT, t_tz), - (NaT, dt_tz)]: - # NaT __add__ or __sub__ Timestamp-like (or inverse) returns NaT - assert right + left is NaT - assert left + right is NaT - assert left - right is NaT - assert right - left is NaT - - # int addition / subtraction - for (left, right) in [(NaT, 2), (NaT, 0), (NaT, -3)]: - assert right + left is NaT - assert left + right is NaT - assert left - right is NaT - assert right - left is NaT - - -def test_nat_rfloordiv_timedelta(): - # GH#18846 + klass_names = dir(klass) + + overlap = [x for x in nat_names if x in klass_names and + not x.startswith("_") and + callable(getattr(klass, x))] + + # Timestamp takes precedence over Timedelta in terms of overlap. + if klass is Timedelta: + ts_names = dir(Timestamp) + overlap = [x for x in overlap if x not in ts_names] + + if as_tuple: + overlap = [(klass, method) for method in overlap] + + overlap.sort() + return overlap + + +@pytest.mark.parametrize("klass,expected", [ + (Timestamp, ["astimezone", "ceil", "combine", "ctime", "date", "day_name", + "dst", "floor", "fromisoformat", "fromordinal", + "fromtimestamp", "isocalendar", "isoformat", "isoweekday", + "month_name", "now", "replace", "round", "strftime", + "strptime", "time", "timestamp", "timetuple", "timetz", + "to_datetime64", "to_pydatetime", "today", "toordinal", + "tz_convert", "tz_localize", "tzname", "utcfromtimestamp", + "utcnow", "utcoffset", "utctimetuple", "weekday"]), + (Timedelta, ["total_seconds"]) +]) +def test_overlap_public_nat_methods(klass, expected): + # see gh-17327 + # + # NaT should have *most* of the Timestamp and Timedelta methods. + # In case when Timestamp, Timedelta, and NaT are overlap, the overlap + # is considered to be with Timestamp and NaT, not Timedelta. + assert _get_overlap_public_nat_methods(klass) == expected + + +@pytest.mark.parametrize("compare", ( + _get_overlap_public_nat_methods(Timestamp, True) + + _get_overlap_public_nat_methods(Timedelta, True)) +) +def test_nat_doc_strings(compare): + # see gh-17327 + # + # The docstrings for overlapping methods should match. + klass, method = compare + klass_doc = getattr(klass, method).__doc__ + + nat_doc = getattr(NaT, method).__doc__ + assert klass_doc == nat_doc + + +_ops = { + "left_plus_right": lambda a, b: a + b, + "right_plus_left": lambda a, b: b + a, + "left_minus_right": lambda a, b: a - b, + "right_minus_left": lambda a, b: b - a, + "left_times_right": lambda a, b: a * b, + "right_times_left": lambda a, b: b * a, + "left_div_right": lambda a, b: a / b, + "right_div_left": lambda a, b: b / a, +} + + +@pytest.mark.parametrize("op_name", list(_ops.keys())) +@pytest.mark.parametrize("value,val_type", [ + (2, "scalar"), + (1.5, "scalar"), + (np.nan, "scalar"), + (timedelta(3600), "timedelta"), + (Timedelta("5s"), "timedelta"), + (datetime(2014, 1, 1), "timestamp"), + (Timestamp("2014-01-01"), "timestamp"), + (Timestamp("2014-01-01", tz="UTC"), "timestamp"), + (Timestamp("2014-01-01", tz="US/Eastern"), "timestamp"), + (pytz.timezone("Asia/Tokyo").localize(datetime(2014, 1, 1)), "timestamp"), +]) +def test_nat_arithmetic_scalar(op_name, value, val_type): + # see gh-6873 + invalid_ops = { + "scalar": {"right_div_left"}, + "timedelta": {"left_times_right", "right_times_left"}, + "timestamp": {"left_times_right", "right_times_left", + "left_div_right", "right_div_left"} + } + + op = _ops[op_name] + + if op_name in invalid_ops.get(val_type, set()): + if (val_type == "timedelta" and "times" in op_name and + isinstance(value, Timedelta)): + msg = "Cannot multiply" + else: + msg = "unsupported operand type" + + with pytest.raises(TypeError, match=msg): + op(NaT, value) + else: + if val_type == "timedelta" and "div" in op_name: + expected = np.nan + else: + expected = NaT + + assert op(NaT, value) is expected + + +@pytest.mark.parametrize("val,expected", [ + (np.nan, NaT), + (NaT, np.nan), + (np.timedelta64("NaT"), np.nan) +]) +def test_nat_rfloordiv_timedelta(val, expected): + # see gh-#18846 + # # See also test_timedelta.TestTimedeltaArithmetic.test_floordiv td = Timedelta(hours=3, minutes=4) - - assert td // np.nan is NaT - assert np.isnan(td // NaT) - assert np.isnan(td // np.timedelta64('NaT')) - - -def test_nat_arithmetic_index(): - # GH 11718 - - dti = DatetimeIndex(['2011-01-01', '2011-01-02'], name='x') - exp = DatetimeIndex([NaT, NaT], name='x') - tm.assert_index_equal(dti + NaT, exp) - tm.assert_index_equal(NaT + dti, exp) - - dti_tz = DatetimeIndex(['2011-01-01', '2011-01-02'], - tz='US/Eastern', name='x') - exp = DatetimeIndex([NaT, NaT], name='x', tz='US/Eastern') - tm.assert_index_equal(dti_tz + NaT, exp) - tm.assert_index_equal(NaT + dti_tz, exp) - - exp = TimedeltaIndex([NaT, NaT], name='x') - for (left, right) in [(NaT, dti), (NaT, dti_tz)]: - tm.assert_index_equal(left - right, exp) - tm.assert_index_equal(right - left, exp) - - # timedelta # GH#19124 - tdi = TimedeltaIndex(['1 day', '2 day'], name='x') - tdi_nat = TimedeltaIndex([NaT, NaT], name='x') - - tm.assert_index_equal(tdi + NaT, tdi_nat) - tm.assert_index_equal(NaT + tdi, tdi_nat) - tm.assert_index_equal(tdi - NaT, tdi_nat) - tm.assert_index_equal(NaT - tdi, tdi_nat) - - -@pytest.mark.parametrize('box', [TimedeltaIndex, Series]) -def test_nat_arithmetic_td64_vector(box): - # GH#19124 - vec = box(['1 day', '2 day'], dtype='timedelta64[ns]') - box_nat = box([NaT, NaT], dtype='timedelta64[ns]') - - tm.assert_equal(vec + NaT, box_nat) - tm.assert_equal(NaT + vec, box_nat) - tm.assert_equal(vec - NaT, box_nat) - tm.assert_equal(NaT - vec, box_nat) + assert td // val is expected + + +@pytest.mark.parametrize("op_name", [ + "left_plus_right", "right_plus_left", + "left_minus_right", "right_minus_left" +]) +@pytest.mark.parametrize("value", [ + DatetimeIndex(["2011-01-01", "2011-01-02"], name="x"), + DatetimeIndex(["2011-01-01", "2011-01-02"], name="x"), + TimedeltaIndex(["1 day", "2 day"], name="x"), +]) +def test_nat_arithmetic_index(op_name, value): + # see gh-11718 + exp_name = "x" + exp_data = [NaT] * 2 + + if isinstance(value, DatetimeIndex) and "plus" in op_name: + expected = DatetimeIndex(exp_data, name=exp_name, tz=value.tz) + else: + expected = TimedeltaIndex(exp_data, name=exp_name) + + tm.assert_index_equal(_ops[op_name](NaT, value), expected) + + +@pytest.mark.parametrize("op_name", [ + "left_plus_right", "right_plus_left", + "left_minus_right", "right_minus_left" +]) +@pytest.mark.parametrize("box", [TimedeltaIndex, Series]) +def test_nat_arithmetic_td64_vector(op_name, box): + # see gh-19124 + vec = box(["1 day", "2 day"], dtype="timedelta64[ns]") + box_nat = box([NaT, NaT], dtype="timedelta64[ns]") + tm.assert_equal(_ops[op_name](vec, NaT), box_nat) def test_nat_pinned_docstrings(): - # GH17327 + # see gh-17327 assert NaT.ctime.__doc__ == datetime.ctime.__doc__