Skip to content

ENH: Arithmetic with Timestamp-based intervals #36001

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
Closed
20 changes: 20 additions & 0 deletions doc/source/whatsnew/v1.2.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,26 @@ For example:
buffer = io.BytesIO()
data.to_csv(buffer, mode="w+b", encoding="utf-8", compression="gzip")

Arithmetic with Timestamp and Timedelta-based Intervals
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Arithmetic can now be performed on :class:`Interval` s having their left and right
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason for all the spaces between backtick and "s"?

ends as :class:`Timestamp` s or :class:`Timedelta` s, like what would be possible
if the ends were numeric (:issue:`35908`).

Arithmetic can be performed by directly using arithmetic operators (`-` or `+`),
so something like this will work:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add something like this as an example in Interval


.. ipython:: python

interval = pd.Interval(pd.Timestamp("1900-01-01"), pd.Timestamp("1900-01-02"))
interval - pd.Timestamp("1900-01-01")

This works when endpoints are :class:`Timestamp` s or :class:`Timedelta` s.

However, it should be noted that adding :class:`Timestamp` s , and subtracting :class:`Timestamp`
from a :class:`Timedelta` is illegal.

.. _whatsnew_120.enhancements.other:

Other enhancements
Expand Down
4 changes: 4 additions & 0 deletions pandas/_libs/interval.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,8 @@ cdef class Interval(IntervalMixin):
isinstance(y, numbers.Number)
or PyDelta_Check(y)
or is_timedelta64_object(y)
or isinstance(y, _Timestamp)
or isinstance(y, _Timedelta)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is covered by L397

):
return Interval(self.left + y, self.right + y, closed=self.closed)
elif (
Expand All @@ -413,6 +415,8 @@ cdef class Interval(IntervalMixin):
isinstance(y, numbers.Number)
or PyDelta_Check(y)
or is_timedelta64_object(y)
or isinstance(y, _Timestamp)
or isinstance(y, _Timedelta)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the PyDelta_Check call makes the _Timedelta check extraneous. The remaining isinstance checks can be combined.

):
return Interval(self.left - y, self.right - y, closed=self.closed)
return NotImplemented
Expand Down
178 changes: 178 additions & 0 deletions pandas/tests/scalar/interval/test_interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,184 @@ def test_math_sub(self, closed):
with pytest.raises(TypeError, match=msg):
interval - "foo"

def test_math_sub_interval_timestamp_timestamp(self, closed):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd name this test_subtraction_interval_mixed_timestamp_timedelta

# Tests for interval of timestamp - timestamp
interval = Interval(
Timestamp("1900-01-01"), Timestamp("1900-01-02"), closed=closed
)
expected = Interval(
Timedelta("0 days 00:00:00"), Timedelta("1 days 00:00:00"), closed=closed
)

result = interval - Timestamp("1900-01-01")
assert result == expected

expected = Interval(
interval.left - Timestamp("1900-01-01"),
interval.right - Timestamp("1900-01-01"),
closed=closed,
)
assert result == expected

result = interval
result -= Timestamp("1900-01-01")

expected = Interval(
Timedelta("0 days 00:00:00"), Timedelta("1 days 00:00:00"), closed=closed
)
assert result == expected

expected = Interval(
interval.left - Timestamp("1900-01-01"),
interval.right - Timestamp("1900-01-01"),
closed=closed,
)
assert result == expected

def test_math_sub_interval_timestamp_timedelta(self, closed):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_subtraction_interval_mixed_timestamp_timedelta

# Tests for interval of timestamps - timedelta
interval = Interval(
Timestamp("1900-01-01"), Timestamp("1900-01-02"), closed=closed
)
expected = Interval(
Timestamp("1899-12-31"), Timestamp("1900-01-01"), closed=closed
)

result = interval - Timedelta("1 days 00:00:00")
assert result == expected

expected = Interval(
interval.left - Timedelta("1 days 00:00:00"),
interval.right - Timedelta("1 days 00:00:00"),
closed=closed,
)
assert result == expected

