Skip to content

Commit 549c402

Browse files
committed
fix datetimelike
1 parent 6be074e commit 549c402

File tree

2 files changed

+136
-48
lines changed

2 files changed

+136
-48
lines changed

pandas/core/indexes/interval.py

+36-26
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@
2828
intervals_to_interval_bounds)
2929

3030
from pandas.core.indexes.datetimes import date_range
31+
from pandas.core.indexes.timedeltas import timedelta_range
3132
from pandas.core.indexes.multi import MultiIndex
3233
from pandas.compat.numpy import function as nv
3334
from pandas.core import common as com
3435
from pandas.util._decorators import cache_readonly, Appender
3536
from pandas.core.config import get_option
36-
from pandas.tseries.offsets import DateOffset
3737
from pandas.tseries.frequencies import to_offset
3838

3939
import pandas.core.indexes.base as ibase
@@ -1107,48 +1107,52 @@ def interval_range(start=None, end=None, periods=None, freq=None,
11071107
raise ValueError('Of the three parameters: start, end, and periods, '
11081108
'exactly two must be specified')
11091109

1110-
# assume datetime-like unless we find numeric start or end
1111-
is_datetime_interval = True
1110+
iv_type = {'numeric': True, 'timestamp': True, 'timedelta': True}
11121111

1112+
start = com._maybe_box_datetimelike(start)
11131113
if is_number(start):
1114-
is_datetime_interval = False
1114+
iv_type.update({k: False for k in iv_type if k != 'numeric'})
1115+
elif isinstance(start, Timestamp):
1116+
iv_type.update({k: False for k in iv_type if k != 'timestamp'})
1117+
elif isinstance(start, Timedelta):
1118+
iv_type.update({k: False for k in iv_type if k != 'timedelta'})
11151119
elif start is not None:
1116-
try:
1117-
start = Timestamp(start)
1118-
except (TypeError, ValueError):
1119-
raise ValueError('start must be numeric or datetime-like')
1120+
msg = 'start must be numeric or datetime-like, got {start}'
1121+
raise ValueError(msg.format(start=start))
11201122

1123+
end = com._maybe_box_datetimelike(end)
11211124
if is_number(end):
1122-
is_datetime_interval = False
1125+
iv_type.update({k: False for k in iv_type if k != 'numeric'})
1126+
elif isinstance(end, Timestamp):
1127+
iv_type.update({k: False for k in iv_type if k != 'timestamp'})
1128+
elif isinstance(end, Timedelta):
1129+
iv_type.update({k: False for k in iv_type if k != 'timedelta'})
11231130
elif end is not None:
1124-
try:
1125-
end = Timestamp(end)
1126-
except (TypeError, ValueError):
1127-
raise ValueError('end must be numeric or datetime-like')
1131+
msg = 'end must be numeric or datetime-like, got {end}'
1132+
raise ValueError(msg.format(end=end))
11281133

11291134
if is_float(periods):
11301135
periods = int(periods)
11311136
elif not is_integer(periods) and periods is not None:
11321137
msg = 'periods must be a number, got {periods}'
11331138
raise TypeError(msg.format(periods=periods))
11341139

1135-
if is_datetime_interval:
1136-
freq = freq or 'D'
1137-
if not isinstance(freq, DateOffset):
1138-
try:
1139-
freq = to_offset(freq)
1140-
except ValueError:
1141-
raise ValueError('freq must be convertible to DateOffset when '
1142-
'start/end are datetime-like')
1140+
freq = freq or (1 if iv_type['numeric'] else 'D')
1141+
if is_number(freq):
1142+
iv_type.update({k: False for k in iv_type if k != 'numeric'})
11431143
else:
1144-
freq = freq or 1
1144+
try:
1145+
freq = to_offset(freq)
1146+
iv_type['numeric'] = False
1147+
except ValueError:
1148+
raise ValueError('freq must be numeric or convertible to '
1149+
'DateOffset, got {freq}'.format(freq=freq))
11451150

11461151
# verify type compatibility
1147-
is_numeric_interval = all(map(is_number, com._not_none(start, end, freq)))
1148-
if not is_datetime_interval and not is_numeric_interval:
1152+
if not any(iv_type.values()):
11491153
raise TypeError("start, end, freq need to be type compatible")
11501154

1151-
if is_numeric_interval:
1155+
if iv_type['numeric']:
11521156
if periods is None:
11531157
periods = int((end - start) // freq)
11541158

@@ -1160,10 +1164,16 @@ def interval_range(start=None, end=None, periods=None, freq=None,
11601164

11611165
# end + freq for inclusive endpoint
11621166
breaks = np.arange(start, end + freq, freq)
1163-
else:
1167+
elif iv_type['timestamp']:
11641168
# add one to account for interval endpoints (n breaks = n-1 intervals)
11651169
if periods is not None:
11661170
periods += 1
11671171
breaks = date_range(start=start, end=end, periods=periods, freq=freq)
1172+
else:
1173+
# add one to account for interval endpoints (n breaks = n-1 intervals)
1174+
if periods is not None:
1175+
periods += 1
1176+
breaks = timedelta_range(start=start, end=end, periods=periods,
1177+
freq=freq)
11681178

11691179
return IntervalIndex.from_breaks(breaks, name=name, closed=closed)

pandas/tests/indexes/test_interval.py

+100-22
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
import pytest
44
import numpy as np
5-
import datetime
5+
from datetime import timedelta
66
from pandas import (Interval, IntervalIndex, Index, isna,
77
interval_range, Timestamp, Timedelta,
8-
compat, date_range, DateOffset)
8+
compat, date_range, timedelta_range, DateOffset)
9+
from pandas.tseries.offsets import Day
910
from pandas._libs.interval import IntervalTree
1011
from pandas.tests.indexes.common import Base
1112
import pandas.util.testing as tm
@@ -763,8 +764,8 @@ def test_construction_from_numeric(self, closed):
763764
def test_construction_from_timestamp(self, closed):
764765
# combinations of start/end/periods without freq
765766
start, end = Timestamp('2017-01-01'), Timestamp('2017-01-06')
766-
expected = IntervalIndex.from_breaks(date_range(start=start, end=end),
767-
name='foo', closed=closed)
767+
breaks = date_range(start=start, end=end)
768+
expected = IntervalIndex.from_breaks(breaks, name='foo', closed=closed)
768769

769770
result = interval_range(start=start, end=end, name='foo',
770771
closed=closed)
@@ -826,37 +827,90 @@ def test_construction_from_timestamp(self, closed):
826827
closed=closed)
827828
tm.assert_index_equal(result, expected)
828829

830+
@pytest.mark.parametrize('closed', ['left', 'right', 'neither', 'both'])
831+
def test_construction_from_timedelta(self, closed):
832+
# combinations of start/end/periods without freq
833+
start, end = Timedelta('1 day'), Timedelta('6 days')
834+
breaks = timedelta_range(start=start, end=end)
835+
expected = IntervalIndex.from_breaks(breaks, name='foo', closed=closed)
836+
837+
result = interval_range(start=start, end=end, name='foo',
838+
closed=closed)
839+
tm.assert_index_equal(result, expected)
840+
841+
result = interval_range(start=start, periods=5, name='foo',
842+
closed=closed)
843+
tm.assert_index_equal(result, expected)
844+
845+
result = interval_range(end=end, periods=5, name='foo',
846+
closed=closed)
847+
tm.assert_index_equal(result, expected)
848+
849+
# combinations of start/end/periods with fixed freq
850+
freq = '2D'
851+
start, end = Timedelta('1 day'), Timedelta('7 days')
852+
breaks = timedelta_range(start=start, end=end, freq=freq)
853+
expected = IntervalIndex.from_breaks(breaks, name='foo', closed=closed)
854+
855+
result = interval_range(start=start, end=end, freq=freq, name='foo',
856+
closed=closed)
857+
tm.assert_index_equal(result, expected)
858+
859+
result = interval_range(start=start, periods=3, freq=freq, name='foo',
860+
closed=closed)
861+
tm.assert_index_equal(result, expected)
862+
863+
result = interval_range(end=end, periods=3, freq=freq, name='foo',
864+
closed=closed)
865+
tm.assert_index_equal(result, expected)
866+
867+
# output truncates early if freq causes end to be skipped.
868+
end = Timedelta('7 days 1 hour')
869+
result = interval_range(start=start, end=end, freq=freq, name='foo',
870+
closed=closed)
871+
tm.assert_index_equal(result, expected)
872+
829873
def test_constructor_coverage(self):
830874
# float value for periods
831875
expected = pd.interval_range(start=0, periods=10)
832876
result = pd.interval_range(start=0, periods=10.5)
833877
tm.assert_index_equal(result, expected)
834878

835-
# equivalent datetime-like start/end
879+
# equivalent timestamp-like start/end
836880
start, end = Timestamp('2017-01-01'), Timestamp('2017-01-15')
837881
expected = pd.interval_range(start=start, end=end)
838882

839883
result = pd.interval_range(start=start.to_pydatetime(),
840884
end=end.to_pydatetime())
841885
tm.assert_index_equal(result, expected)
842886

843-
result = pd.interval_range(start=start.date(), end=end.date())
887+
result = pd.interval_range(start=start.tz_localize('UTC'),
888+
end=end.tz_localize('UTC'))
844889
tm.assert_index_equal(result, expected)
845890

846-
result = pd.interval_range(start=str(start), end=str(end))
891+
result = pd.interval_range(start=start.asm8, end=end.asm8)
847892
tm.assert_index_equal(result, expected)
848893

849-
result = pd.interval_range(start=start.strftime('%Y-%m-%d'),
850-
end=end.strftime('%Y-%m-%d'))
894+
# equivalent freq with timestamp
895+
equiv_freq = ['D', Day(), Timedelta(days=1), timedelta(days=1),
896+
DateOffset(days=1)]
897+
for freq in equiv_freq:
898+
result = pd.interval_range(start=start, end=end, freq=freq)
899+
tm.assert_index_equal(result, expected)
900+
901+
# equivalent timedelta-like start/end
902+
start, end = Timedelta(days=1), Timedelta(days=10)
903+
expected = pd.interval_range(start=start, end=end)
904+
905+
result = pd.interval_range(start=start.to_pytimedelta(),
906+
end=end.to_pytimedelta())
851907
tm.assert_index_equal(result, expected)
852908

853-
result = pd.interval_range(start=start.strftime('%m/%d/%y'),
854-
end=end.strftime('%m/%d/%y'))
909+
result = pd.interval_range(start=start.asm8, end=end.asm8)
855910
tm.assert_index_equal(result, expected)
856911

857-
# equivalent freq
858-
equiv_freq = ['D', DateOffset(days=1), Timedelta(days=1),
859-
datetime.timedelta(days=1)]
912+
# equivalent freq with timedelta
913+
equiv_freq = ['D', Day(), Timedelta(days=1), timedelta(days=1)]
860914
for freq in equiv_freq:
861915
result = pd.interval_range(start=start, end=end, freq=freq)
862916
tm.assert_index_equal(result, expected)
@@ -885,37 +939,61 @@ def test_errors(self):
885939
# mixed units
886940
msg = 'start, end, freq need to be type compatible'
887941
with tm.assert_raises_regex(TypeError, msg):
888-
interval_range(start=Timestamp('20130101'), end=10, freq=2)
942+
interval_range(start=0, end=Timestamp('20130101'), freq=2)
943+
944+
with tm.assert_raises_regex(TypeError, msg):
945+
interval_range(start=0, end=Timedelta('1 day'), freq=2)
889946

890947
with tm.assert_raises_regex(TypeError, msg):
891-
interval_range(start=0, end=Timestamp('20130101'), freq=2)
948+
interval_range(start=0, end=10, freq='D')
949+
950+
with tm.assert_raises_regex(TypeError, msg):
951+
interval_range(start=Timestamp('20130101'), end=10, freq='D')
952+
953+
with tm.assert_raises_regex(TypeError, msg):
954+
interval_range(start=Timestamp('20130101'),
955+
end=Timedelta('1 day'), freq='D')
892956

893957
with tm.assert_raises_regex(TypeError, msg):
894-
interval_range(start=0, end=10, freq=Timedelta('1day'))
958+
interval_range(start=Timestamp('20130101'),
959+
end=Timestamp('20130110'), freq=2)
960+
961+
with tm.assert_raises_regex(TypeError, msg):
962+
interval_range(start=Timedelta('1 day'), end=10, freq='D')
963+
964+
with tm.assert_raises_regex(TypeError, msg):
965+
interval_range(start=Timedelta('1 day'),
966+
end=Timestamp('20130110'), freq='D')
967+
968+
with tm.assert_raises_regex(TypeError, msg):
969+
interval_range(start=Timedelta('1 day'),
970+
end=Timedelta('10 days'), freq=2)
895971

896972
# invalid periods
897973
msg = 'periods must be a number, got foo'
898974
with tm.assert_raises_regex(TypeError, msg):
899975
interval_range(start=0, periods='foo')
900976

901977
# invalid start
902-
msg = 'start must be numeric or datetime-like'
978+
msg = 'start must be numeric or datetime-like, got foo'
903979
with tm.assert_raises_regex(ValueError, msg):
904980
interval_range(start='foo', periods=10)
905981

906982
# invalid end
907-
msg = 'end must be numeric or datetime-like'
983+
msg = 'end must be numeric or datetime-like, got \(0, 1\]'
908984
with tm.assert_raises_regex(ValueError, msg):
909985
interval_range(end=Interval(0, 1), periods=10)
910986

911987
# invalid freq for datetime-like
912-
msg = ('freq must be convertible to DateOffset when start/end are '
913-
'datetime-like')
988+
msg = 'freq must be numeric or convertible to DateOffset, got foo'
989+
with tm.assert_raises_regex(ValueError, msg):
990+
interval_range(start=0, end=10, freq='foo')
991+
914992
with tm.assert_raises_regex(ValueError, msg):
915993
interval_range(start=Timestamp('20130101'), periods=10, freq='foo')
916994

917995
with tm.assert_raises_regex(ValueError, msg):
918-
interval_range(end=Timestamp('20130101'), periods=10, freq='foo')
996+
interval_range(end=Timedelta('1 day'), periods=10, freq='foo')
919997

920998

921999
class TestIntervalTree(object):

0 commit comments

Comments
 (0)