Skip to content

Commit 3a06089

Browse files
authored
Add Kernel.to_timeout/1 (#13468)
1 parent c557310 commit 3a06089

File tree

2 files changed

+213
-0
lines changed

2 files changed

+213
-0
lines changed

lib/elixir/lib/kernel.ex

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6093,6 +6093,150 @@ defmodule Kernel do
60936093
Macro.compile_apply(mod, fun, [code, options, __CALLER__ | args], __CALLER__)
60946094
end
60956095

6096+
hour_in_ms = 1000 * 60 * 60
6097+
day_in_ms = 24 * hour_in_ms
6098+
week_in_ms = 7 * day_in_ms
6099+
6100+
@doc """
6101+
Constructs a millisecond timeout from the given components, duration, or timeout.
6102+
6103+
This function is useful for constructing timeouts to use in functions that
6104+
expect `t:timeout/0` values (such as `Process.send_after/4` and many others).
6105+
6106+
## Argument
6107+
6108+
The `duration` argument can be one of a `Duration`, a `t:timeout/0`, or a list
6109+
of components. Each of these is described below.
6110+
6111+
### Passing `Duration`s
6112+
6113+
`t:Duration.t/0` structs can be converted to timeouts. The given duration must have
6114+
`year` and `month` fields set to `0`, since those cannot be reliably converted to
6115+
milliseconds (due to the varying number of days in a month and year).
6116+
6117+
Microseconds in durations are converted to milliseconds (through `System.convert_time_unit/3`).
6118+
6119+
### Passing components
6120+
6121+
The `duration` argument can also be keyword list which can contain the following
6122+
keys, each appearing at most once with a non-negative integer value:
6123+
6124+
* `:week` - the number of weeks (a week is always 7 days)
6125+
* `:day` - the number of days (a day is always 24 hours)
6126+
* `:hour` - the number of hours
6127+
* `:minute` - the number of minutes
6128+
* `:second` - the number of seconds
6129+
* `:millisecond` - the number of milliseconds
6130+
6131+
The timeout is calculated as the sum of the components, each multiplied by
6132+
the corresponding factor.
6133+
6134+
### Passing timeouts
6135+
6136+
You can also pass timeouts directly to this functions, that is, milliseconds or
6137+
the atom `:infinity`. In this case, this function just returns the given argument.
6138+
6139+
## Examples
6140+
6141+
With a keyword list:
6142+
6143+
iex> to_timeout(hour: 1, minute: 30)
6144+
5400000
6145+
6146+
With a duration:
6147+
6148+
iex> to_timeout(%Duration{hour: 1, minute: 30})
6149+
5400000
6150+
6151+
With a timeout:
6152+
6153+
iex> to_timeout(5400000)
6154+
5400000
6155+
iex> to_timeout(:infinity)
6156+
:infinity
6157+
6158+
"""
6159+
@doc since: "1.17.0"
6160+
@spec to_timeout([component, ...] | timeout() | Duration.t()) :: non_neg_integer()
6161+
when component: [{unit, non_neg_integer()}, ...],
6162+
unit: :week | :day | :hour | :minute | :second | :millisecond
6163+
def to_timeout(duration)
6164+
6165+
def to_timeout(:infinity), do: :infinity
6166+
def to_timeout(timeout) when is_integer(timeout) and timeout >= 0, do: timeout
6167+
6168+
def to_timeout(%{__struct__: Duration} = duration) do
6169+
case duration do
6170+
%{year: year} when year != 0 ->
6171+
raise ArgumentError,
6172+
"duration with a non-zero year cannot be reliably converted to timeouts"
6173+
6174+
%{month: month} when month != 0 ->
6175+
raise ArgumentError,
6176+
"duration with a non-zero month cannot be reliably converted to timeouts"
6177+
6178+
_other ->
6179+
{microsecond, _precision} = duration.microsecond
6180+
millisecond = :erlang.convert_time_unit(microsecond, :microsecond, :millisecond)
6181+
6182+
duration.week * unquote(week_in_ms) +
6183+
duration.day * unquote(day_in_ms) +
6184+
duration.hour * unquote(hour_in_ms) +
6185+
duration.minute * 60_000 +
6186+
duration.second * 1000 +
6187+
millisecond
6188+
end
6189+
end
6190+
6191+
def to_timeout(components) when is_list(components) do
6192+
reducer = fn
6193+
{key, value}, {acc, seen_keys} when is_integer(value) and value >= 0 ->
6194+
case :lists.member(key, seen_keys) do
6195+
true ->
6196+
raise ArgumentError, "timeout component #{inspect(key)} is duplicated"
6197+
6198+
false ->
6199+
:ok
6200+
end
6201+
6202+
factor =
6203+
case key do
6204+
:week ->
6205+
unquote(week_in_ms)
6206+
6207+
:day ->
6208+
unquote(day_in_ms)
6209+
6210+
:hour ->
6211+
unquote(hour_in_ms)
6212+
6213+
:minute ->
6214+
60_000
6215+
6216+
:second ->
6217+
1000
6218+
6219+
:millisecond ->
6220+
1
6221+
6222+
other ->
6223+
raise ArgumentError, """
6224+
timeout component #{inspect(other)} is not a valid timeout component, valid \
6225+
values are: :week, :day, :hour, :minute, :second, :millisecond\
6226+
"""
6227+
end
6228+
6229+
{acc + value * factor, [key | seen_keys]}
6230+
6231+
{key, value}, {_acc, _seen_keys} ->
6232+
raise ArgumentError,
6233+
"timeout component #{inspect(key)} must be a non-negative " <>
6234+
"integer, got: #{inspect(value)}"
6235+
end
6236+
6237+
elem(:lists.foldl(reducer, {0, _seen_keys = []}, components), 0)
6238+
end
6239+
60966240
## Sigils
60976241

60986242
@doc ~S"""

lib/elixir/test/elixir/kernel_test.exs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,4 +1500,73 @@ defmodule KernelTest do
15001500
""")
15011501
end
15021502
end
1503+
1504+
describe "to_timeout/1" do
1505+
test "works with keyword lists" do
1506+
assert to_timeout(hour: 2) == 1000 * 60 * 60 * 2
1507+
assert to_timeout(minute: 74) == 1000 * 60 * 74
1508+
assert to_timeout(second: 1293) == 1_293_000
1509+
assert to_timeout(millisecond: 1_234_123) == 1_234_123
1510+
1511+
assert to_timeout(hour: 2, minute: 30) == 1000 * 60 * 60 * 2 + 1000 * 60 * 30
1512+
assert to_timeout(minute: 30, hour: 2) == 1000 * 60 * 60 * 2 + 1000 * 60 * 30
1513+
assert to_timeout(minute: 74, second: 30) == 1000 * 60 * 74 + 1000 * 30
1514+
end
1515+
1516+
test "raises on invalid values with keyword lists" do
1517+
for unit <- [:hour, :minute, :second, :millisecond],
1518+
value <- [-1, 1.0, :not_an_int] do
1519+
message =
1520+
"timeout component #{inspect(unit)} must be a non-negative integer, " <>
1521+
"got: #{inspect(value)}"
1522+
1523+
assert_raise ArgumentError, message, fn -> to_timeout([{unit, value}]) end
1524+
end
1525+
end
1526+
1527+
test "raises on invalid keys with keyword lists" do
1528+
message =
1529+
"timeout component :not_a_unit is not a valid timeout component, valid values are: " <>
1530+
":week, :day, :hour, :minute, :second, :millisecond"
1531+
1532+
assert_raise ArgumentError, message, fn -> to_timeout(minute: 3, not_a_unit: 1) end
1533+
end
1534+
1535+
test "raises on duplicated components with keyword lists" do
1536+
assert_raise ArgumentError, "timeout component :minute is duplicated", fn ->
1537+
to_timeout(minute: 3, hour: 2, minute: 1)
1538+
end
1539+
end
1540+
1541+
test "works with durations" do
1542+
assert to_timeout(Duration.new!(hour: 2)) == 1000 * 60 * 60 * 2
1543+
assert to_timeout(Duration.new!(minute: 74)) == 1000 * 60 * 74
1544+
assert to_timeout(Duration.new!(second: 1293)) == 1_293_000
1545+
assert to_timeout(Duration.new!(microsecond: {1_234_123, 4})) == 1_234
1546+
1547+
assert to_timeout(Duration.new!(hour: 2, minute: 30)) == 1000 * 60 * 60 * 2 + 1000 * 60 * 30
1548+
assert to_timeout(Duration.new!(minute: 30, hour: 2)) == 1000 * 60 * 60 * 2 + 1000 * 60 * 30
1549+
assert to_timeout(Duration.new!(minute: 74, second: 30)) == 1000 * 60 * 74 + 1000 * 30
1550+
end
1551+
1552+
test "raises on durations with non-zero months or days" do
1553+
message = "duration with a non-zero month cannot be reliably converted to timeouts"
1554+
1555+
assert_raise ArgumentError, message, fn ->
1556+
to_timeout(Duration.new!(month: 3))
1557+
end
1558+
1559+
message = "duration with a non-zero year cannot be reliably converted to timeouts"
1560+
1561+
assert_raise ArgumentError, message, fn ->
1562+
to_timeout(Duration.new!(year: 1))
1563+
end
1564+
end
1565+
1566+
test "works with timeouts" do
1567+
assert to_timeout(1_000) == 1_000
1568+
assert to_timeout(0) == 0
1569+
assert to_timeout(:infinity) == :infinity
1570+
end
1571+
end
15031572
end

0 commit comments

Comments
 (0)