Skip to content

Commit d6dcbb4

Browse files
committed
Added closed parameter to the DatetimeIndex constructor.
1 parent 38e4d46 commit d6dcbb4

File tree

3 files changed

+65
-9
lines changed

3 files changed

+65
-9
lines changed

doc/source/release.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ Improvements to existing features
174174
- :meth:`~pandas.io.json.json_normalize` is a new method to allow you to create a flat table
175175
from semi-structured JSON data. :ref:`See the docs<io.json_normalize>` (:issue:`1067`)
176176
- ``DataFrame.from_records()`` will now accept generators (:issue:`4910`)
177+
- DatetimeIndex (and date_range) can now be constructed in a left- or
178+
right-open fashion using the ``closed`` parameter (:issue:`4579`)
177179

178180
API Changes
179181
~~~~~~~~~~~

pandas/tseries/index.py

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ class DatetimeIndex(Int64Index):
115115
end : end time, datetime-like, optional
116116
If periods is none, generated index will extend to first conforming
117117
time on or just past end argument
118+
closed : string or None, default None
119+
Make the interval closed with respect to the given frequency to
120+
the 'left', 'right', or both sides (None)
118121
"""
119122
_join_precedence = 10
120123

@@ -143,7 +146,8 @@ class DatetimeIndex(Int64Index):
143146
def __new__(cls, data=None,
144147
freq=None, start=None, end=None, periods=None,
145148
copy=False, name=None, tz=None,
146-
verify_integrity=True, normalize=False, **kwds):
149+
verify_integrity=True, normalize=False,
150+
closed=None, **kwds):
147151

148152
dayfirst = kwds.pop('dayfirst', None)
149153
yearfirst = kwds.pop('yearfirst', None)
@@ -184,7 +188,7 @@ def __new__(cls, data=None,
184188

185189
if data is None:
186190
return cls._generate(start, end, periods, name, offset,
187-
tz=tz, normalize=normalize,
191+
tz=tz, normalize=normalize, closed=closed,
188192
infer_dst=infer_dst)
189193

190194
if not isinstance(data, np.ndarray):
@@ -289,7 +293,7 @@ def __new__(cls, data=None,
289293

290294
@classmethod
291295
def _generate(cls, start, end, periods, name, offset,
292-
tz=None, normalize=False, infer_dst=False):
296+
tz=None, normalize=False, infer_dst=False, closed=None):
293297
if com._count_not_none(start, end, periods) != 2:
294298
raise ValueError('Must specify two of start, end, or periods')
295299

@@ -301,6 +305,24 @@ def _generate(cls, start, end, periods, name, offset,
301305
if end is not None:
302306
end = Timestamp(end)
303307

308+
left_closed = False
309+
right_closed = False
310+
311+
if start is None and end is None:
312+
if closed is not None:
313+
raise ValueError("Closed has to be None if not both of start"
314+
"and end are defined")
315+
316+
if closed is None:
317+
left_closed = True
318+
right_closed = True
319+
elif closed == "left":
320+
left_closed = True
321+
elif closed == "right":
322+
right_closed = True
323+
else:
324+
raise ValueError("Closed has to be either 'left', 'right' or None")
325+
304326
try:
305327
inferred_tz = tools._infer_tzinfo(start, end)
306328
except:
@@ -387,6 +409,11 @@ def _generate(cls, start, end, periods, name, offset,
387409
index.offset = offset
388410
index.tz = tz
389411

412+
if not left_closed:
413+
index = index[1:]
414+
if not right_closed:
415+
index = index[:-1]
416+
390417
return index
391418

392419
def _box_values(self, values):
@@ -1715,7 +1742,7 @@ def _generate_regular_range(start, end, periods, offset):
17151742

17161743

17171744
def date_range(start=None, end=None, periods=None, freq='D', tz=None,
1718-
normalize=False, name=None):
1745+
normalize=False, name=None, closed=None):
17191746
"""
17201747
Return a fixed frequency datetime index, with day (calendar) as the default
17211748
frequency
@@ -1737,6 +1764,9 @@ def date_range(start=None, end=None, periods=None, freq='D', tz=None,
17371764
Normalize start/end dates to midnight before generating date range
17381765
name : str, default None
17391766
Name of the resulting index
1767+
closed : string or None, default None
1768+
Make the interval closed with respect to the given frequency to
1769+
the 'left', 'right', or both sides (None)
17401770
17411771
Notes
17421772
-----
@@ -1747,11 +1777,12 @@ def date_range(start=None, end=None, periods=None, freq='D', tz=None,
17471777
rng : DatetimeIndex
17481778
"""
17491779
return DatetimeIndex(start=start, end=end, periods=periods,
1750-
freq=freq, tz=tz, normalize=normalize, name=name)
1780+
freq=freq, tz=tz, normalize=normalize, name=name,
1781+
closed=closed)
17511782

17521783

17531784
def bdate_range(start=None, end=None, periods=None, freq='B', tz=None,
1754-
normalize=True, name=None):
1785+
normalize=True, name=None, closed=None):
17551786
"""
17561787
Return a fixed frequency datetime index, with business day as the default
17571788
frequency
@@ -1773,6 +1804,9 @@ def bdate_range(start=None, end=None, periods=None, freq='B', tz=None,
17731804
Normalize start/end dates to midnight before generating date range
17741805
name : str, default None
17751806
Name for the resulting index
1807+
closed : string or None, default None
1808+
Make the interval closed with respect to the given frequency to
1809+
the 'left', 'right', or both sides (None)
17761810
17771811
Notes
17781812
-----
@@ -1784,11 +1818,12 @@ def bdate_range(start=None, end=None, periods=None, freq='B', tz=None,
17841818
"""
17851819

17861820
return DatetimeIndex(start=start, end=end, periods=periods,
1787-
freq=freq, tz=tz, normalize=normalize, name=name)
1821+
freq=freq, tz=tz, normalize=normalize, name=name,
1822+
closed=closed)
17881823

17891824

17901825
def cdate_range(start=None, end=None, periods=None, freq='C', tz=None,
1791-
normalize=True, name=None, **kwargs):
1826+
normalize=True, name=None, closed=None, **kwargs):
17921827
"""
17931828
**EXPERIMENTAL** Return a fixed frequency datetime index, with
17941829
CustomBusinessDay as the default frequency
@@ -1820,6 +1855,9 @@ def cdate_range(start=None, end=None, periods=None, freq='C', tz=None,
18201855
holidays : list
18211856
list/array of dates to exclude from the set of valid business days,
18221857
passed to ``numpy.busdaycalendar``
1858+
closed : string or None, default None
1859+
Make the interval closed with respect to the given frequency to
1860+
the 'left', 'right', or both sides (None)
18231861
18241862
Notes
18251863
-----
@@ -1835,7 +1873,8 @@ def cdate_range(start=None, end=None, periods=None, freq='C', tz=None,
18351873
weekmask = kwargs.pop('weekmask', 'Mon Tue Wed Thu Fri')
18361874
freq = CDay(holidays=holidays, weekmask=weekmask)
18371875
return DatetimeIndex(start=start, end=end, periods=periods, freq=freq,
1838-
tz=tz, normalize=normalize, name=name, **kwargs)
1876+
tz=tz, normalize=normalize, name=name,
1877+
closed=closed, **kwargs)
18391878

18401879

18411880
def _to_m8(key, tz=None):

pandas/tseries/tests/test_daterange.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,21 @@ def test_month_range_union_tz(self):
394394

395395
early_dr.union(late_dr)
396396

397+
def test_range_closed(self):
398+
begin = datetime(2011, 1, 1)
399+
end = datetime(2014, 1, 1)
400+
401+
for freq in ["3D", "2M", "7W", "3H", "A"]:
402+
closed = date_range(begin, end, closed=None, freq=freq)
403+
left = date_range(begin, end, closed="left", freq=freq)
404+
right = date_range(begin, end, closed="right", freq=freq)
405+
406+
expected_left = closed[:-1]
407+
expected_right = closed[1:]
408+
409+
self.assert_(expected_left.equals(left))
410+
self.assert_(expected_right.equals(right))
411+
397412

398413
class TestCustomDateRange(unittest.TestCase):
399414

0 commit comments

Comments
 (0)