Skip to content

Commit b0316a6

Browse files
committed
ENH: Timestamp.__sub__(datetime) with non-nano
1 parent 903237a commit b0316a6

File tree

2 files changed

+69
-10
lines changed

2 files changed

+69
-10
lines changed

pandas/_libs/tslibs/timestamps.pyx

+9-10
Original file line numberDiff line numberDiff line change
@@ -413,9 +413,6 @@ cdef class _Timestamp(ABCTimestamp):
413413
result._set_freq(self._freq) # avoid warning in constructor
414414
return result
415415

416-
elif isinstance(self, _Timestamp) and self._reso != NPY_FR_ns:
417-
raise NotImplementedError(self._reso)
418-
419416
elif is_integer_object(other):
420417
raise integer_op_not_supported(self)
421418

@@ -450,9 +447,6 @@ cdef class _Timestamp(ABCTimestamp):
450447
neg_other = -other
451448
return self + neg_other
452449

453-
elif isinstance(self, _Timestamp) and self._reso != NPY_FR_ns:
454-
raise NotImplementedError(self._reso)
455-
456450
elif is_array(other):
457451
if other.dtype.kind in ['i', 'u']:
458452
raise integer_op_not_supported(self)
@@ -483,10 +477,18 @@ cdef class _Timestamp(ABCTimestamp):
483477
"Cannot subtract tz-naive and tz-aware datetime-like objects."
484478
)
485479

480+
# We allow silent casting to the lower resolution if and only
481+
# if it is lossless.
482+
if self._reso < other._reso:
483+
other = (<_Timestamp>other)._as_reso(self._reso, round_ok=False)
484+
elif self._reso > other._reso:
485+
self = (<_Timestamp>self)._as_reso(other._reso, round_ok=False)
486+
486487
# scalar Timestamp/datetime - Timestamp/datetime -> yields a
487488
# Timedelta
488489
try:
489-
return Timedelta(self.value - other.value)
490+
res_value = self.value - other.value
491+
return Timedelta._from_value_and_reso(res_value, self._reso)
490492
except (OverflowError, OutOfBoundsDatetime, OutOfBoundsTimedelta) as err:
491493
if isinstance(other, _Timestamp):
492494
if both_timestamps:
@@ -507,9 +509,6 @@ cdef class _Timestamp(ABCTimestamp):
507509
return NotImplemented
508510

509511
def __rsub__(self, other):
510-
if self._reso != NPY_FR_ns:
511-
raise NotImplementedError(self._reso)
512-
513512
if PyDateTime_Check(other):
514513
try:
515514
return type(self)(other) - self

pandas/tests/scalar/timestamp/test_timestamp.py

+60
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from pandas._libs.tslibs.timezones import (
2323
dateutil_gettz as gettz,
2424
get_timezone,
25+
maybe_get_tz,
2526
tz_compare,
2627
)
2728
from pandas.errors import OutOfBoundsDatetime
@@ -712,6 +713,11 @@ def dt64(self, reso):
712713
def ts(self, dt64):
713714
return Timestamp._from_dt64(dt64)
714715

716+
@pytest.fixture
717+
def ts_tz(self, ts, tz_aware_fixture):
718+
tz = maybe_get_tz(tz_aware_fixture)
719+
return Timestamp._from_value_and_reso(ts.value, ts._reso, tz)
720+
715721
def test_non_nano_construction(self, dt64, ts, reso):
716722
assert ts.value == dt64.view("i8")
717723

@@ -893,6 +899,60 @@ def test_addsub_timedeltalike_non_nano(self, dt64, ts, td):
893899
assert result._reso == ts._reso
894900
assert result == expected
895901

902+
@pytest.mark.xfail(reason="tz_localize not yet implemented for non-nano")
903+
def test_addsub_offset(self, ts_tz):
904+
# specifically non-Tick offset
905+
off = offsets.YearBegin(1)
906+
result = ts_tz + off
907+
908+
assert isinstance(result, Timestamp)
909+
assert result._reso == ts_tz._reso
910+
# If ts_tz is ever on the last day of the year, the year would be
911+
# incremented by one
912+
assert result.year == ts_tz.year
913+
assert result.day == 31
914+
assert result.month == 12
915+
assert tz_compare(result.tz, ts_tz.tz)
916+
917+
def test_sub_datetimelike_mismatched_reso(self, ts_tz):
918+
# case with non-lossy rounding
919+
ts = ts_tz
920+
921+
# choose a unit for `other` that doesn't match ts_tz's
922+
unit = {
923+
NpyDatetimeUnit.NPY_FR_us.value: "ms",
924+
NpyDatetimeUnit.NPY_FR_ms.value: "s",
925+
NpyDatetimeUnit.NPY_FR_s.value: "us",
926+
}[ts._reso]
927+
other = ts._as_unit(unit)
928+
assert other._reso != ts._reso
929+
930+
result = ts - other
931+
assert isinstance(result, Timedelta)
932+
assert result.value == 0
933+
assert result._reso == min(ts._reso, other._reso)
934+
935+
result = other - ts
936+
assert isinstance(result, Timedelta)
937+
assert result.value == 0
938+
assert result._reso == min(ts._reso, other._reso)
939+
940+
# TODO: clarify in message that add/sub is allowed only when lossless?
941+
msg = "Cannot losslessly convert units"
942+
if ts._reso < other._reso:
943+
# Case where rounding is lossy
944+
other2 = other + Timedelta._from_value_and_reso(1, other._reso)
945+
with pytest.raises(ValueError, match=msg):
946+
ts - other2
947+
with pytest.raises(ValueError, match=msg):
948+
other2 - ts
949+
else:
950+
ts2 = ts + Timedelta._from_value_and_reso(1, ts._reso)
951+
with pytest.raises(ValueError, match=msg):
952+
ts2 - other
953+
with pytest.raises(ValueError, match=msg):
954+
other - ts2
955+
896956

897957
class TestAsUnit:
898958
def test_as_unit(self):

0 commit comments

Comments
 (0)