Skip to content

Commit 3cc5b28

Browse files
committed
ENH: Add length attribute to Interval and IntervalIndex
1 parent 7a0ee19 commit 3cc5b28

File tree

5 files changed

+66
-5
lines changed

5 files changed

+66
-5
lines changed

doc/source/whatsnew/v0.22.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ Other Enhancements
139139
- :func:`read_excel()` has gained the ``nrows`` parameter (:issue:`16645`)
140140
- :func:``DataFrame.to_json`` and ``Series.to_json`` now accept an ``index`` argument which allows the user to exclude the index from the JSON output (:issue:`17394`)
141141
- ``IntervalIndex.to_tuples()`` has gained the ``na_tuple`` parameter to control whether NA is returned as a tuple of NA, or NA itself (:issue:`18756`)
142+
- :class:`Interval` and :class:`IntervalIndex` have gained a ``length`` attribute (:issue:`18789`)
142143

143144
.. _whatsnew_0220.api_breaking:
144145

pandas/_libs/interval.pyx

+6-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,12 @@ cdef class IntervalMixin(object):
5454
return 0.5 * (self.left + self.right)
5555
except TypeError:
5656
# datetime safe version
57-
return self.left + 0.5 * (self.right - self.left)
57+
return self.left + 0.5 * self.length
58+
59+
@property
60+
def length(self):
61+
"""Return the length of the Interval"""
62+
return self.right - self.left
5863

5964

6065
cdef _interval_like(other):

pandas/core/indexes/interval.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,14 @@ def closed(self):
599599
"""
600600
return self._closed
601601

602+
@property
603+
def length(self):
604+
"""
605+
Return an Index with entries denoting the length of each Interval in
606+
the IntervalIndex
607+
"""
608+
return self.right - self.left
609+
602610
def __len__(self):
603611
return len(self.left)
604612

@@ -683,11 +691,10 @@ def mid(self):
683691
Return the midpoint of each Interval in the IntervalIndex as an Index
684692
"""
685693
try:
686-
return Index(0.5 * (self.left.values + self.right.values))
694+
return 0.5 * (self.left + self.right)
687695
except TypeError:
688696
# datetime safe version
689-
delta = self.right - self.left
690-
return self.left + 0.5 * delta
697+
return self.left + 0.5 * self.length
691698

692699
@cache_readonly
693700
def is_monotonic(self):

pandas/tests/indexes/test_interval.py

+17
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,23 @@ def test_properties(self, closed):
283283
tm.assert_numpy_array_equal(np.asarray(index), expected)
284284
tm.assert_numpy_array_equal(index.values, expected)
285285

286+
@pytest.mark.parametrize('breaks', [
287+
[1, 1, 2, 5, 15, 53, 217, 1014, 5335, 31240, 201608],
288+
[-np.inf, -100, -10, 0.5, 1, 1.5, 3.8, 101, 202, np.inf],
289+
pd.to_datetime(['20170101', '20170202', '20170303', '20170404']),
290+
pd.to_timedelta(['1ns', '2ms', '3s', '4M', '5H', '6D'])])
291+
def test_length(self, closed, breaks):
292+
index = IntervalIndex.from_breaks(breaks, closed=closed)
293+
result = index.length
294+
expected = Index(iv.length for iv in index)
295+
tm.assert_index_equal(result, expected)
296+
297+
# with NA
298+
index = index.insert(1, np.nan)
299+
result = index.length
300+
expected = Index(iv.length if notna(iv) else iv for iv in index)
301+
tm.assert_index_equal(result, expected)
302+
286303
def test_with_nans(self, closed):
287304
index = self.create_index(closed=closed)
288305
assert not index.hasnans

pandas/tests/scalar/test_interval.py

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import division
22

3-
from pandas import Interval, Timestamp
3+
import numpy as np
4+
from pandas import Interval, Timestamp, Timedelta
45
from pandas.core.common import _any_none
56

67
import pytest
@@ -66,6 +67,36 @@ def test_hash(self, interval):
6667
# should not raise
6768
hash(interval)
6869

70+
@pytest.mark.parametrize('left, right, expected', [
71+
(0, 5, 5),
72+
(-2, 5.5, 7.5),
73+
(10, 10, 0),
74+
(10, np.inf, np.inf),
75+
(-np.inf, -5, np.inf),
76+
(-np.inf, np.inf, np.inf),
77+
(Timedelta('0 days'), Timedelta('5 days'), Timedelta('5 days')),
78+
(Timedelta('10 days'), Timedelta('10 days'), Timedelta('0 days')),
79+
(Timedelta('1H10M'), Timedelta('5H5M'), Timedelta('3H55M')),
80+
(Timedelta('5S'), Timedelta('1H'), Timedelta('59M55S'))])
81+
def test_length(self, left, right, expected):
82+
# GH 18789
83+
iv = Interval(left, right)
84+
result = iv.length
85+
assert result == expected
86+
87+
@pytest.mark.parametrize('left, right, expected', [
88+
('2017-01-01', '2017-01-06', '5 days'),
89+
('2017-01-01', '2017-01-01 12:00:00', '12 hours'),
90+
('2017-01-01 12:00', '2017-01-01 12:00:00', '0 days'),
91+
('2017-01-01 12:01', '2017-01-05 17:31:00', '4 days 5 hours 30 min')])
92+
@pytest.mark.parametrize('tz', (None, 'UTC', 'CET', 'US/Eastern'))
93+
def test_length_timestamp(self, tz, left, right, expected):
94+
# GH 18789
95+
iv = Interval(Timestamp(left, tz=tz), Timestamp(right, tz=tz))
96+
result = iv.length
97+
expected = Timedelta(expected)
98+
assert result == expected
99+
69100
def test_math_add(self, interval):
70101
expected = Interval(1, 2)
71102
actual = interval + 1

0 commit comments

Comments
 (0)