result = interval
result -= Timedelta("1 days 00:00:00")

expected = Interval(
Timestamp("1899-12-31"), Timestamp("1900-01-01"), closed=closed
)
assert result == expected

expected = Interval(
interval.left - Timedelta("1 days 00:00:00"),
interval.right - Timedelta("1 days 00:00:00"),
closed=closed,
)
assert result == expected

def test_math_add_interval_timestamp_timedelta(self, closed):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_addition_interval_mixed_timestamp_timedelta

interval = Interval(
Timestamp("1900-01-01"), Timestamp("1900-01-02"), closed=closed
)
expected = Interval(
Timestamp("1900-01-02"), Timestamp("1900-01-03"), closed=closed
)

result = interval + Timedelta("1 days 00:00:00")
assert result == expected

result = interval
result += Timedelta("1 days 00:00:00")
assert result == expected

expected = Interval(
interval.left + Timedelta("1 days 00:00:00"),
interval.right + Timedelta("1 days 00:00:00"),
closed=closed,
)

result = interval + Timedelta("1 days 00:00:00")
assert result == expected

result = interval
result += Timedelta("1 days 00:00:00")
assert result == expected

def test_math_add_interval_timedelta_timedelta(self, closed):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_addition_interval_timedelta

interval = Interval(
Timedelta("1 days 00:00:00"), Timedelta("2 days 00:00:00"), closed=closed
)
expected = Interval(
Timedelta("4 days 01:00:00"), Timedelta("5 days 01:00:00"), closed=closed
)

result = interval + Timedelta("3 days 01:00:00")
assert result == expected

result = interval
result += Timedelta("3 days 01:00:00")
assert result == expected

expected = Interval(
interval.left + Timedelta("3 days 01:00:00"),
interval.right + Timedelta("3 days 01:00:00"),
closed=closed,
)

result = interval + Timedelta("3 days 01:00:00")
assert result == expected

result = interval
result += Timedelta("3 days 01:00:00")
assert result == expected

def test_sub_interval_imedelta_timedelta(self, closed):
interval = Interval(
Timedelta("1 days 00:00:00"), Timedelta("2 days 00:00:00"), closed=closed
)
expected = Interval(
Timedelta("-3 days +23:00:00"),
Timedelta("-2 days +23:00:00"),
closed=closed,
)

result = interval - Timedelta("3 days 01:00:00")
assert result == expected

result = interval
result -= Timedelta("3 days 01:00:00")
assert result == expected

expected = Interval(
interval.left - Timedelta("3 days 01:00:00"),
interval.right - Timedelta("3 days 01:00:00"),
closed=closed,
)

result = interval - Timedelta("3 days 01:00:00")
assert result == expected

result = interval
result -= Timedelta("3 days 01:00:00")
assert result == expected

def test_math_add_interval_timestamp_timestamp(self, closed):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_interval_addition_timestamp
(left and right bounds have to be the same type)

interval = Interval(
Timestamp("1900-01-01"), Timestamp("1900-01-02"), closed=closed
)

msg = r"unsupported operand type\(s\) for \+"
with pytest.raises(TypeError, match=msg):
interval = interval + Timestamp("2002-01-08")

with pytest.raises(TypeError, match=msg):
interval += Timestamp("2002-01-08")

def test_math_sub_interval_timedelta_timestamp(self, closed):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_math_interval_mixed_timedelta_timestamp

interval = Interval(
Timedelta("1 days 00:00:00"), Timedelta("3 days 00:00:00"), closed=closed
)

msg = r"unsupported operand type\(s\) for \-"
with pytest.raises(TypeError, match=msg):
interval = interval - Timestamp("1900-01-01")

with pytest.raises(TypeError, match=msg):
interval -= Timestamp("1900-01-01")

def test_math_mult(self, closed):
interval = Interval(0, 1, closed=closed)
expected = Interval(0, 2, closed=closed)
Expand Down