Skip to content

Commit 6c2485d

Browse files
committed
Merge branch 'date_range' of https://github.com/filmor/pandas into filmor-date_range
Conflicts: doc/source/release.rst pandas/tseries/offsets.py
2 parents 4db583d + d6dcbb4 commit 6c2485d

File tree

4 files changed

+76
-19
lines changed

4 files changed

+76
-19
lines changed

doc/source/release.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ Improvements to existing features
181181
- ``DataFrame.interpolate()`` and ``Series.interpolate()`` have been expanded to include
182182
interpolation methods from scipy. (:issue:`4434`, :issue:`1892`)
183183
- ``Series`` now supports a ``to_frame`` method to convert it to a single-column DataFrame (:issue:`5164`)
184+
- DatetimeIndex (and date_range) can now be constructed in a left- or
185+
right-open fashion using the ``closed`` parameter (:issue:`4579`)
184186

185187
API Changes
186188
~~~~~~~~~~~

pandas/tseries/index.py

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

@@ -144,7 +147,8 @@ class DatetimeIndex(Int64Index):
144147
def __new__(cls, data=None,
145148
freq=None, start=None, end=None, periods=None,
146149
copy=False, name=None, tz=None,
147-
verify_integrity=True, normalize=False, **kwds):
150+
verify_integrity=True, normalize=False,
151+
closed=None, **kwds):
148152

149153
dayfirst = kwds.pop('dayfirst', None)
150154
yearfirst = kwds.pop('yearfirst', None)
@@ -185,7 +189,7 @@ def __new__(cls, data=None,
185189

186190
if data is None:
187191
return cls._generate(start, end, periods, name, offset,
188-
tz=tz, normalize=normalize,
192+
tz=tz, normalize=normalize, closed=closed,
189193
infer_dst=infer_dst)
190194

191195
if not isinstance(data, np.ndarray):
@@ -290,7 +294,7 @@ def __new__(cls, data=None,
290294

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

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

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

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

393420
def _box_values(self, values):
@@ -1722,7 +1749,7 @@ def _generate_regular_range(start, end, periods, offset):
17221749

17231750

17241751
def date_range(start=None, end=None, periods=None, freq='D', tz=None,
1725-
normalize=False, name=None):
1752+
normalize=False, name=None, closed=None):
17261753
"""
17271754
Return a fixed frequency datetime index, with day (calendar) as the default
17281755
frequency
@@ -1744,6 +1771,9 @@ def date_range(start=None, end=None, periods=None, freq='D', tz=None,
17441771
Normalize start/end dates to midnight before generating date range
17451772
name : str, default None
17461773
Name of the resulting index
1774+
closed : string or None, default None
1775+
Make the interval closed with respect to the given frequency to
1776+
the 'left', 'right', or both sides (None)
17471777
17481778
Notes
17491779
-----
@@ -1754,11 +1784,12 @@ def date_range(start=None, end=None, periods=None, freq='D', tz=None,
17541784
rng : DatetimeIndex
17551785
"""
17561786
return DatetimeIndex(start=start, end=end, periods=periods,
1757-
freq=freq, tz=tz, normalize=normalize, name=name)
1787+
freq=freq, tz=tz, normalize=normalize, name=name,
1788+
closed=closed)
17581789

17591790

17601791
def bdate_range(start=None, end=None, periods=None, freq='B', tz=None,
1761-
normalize=True, name=None):
1792+
normalize=True, name=None, closed=None):
17621793
"""
17631794
Return a fixed frequency datetime index, with business day as the default
17641795
frequency
@@ -1780,6 +1811,9 @@ def bdate_range(start=None, end=None, periods=None, freq='B', tz=None,
17801811
Normalize start/end dates to midnight before generating date range
17811812
name : str, default None
17821813
Name for the resulting index
1814+
closed : string or None, default None
1815+
Make the interval closed with respect to the given frequency to
1816+
the 'left', 'right', or both sides (None)
17831817
17841818
Notes
17851819
-----
@@ -1791,11 +1825,12 @@ def bdate_range(start=None, end=None, periods=None, freq='B', tz=None,
17911825
"""
17921826

17931827
return DatetimeIndex(start=start, end=end, periods=periods,
1794-
freq=freq, tz=tz, normalize=normalize, name=name)
1828+
freq=freq, tz=tz, normalize=normalize, name=name,
1829+
closed=closed)
17951830

17961831

