Skip to content

Commit

Permalink
feat(api): allow passing multiple keyword arguments to ibis.interval
Browse files Browse the repository at this point in the history
  • Loading branch information
jcrist committed May 3, 2023
1 parent 5a013ff commit 22ee854
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 29 deletions.
68 changes: 39 additions & 29 deletions ibis/expr/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,7 @@ def _time_from_deferred(value: Deferred) -> Deferred:
def interval(
value: int | datetime.timedelta | None = None,
unit: str = 's',
*,
years: int | None = None,
quarters: int | None = None,
months: int | None = None,
Expand All @@ -734,7 +735,7 @@ def interval(
Parameters
----------
value
Interval value. If passed, must be combined with `unit`.
Interval value.
unit
Unit of `value`
years
Expand Down Expand Up @@ -765,37 +766,46 @@ def interval(
IntervalScalar
An interval expression
"""
keyword_value_unit = [
("nanoseconds", nanoseconds, "ns"),
("microseconds", microseconds, "us"),
("milliseconds", milliseconds, "ms"),
("seconds", seconds, "s"),
("minutes", minutes, "m"),
("hours", hours, "h"),
("days", days, "D"),
("weeks", weeks, "W"),
("months", months, "M"),
("quarters", quarters, "Q"),
("years", years, "Y"),
]
if value is not None:
for kw, v, _ in keyword_value_unit:
if v is not None:
raise TypeError(f"Cannot provide both 'value' and '{kw}'")
if isinstance(value, datetime.timedelta):
unit = 's'
value = int(value.total_seconds())
elif not isinstance(value, int):
raise ValueError('Interval value must be an integer')
components = [
(value.microseconds, "us"),
(value.seconds, "s"),
(value.days, "D"),
]
components = [(v, u) for v, u in components if v]
elif isinstance(value, int):
components = [(value, unit)]
else:
raise TypeError("value must be an integer or timedelta")
else:
kwds = [
('Y', years),
('Q', quarters),
('M', months),
('W', weeks),
('D', days),
('h', hours),
('m', minutes),
('s', seconds),
('ms', milliseconds),
('us', microseconds),
('ns', nanoseconds),
]
defined_units = [(k, v) for k, v in kwds if v is not None]

if len(defined_units) != 1:
raise ValueError('Exactly one argument is required')

unit, value = defined_units[0]

value_type = literal(value).type()
type = dt.Interval(unit, value_type=value_type)

return literal(value, type=type)
components = [(v, u) for _, v, u in keyword_value_unit if v is not None]

# If no components, default to 0 s
if not components:
components.append((0, "s"))

intervals = [
literal(v, type=dt.Interval(u, value_type=literal(v).type()))
for v, u in components
]
return functools.reduce(operator.add, intervals)


def case() -> bl.SearchedCaseBuilder:
Expand Down
68 changes: 68 additions & 0 deletions ibis/tests/expr/test_temporal.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import ibis.expr.types as ir
from ibis.common.enums import IntervalUnit
from ibis.expr import api
from ibis.tests.util import assert_equal


def test_temporal_literals():
Expand All @@ -20,6 +21,73 @@ def test_temporal_literals():
assert isinstance(timestamp, ir.TimestampScalar)


def test_interval_function_integers():
# No args, default to 0 seconds
assert_equal(ibis.interval(), ibis.interval(0, "s"))

# Default unit is seconds
assert_equal(ibis.interval(1), ibis.interval(1, "s"))

# unit is used if provided
res = ibis.interval(1, "D")
sol = ibis.literal(1, type=dt.Interval("D", value_type=ibis.literal(1).type()))
assert_equal(res, sol)


def test_interval_function_timedelta():
res = ibis.interval(datetime.timedelta())
sol = ibis.interval()
assert_equal(res, sol)

res = ibis.interval(datetime.timedelta(microseconds=10))
sol = ibis.interval(10, "us")
assert_equal(res, sol)

res = ibis.interval(datetime.timedelta(days=5))
sol = ibis.interval(5, "D")
assert_equal(res, sol)

res = ibis.interval(datetime.timedelta(seconds=10, microseconds=2))
sol = ibis.interval(2, "us") + ibis.interval(10, "s")
assert_equal(res, sol)


@pytest.mark.parametrize(
"kw,unit",
[
("nanoseconds", "ns"),
("microseconds", "us"),
("milliseconds", "ms"),
("seconds", "s"),
("minutes", "m"),
("hours", "h"),
("days", "D"),
("weeks", "W"),
("months", "M"),
("quarters", "Q"),
("years", "Y"),
],
)
def test_interval_function_unit_keywords(kw, unit):
res = ibis.interval(**{kw: 1})
sol = ibis.interval(1, unit)
assert_equal(res, sol)


def test_interval_function_multiple_keywords():
res = ibis.interval(microseconds=10, hours=3, days=2)
sol = ibis.interval(10, "us") + ibis.interval(3, "h") + ibis.interval(2, "D")
assert_equal(res, sol)


def test_interval_function_invalid():
with pytest.raises(TypeError, match="integer or timedelta"):
ibis.interval(1.5)

with pytest.raises(TypeError, match="'value' and 'microseconds'"):
ibis.interval(1, microseconds=2)


@pytest.mark.parametrize(
('interval', 'unit', 'expected'),
[
Expand Down

0 comments on commit 22ee854

Please sign in to comment.