Packages

A tiny library to parse human readable formats into milliseconds.

Current section

Files

Jump to
ex_ms lib ex_ms.ex
Raw

lib/ex_ms.ex

defmodule Millisecond do
@moduledoc """
A tiny library to parse human readable formats into milliseconds.
"""
@doc """
A tiny library to parse human readable formats into milliseconds to
make configurations easier.
## Examples
iex> Millisecond.ms("100")
{:ok, 100}
iex> Millisecond.ms("1s")
{:ok, 1_000}
iex> Millisecond.ms!("1s")
1_000
iex> Millisecond.ms("1m")
{:ok, 60_000}
iex> Millisecond.ms("1.5m")
{:ok, 9.0e4}
iex> Millisecond.ms("-0.5m")
{:ok, -3.0e4}
iex> Millisecond.ms("1h")
{:ok, 3_600_000}
iex> Millisecond.ms("1h 1m 1s")
{:ok, 3_661_000}
iex> Millisecond.ms("1y 1mo 1d")
{:ok, 34_236_000_000}
iex> Millisecond.ms("RANDOM STRING")
:error
iex> Millisecond.ms!("1nvalid")
** (ArgumentError) Format is invalid: \"1nvalid\"
iex> Millisecond.ms("1d 1mo 1y")
:error
iex> Millisecond.ms("1hour 1minute 1second")
{:ok, 3_661_000}
iex> Millisecond.ms("1 minutes 1 milliseconds")
{:ok, 60001}
iex> Millisecond.ms("1year 1month 1day")
{:ok, 34_236_000_000}
"""
@second_ms 1000
@minute_ms 60 * @second_ms
@hour_ms 60 * @minute_ms
@day_ms 24 * @hour_ms
@week_ms 7 * @day_ms
@month_ms 30 * @day_ms
@year_ms round(365.25 * @day_ms)
@steps [
:year,
:month,
:week,
:day,
:hour,
:minute,
:second,
:millisecond
]
@type t :: %__MODULE__{
year: number(),
month: number(),
week: number(),
day: number(),
hour: number(),
minute: number(),
second: number(),
millisecond: number()
}
@type millisecond :: pos_integer()
defstruct @steps
@doc """
Converts a string format into milliseconds.
The main function of this library.
## Example
iex> import Millisecond, only: [ms: 1, ms!: 1]
iex> ms!('2 days')
172800000
iex> ms!('1d')
86400000
iex> ms!('10h')
36000000
iex> ms!('2.5 hrs')
9000000
iex> ms!('2h')
7200000
iex> ms!('1m')
60000
iex> ms!('5s')
5000
iex> ms!('1y')
31557600000
iex> ms!('100')
100
iex> ms!('-3 days')
-259200000
iex> ms!('-1h')
-3600000
iex> ms!('-200')
-200
"""
@spec ms(charlist) :: {:ok, millisecond} | :error
def ms(text) do
case parse(text) do
{:ok, data} ->
{:ok, to_milliseconds(data)}
:error ->
:error
end
end
@doc """
This is `ms/1` but returns milliseconds directly on success and raises
an error otherwise.
"""
@spec ms!(charlist) :: millisecond
def ms!(text) do
text
|> parse!()
|> to_milliseconds()
end
@doc """
Converts a string to a `Millisecond` struct.
This is intended to be an low-level function to mainly separate
parsing and conversion.
## Examples
iex> Millisecond.parse("1h 1m 1s")
{:ok, %Millisecond{hour: 1, minute: 1, second: 1}}
iex> Millisecond.parse("invalid format")
:error
"""
@spec parse(charlist()) :: {:ok, t()} | :error
def parse(text) when is_binary(text) do
text
|> String.trim()
|> String.downcase()
|> String.split(" ", trim: true, parts: 3 * length(@steps))
|> Enum.map(&parse_number/1)
|> (fn units ->
case units do
[{:error, _}] ->
:error
[{_, _}] ->
do_process(units)
units ->
units
|> Enum.reduce_while({nil, []}, fn res, {quantity, grouped_units} ->
case res do
{:error, _} when is_nil(quantity) ->
{:halt, :error}
{:error, unit} ->
{:cont, {nil, [{quantity, unit} | grouped_units]}}
{new_quantity, ""} when is_nil(quantity) ->
{:cont, {new_quantity, grouped_units}}
{_, ""} ->
{:halt, :error}
unit ->
{:cont, {nil, [unit | grouped_units]}}
end
end)
|> case do
:error ->
:error
{nil, grouped_units} ->
grouped_units
|> Enum.reverse()
|> do_process()
{_, _} ->
:error
end
end
end).()
end
def parse(_), do: :error
@doc """
This is `parse/1` but returns the data directly on success and raises
an error otherwise.
"""
@spec parse!(charlist) :: t()
def parse!(text) do
case parse(text) do
{:ok, mil1iseconds} -> mil1iseconds
:error -> raise ArgumentError, "Format is invalid: #{inspect(text)}"
end
end
defp parse_number(text) do
if String.contains?(text, ".") do
Float.parse(text)
else
Integer.parse(text)
end
|> case do
:error -> {:error, text}
res -> res
end
end
defp do_process([]), do: :error
defp do_process(values) do
@steps
|> Enum.reduce_while({%Millisecond{}, values}, fn step, acc ->
with {state, [value | remaining_values]} <- acc,
{quantity, unit} <- value,
true <- do_parse_unit(step, unit) do
{:cont, {Map.put(state, step, quantity), remaining_values}}
else
false -> {:cont, acc}
_ -> {:halt, acc}
end
end)
|> case do
{state, []} ->
{:ok, state}
_ ->
:error
end
end
@year_units ["years", "year", "yrs", "yr", "y"]
defp do_parse_unit(:year, unit) when unit in @year_units, do: true
@month_units ["months", "month", "mo"]
defp do_parse_unit(:month, unit) when unit in @month_units, do: true
@week_units ["weeks", "week", "w"]
defp do_parse_unit(:week, unit) when unit in @week_units, do: true
@day_units ["days", "day", "d"]
defp do_parse_unit(:day, unit) when unit in @day_units, do: true
@hour_units ["hours", "hour", "hrs", "hr", "h"]
defp do_parse_unit(:hour, unit) when unit in @hour_units, do: true
@minute_units ["minutes", "minute", "mins", "min", "m"]
defp do_parse_unit(:minute, unit) when unit in @minute_units,
do: true
@second_units ["seconds", "second", "secs", "sec", "s"]
defp do_parse_unit(:second, unit) when unit in @second_units, do: true
@millisecond_units ["milliseconds", "millisecond", "msecs", "msec", "ms"]
defp do_parse_unit(:millisecond, unit) when unit in @millisecond_units, do: true
defp do_parse_unit(:millisecond, ""), do: true
defp do_parse_unit(_, _), do: false
@doc """
Converts a `Millisecond` struct to the intended milliseconds format.
This is intended to be an low-level function to mainly separate
parsing and conversion.
## Examples
iex> data = Millisecond.parse!("1h 1m 1s")
iex> Millisecond.to_milliseconds(data)
3_661_000
iex> data = Millisecond.parse!("1y 1mo 1d")
iex> Millisecond.to_milliseconds(data)
34_236_000_000
"""
@spec to_milliseconds(t()) :: millisecond()
def to_milliseconds(%Millisecond{} = data) do
@steps
|> Enum.reduce(0, fn step, acc ->
if multiplier = Map.get(data, step, nil) do
value =
case step do
:year -> @year_ms
:month -> @month_ms
:week -> @week_ms
:day -> @day_ms
:hour -> @hour_ms
:minute -> @minute_ms
:second -> @second_ms
:millisecond -> 1
end
acc + multiplier * value
else
acc
end
end)
end
@doc """
Adds the `Millisecond` to a `DateTime` to produce a future datetime.
THis is an example of its intended use for configuration.
## Examples
iex> ms = Millisecond.parse!("100ms")
iex> now = DateTime.utc_now()
iex> now |> Millisecond.add(ms) |> DateTime.diff(now, :millisecond)
100
"""
@spec add(DateTime.t(), t()) :: DateTime.t()
def add(%DateTime{} = datetime, %Millisecond{} = data) do
data
|> to_milliseconds()
|> (&DateTime.add(datetime, &1, :millisecond)).()
end
@doc """
Subtracts the `Millisecond` from a `DateTime` to produce a past datetime.
THis is an example of its intended use for configuration.
## Examples
iex> ms = Millisecond.parse!("100ms")
iex> now = DateTime.utc_now()
iex> now |> Millisecond.subtract(ms) |> DateTime.diff(now, :millisecond)
-100
"""
@spec subtract(DateTime.t(), t()) :: DateTime.t()
def subtract(%DateTime{} = datetime, %Millisecond{} = data) do
data
|> to_milliseconds()
|> (&DateTime.add(datetime, -&1, :millisecond)).()
end
if Code.ensure_compiled(Timex) == {:module, Timex} do
@doc """
Converts an `Millisecond` struct into a `Timex.Duration` struct.
This is inteded to show that `Millisecond` can be converted into a
more appropriate date/time struct such `Timex.Duration`. If you have
`Timex` already, perhaps use `Timex.Duration.parse/1` as it uses the
ISO 8601 Duration format.
## Examples
iex> {:ok, data} = Millisecond.parse("1h 1s")
iex> duration = Millisecond.to_duration(data)
iex> Timex.Duration.to_string(duration)
"PT1H1S"
iex> {:ok, data} = Millisecond.parse("1d 1h")
iex> {:ok, duration} = Timex.Duration.parse("P1DT1H")
iex> Millisecond.to_duration(data) == duration
true
"""
@spec to_duration(t()) :: Timex.Duration.t()
def to_duration(data) do
alias Timex.Duration
Enum.reduce(@steps, Duration.zero(), fn step, acc ->
if multiplier = Map.get(data, step, nil) do
duration =
case step do
:year -> Duration.from_days(365 * multiplier)
:month -> Duration.from_days(30 * multiplier)
:week -> Duration.from_days(7 * multiplier)
:day -> Duration.from_days(multiplier)
:hour -> Duration.from_hours(multiplier)
:minute -> Duration.from_minutes(multiplier)
:second -> Duration.from_seconds(multiplier)
:millisecond -> Duration.from_milliseconds(multiplier)
end
Duration.add(acc, duration)
else
acc
end
end)
end
end
end