Skip to content
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

Interval quarter #1259

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/release.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ New Features
* Support for Interval types and expressions with support for execution on the
Impala and Clickhouse backends (:issue:`1243`)
* Isnan, isinf operations for float and double values (:issue:`1261`)
* Support for an interval with a quarter period (:issue:`1259`)

Bug Fixes
~~~~~~~~~
Expand Down
29 changes: 25 additions & 4 deletions ibis/expr/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,8 @@ def time(value):
return ir.TimeScalar(ir.literal(value).op())


def interval(value=None, unit='s', years=None, months=None, weeks=None,
days=None, hours=None, minutes=None, seconds=None,
def interval(value=None, unit='s', years=None, quarters=None, months=None,
weeks=None, days=None, hours=None, minutes=None, seconds=None,
milliseconds=None, microseconds=None, nanoseconds=None):
"""
Returns an interval literal
Expand All @@ -227,6 +227,7 @@ def interval(value=None, unit='s', years=None, months=None, weeks=None,
----------
value : int or datetime.timedelta, default None
years : int, default None
quarters : int, default None
months : int, default None
days : int, default None
weeks : int, default None
Expand All @@ -250,6 +251,7 @@ def interval(value=None, unit='s', years=None, months=None, weeks=None,
else:
kwds = [
('Y', years),
('Q', quarters),
('M', months),
('w', weeks),
('d', days),
Expand Down Expand Up @@ -312,6 +314,10 @@ def month(value=1):
return interval(months=value)


def quarter(value=1):
return interval(quarters=value)


def year(value=1):
return interval(years=value)

Expand Down Expand Up @@ -2094,10 +2100,22 @@ def _timestamp_date(arg):


def _convert_unit(value, unit, to):
factors = (7, 24, 60, 60, 1000, 1000, 1000)
units = ('w', 'd', 'h', 'm', 's', 'ms', 'us', 'ns')
factors = (7, 24, 60, 60, 1000, 1000, 1000)

monthly_units = ('Y', 'Q', 'M')
monthly_factors = (4, 3)

try:
i, j = units.index(unit), units.index(to)
except ValueError:
try:
i, j = monthly_units.index(unit), monthly_units.index(to)
factors = monthly_factors
except ValueError:
raise ValueError('Cannot convert to or from '
'non-fixed-length interval')

i, j = units.index(unit), units.index(to)
factor = functools.reduce(operator.mul, factors[i:j], 1)

if i < j:
Expand Down Expand Up @@ -2127,6 +2145,9 @@ def _interval_property(target_unit):

_interval_value_methods = dict(
to_unit=_to_unit,
years=_interval_property('Y'),
quarters=_interval_property('Q'),
months=_interval_property('M'),
weeks=_interval_property('w'),
days=_interval_property('d'),
hours=_interval_property('h'),
Expand Down
1 change: 1 addition & 0 deletions ibis/expr/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,7 @@ class Interval(DataType):

_units = dict(
Y='year',
Q='quarter',
M='month',
w='week',
d='day',
Expand Down
6 changes: 3 additions & 3 deletions ibis/expr/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2464,7 +2464,7 @@ class DateSubtract(TemporalSubtract):
rules.date,
rules.one_of([
rules.date,
rules.interval(units=['Y', 'M', 'w', 'd'])
rules.interval(units=['Y', 'Q', 'M', 'w', 'd'])
])
]
output_unit = 'd'
Expand Down Expand Up @@ -2499,7 +2499,7 @@ class TimestampSubtract(TemporalSubtract):

class DateAdd(Add):

input_type = [rules.date, rules.interval(units=['Y', 'M', 'w', 'd'])]
input_type = [rules.date, rules.interval(units=['Y', 'Q', 'M', 'w', 'd'])]
output_type = rules.shape_like_arg(0, 'date')


Expand Down Expand Up @@ -2547,7 +2547,7 @@ class IntervalFromInteger(ValueOp):
input_type = [
rules.integer,
rules.string_options([
'Y', 'M', 'w', 'd',
'Y', 'Q', 'M', 'w', 'd',
'h', 'm', 's', 'ms', 'us', 'ns'
], name='unit')
]
Expand Down
2 changes: 1 addition & 1 deletion ibis/expr/tests/test_datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ def test_timestamp_with_timezone_parser_invalid_timezone():


@pytest.mark.parametrize('unit', [
'Y', 'M', 'w', 'd', # date units
'Y', 'Q', 'M', 'w', 'd', # date units
'h', 'm', 's', 'ms', 'us', 'ns' # time units
])
def test_interval(unit):
Expand Down
83 changes: 68 additions & 15 deletions ibis/expr/tests/test_temporal.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@


@pytest.mark.parametrize(('interval', 'unit', 'expected'), [
(api.month(3), 'Q', api.quarter(1)),
(api.month(12), 'Y', api.year(1)),
(api.quarter(8), 'Y', api.year(2)),
(api.day(14), 'w', api.week(2)),
(api.minute(240), 'h', api.hour(4)),
(api.second(360), 'm', api.minute(6)),
Expand All @@ -25,19 +28,22 @@ def test_upconvert(interval, unit, expected):
assert result.type().unit == expected.type().unit


@pytest.mark.parametrize(('delta', 'target'), [
(api.day(), 'w'),
(api.hour(), 'd'),
(api.minute(), 'h'),
(api.second(), 'm'),
(api.second(), 'd'),
(api.millisecond(), 's'),
(api.microsecond(), 's'),
(api.nanosecond(), 's'),
@pytest.mark.parametrize('target', [
'Y', 'Q', 'M'
])
@pytest.mark.parametrize('delta', [
api.week(),
api.day(),
api.hour(),
api.minute(),
api.second(),
api.millisecond(),
api.microsecond(),
api.nanosecond()
])
def test_cannot_upconvert(delta, target):
assert isinstance(delta, ir.IntervalScalar)
assert delta.to_unit(target).type().unit == target
with pytest.raises(ValueError):
delta.to_unit(target)


@pytest.mark.parametrize('expr', [
Expand Down Expand Up @@ -114,6 +120,7 @@ def test_combine_with_different_kinds(a, b, unit):


@pytest.mark.parametrize(('case', 'expected'), [
(api.interval(quarters=2), api.quarter(2)),
(api.interval(weeks=2), api.week(2)),
(api.interval(days=3), api.day(3)),
(api.interval(hours=4), api.hour(4)),
Expand Down Expand Up @@ -184,6 +191,7 @@ def test_offset_months():
api.interval(3600),
api.interval(datetime.timedelta(days=3)),
api.interval(years=1),
api.interval(quarters=3),
api.interval(months=2),
api.interval(weeks=3),
api.interval(days=-1),
Expand Down Expand Up @@ -292,11 +300,35 @@ def test_interval_properties(prop, expected_unit):
assert getattr(i, prop).type().unit == expected_unit


@pytest.mark.parametrize('interval', [
api.interval(years=1),
api.interval(quarters=4),
api.interval(months=12)
])
@pytest.mark.parametrize(('prop', 'expected_unit'), [
('months', 'M'),
('quarters', 'Q'),
('years', 'Y')
])
def test_interval_monthly_properties(interval, prop, expected_unit):
assert getattr(interval, prop).type().unit == expected_unit


@pytest.mark.parametrize(('interval', 'prop'), [
(api.interval(hours=48), 'months'),
(api.interval(years=2), 'seconds'),
(api.interval(quarters=1), 'weeks')
])
def test_unsupported_properties(interval, prop):
with pytest.raises(ValueError):
getattr(interval, prop)


@pytest.mark.parametrize('column', [
'a', 'b', 'c', 'd' # integer columns
])
@pytest.mark.parametrize('unit', [
'Y', 'M', 'd', 'w',
'Y', 'Q', 'M', 'd', 'w',
'h', 'm', 's', 'ms', 'us', 'ns'
])
def test_integer_to_interval(column, unit, table):
Expand All @@ -308,7 +340,7 @@ def test_integer_to_interval(column, unit, table):


@pytest.mark.parametrize('unit', [
'Y', 'M', 'd', 'w',
'Y', 'Q', 'M', 'd', 'w',
'h', 'm', 's', 'ms', 'us', 'ns'
])
@pytest.mark.parametrize('operands', [
Expand All @@ -324,7 +356,7 @@ def test_integer_to_interval(column, unit, table):
operator.gt,
operator.le,
operator.lt
])
], ids=lambda op: op.__name__)
def test_interval_comparisons(unit, operands, operator, table):
a, b = operands(table, unit)
expr = operator(a, b)
Expand All @@ -340,21 +372,42 @@ def test_interval_comparisons(unit, operands, operator, table):
lambda t: (api.date('2016-01-01'), t.j),
lambda t: (t.j, t.i.date()),
lambda t: (t.i.date(), t.j)
], ids=[
'literal-literal',
'column-literal',
'literal-column',
'column-casted',
'casted-column'
])
@pytest.mark.parametrize('interval', [
lambda t: api.interval(years=4),
lambda t: api.interval(quarters=4),
lambda t: api.interval(months=3),
lambda t: api.interval(weeks=2),
lambda t: api.interval(days=1),
lambda t: t.c.to_interval(unit='Y'),
lambda t: t.c.to_interval(unit='M'),
lambda t: t.c.to_interval(unit='w'),
lambda t: t.c.to_interval(unit='d'),
], ids=[
'years',
'quarters',
'months',
'weeks',
'days',
'to-years',
'to-months',
'to-weeks',
'to-days'
])
@pytest.mark.parametrize('arithmetic', [
lambda a, i: a - i,
lambda a, i: a + i,
lambda a, i: i + a
], ids=[
'subtract',
'radd',
'add'
])
@pytest.mark.parametrize('operator', [
operator.eq,
Expand All @@ -363,7 +416,7 @@ def test_interval_comparisons(unit, operands, operator, table):
operator.gt,
operator.le,
operator.lt
])
], ids=lambda op: op.__name__)
def test_complex_date_comparisons(operands, interval, arithmetic, operator,
table):
(a, b), i = operands(table), interval(table)
Expand Down