defmodule Durex do
@moduledoc """
Parse durations, such as `"1s"`, to its numerical millisecond value, e.g. `1_000`,
so you can do things such as:
```
"1s"
|> Durex.ms!()
|> Process.sleep()
```
## Examples
iex> Durex.ms "3s"
{:ok, 3_000}
iex> Durex.ms "1h"
{:ok, 3600_000}
# Works with float too
iex> Durex.ms "0.5s"
{:ok, 500}
iex> Durex.ms "1.5h"
{:ok, 5400_000}
# Cannot ms duration less than 1ms | 1.0ms
iex> Durex.ms "0.5ms"
:error
# Fractional durations in ms will be truncated
iex> Durex.ms "1.5ms"
{:ok, 1}
Of course, there is also the bang version `ms!/1`:
# Bang version available
iex> Durex.ms! "3s"
3_000
#### Supported units
* `ms` (for millisecond)
* `s` (for second)
* `m` (for minute)
* `h` (for hour)
* `d` (for days)
* `w` (for week)
#### Performance Notes
* Parsing durations which include integers is about 4x faster
than their version containing floats.
So instead of parsing "0.5s", use "500ms" for maximum performance.
* To benchmark, run: `$ mix run bench/ms.exs`
"""
@type duration :: bitstring
defmacrop s_to_ms(s), do: quote(do: 1_000 * unquote(s))
defmacrop m_to_ms(m), do: quote(do: 1_000 * 60 * unquote(m))
defmacrop h_to_ms(h), do: quote(do: 1_000 * 60 * 60 * unquote(h))
defmacrop d_to_ms(d), do: quote(do: 1_000 * 60 * 60 * 24 * unquote(d))
defmacrop w_to_ms(w), do: quote(do: 1_000 * 60 * 60 * 24 * 7 * unquote(w))
@doc "Parse duration as milliseconds"
@spec ms(duration) :: {:ok, pos_integer} | :error
def ms(duration) when is_bitstring(duration) do
case Integer.parse(duration) do
{ms, "ms"} when ms >= 1 ->
{:ok, ms}
{s, "s"} when s > 0 ->
{:ok, s_to_ms(s)}
{m, "m"} when m > 0 ->
{:ok, m_to_ms(m)}
{h, "h"} when h > 0 ->
{:ok, h_to_ms(h)}
{d, "d"} when d > 0 ->
{:ok, d_to_ms(d)}
{w, "w"} when w > 0 ->
{:ok, w_to_ms(w)}
_ ->
case Float.parse(duration) do
{ms, "ms"} when ms >= 1.0 -> {:ok, trunc(ms)}
{s, "s"} when s > 0.0 -> {:ok, trunc(s_to_ms(s))}
{m, "m"} when m > 0.0 -> {:ok, trunc(m_to_ms(m))}
{h, "h"} when h > 0.0 -> {:ok, trunc(h_to_ms(h))}
{d, "d"} when d > 0.0 -> {:ok, trunc(d_to_ms(d))}
{w, "w"} when w > 0.0 -> {:ok, trunc(w_to_ms(w))}
_ -> :error
end
end
end
@doc "Parse duration but raise if it fails"
@spec ms!(duration) :: pos_integer
def ms!(duration) do
case ms(duration) do
{:ok, ms} -> ms
:error -> raise ArgumentError, "cannot parse #{inspect(duration)}"
end
end
end