17971832
def cdate_range(start=None, end=None, periods=None, freq='C', tz=None,
1798-
normalize=True, name=None, **kwargs):
1833+
normalize=True, name=None, closed=None, **kwargs):
17991834
"""
18001835
**EXPERIMENTAL** Return a fixed frequency datetime index, with
18011836
CustomBusinessDay as the default frequency
@@ -1827,6 +1862,9 @@ def cdate_range(start=None, end=None, periods=None, freq='C', tz=None,
18271862
holidays : list
18281863
list/array of dates to exclude from the set of valid business days,
18291864
passed to ``numpy.busdaycalendar``
1865+
closed : string or None, default None
1866+
Make the interval closed with respect to the given frequency to
1867+
the 'left', 'right', or both sides (None)
18301868
18311869
Notes
18321870
-----
@@ -1842,7 +1880,8 @@ def cdate_range(start=None, end=None, periods=None, freq='C', tz=None,
18421880
weekmask = kwargs.pop('weekmask', 'Mon Tue Wed Thu Fri')
18431881
freq = CDay(holidays=holidays, weekmask=weekmask)
18441882
return DatetimeIndex(start=start, end=end, periods=periods, freq=freq,
1845-
tz=tz, normalize=normalize, name=name, **kwargs)
1883+
tz=tz, normalize=normalize, name=name,
1884+
closed=closed, **kwargs)
18461885

18471886

18481887
def _to_m8(key, tz=None):

pandas/tseries/offsets.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# import after tools, dateutil check
99
from dateutil.relativedelta import relativedelta
1010
import pandas.tslib as tslib
11+
from pandas.tslib import Timestamp
1112
from pandas import _np_version_under1p7
1213

1314
__all__ = ['Day', 'BusinessDay', 'BDay', 'CustomBusinessDay', 'CDay',
@@ -92,9 +93,9 @@ def apply(self, other):
9293
else:
9394
for i in range(-self.n):
9495
other = other - self._offset
95-
return other
96+
return Timestamp(other)
9697
else:
97-
return other + timedelta(self.n)
98+
return Timestamp(other + timedelta(self.n))
9899

99100
def isAnchored(self):
100101
return (self.n == 1)
@@ -392,7 +393,7 @@ def apply(self, other):
392393
if self.offset:
393394
result = result + self.offset
394395

395-
return result
396+
return Timestamp(result)
396397

397398
elif isinstance(other, (timedelta, Tick)):
398399
return BDay(self.n, offset=self.offset + other,
@@ -542,7 +543,7 @@ def apply(self, other):
542543
if n <= 0:
543544
n = n + 1
544545
other = other + relativedelta(months=n, day=31)
545-
return other
546+
return Timestamp(other)
546547

547548
@classmethod
548549
def onOffset(cls, dt):
@@ -562,7 +563,7 @@ def apply(self, other):
562563
n += 1
563564

564565
other = other + relativedelta(months=n, day=1)
565-
return other
566+
return Timestamp(other)
566567

567568
@classmethod
568569
def onOffset(cls, dt):
@@ -678,7 +679,7 @@ def apply(self, other):
678679
other = other + timedelta((self.weekday - otherDay) % 7)
679680
for i in range(-k):
680681
other = other - self._inc
681-
return other
682+
return Timestamp(other)
682683

683684
def onOffset(self, dt):
684685
return dt.weekday() == self.weekday
@@ -964,7 +965,7 @@ def apply(self, other):
964965

965966
other = other + relativedelta(months=monthsToGo + 3 * n, day=31)
966967

967-
return other
968+
return Timestamp(other)
968969

969970
def onOffset(self, dt):
970971
modMonth = (dt.month - self.startingMonth) % 3
@@ -996,7 +997,7 @@ def apply(self, other):
996997
n = n + 1
997998

998999
other = other + relativedelta(months=3 * n - monthsSince, day=1)
999-
return other
1000+
return Timestamp(other)
10001001

10011002

10021003
class YearOffset(DateOffset):
@@ -1138,7 +1139,7 @@ def _rollf(date):
11381139
# n == 0, roll forward
11391140
result = _rollf(result)
11401141

1141-
return result
1142+
return Timestamp(result)
11421143

11431144
def onOffset(self, dt):
11441145
wkday, days_in_month = tslib.monthrange(dt.year, self.month)
@@ -1185,7 +1186,7 @@ def _rollf(date):
11851186
# n == 0, roll forward
11861187
result = _rollf(result)
11871188

1188-
return result
1189+
return Timestamp(result)
11891190

11901191
def onOffset(self, dt):
11911192
return dt.month == self.month and dt.day == 1

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)