defmodule Timex.Duration do
@moduledoc """
This module provides a friendly API for working with Erlang
timestamps, i.e. `{megasecs, secs, microsecs}`. In addition,
it provides an easy way to wrap the measurement of function
execution time (via `measure`).
"""
alias __MODULE__
alias Timex.Types
use Timex.Constants
@enforce_keys [:megaseconds, :seconds, :microseconds]
defstruct megaseconds: 0, seconds: 0, microseconds: 0
@type t :: %__MODULE__{
megaseconds: integer,
seconds: integer,
microseconds: integer
}
@type units ::
:microsecond
| :microseconds
| :millisecond
| :milliseconds
| :second
| :seconds
| :minutes
| :hours
| :days
| :weeks
@type measurement_units :: :microseconds | :milliseconds | :seconds | :minutes | :hours
@type to_options :: [truncate: boolean]
@doc """
Converts a Duration to an Erlang timestamp
## Example
iex> d = %Timex.Duration{megaseconds: 1, seconds: 2, microseconds: 3}
...> Timex.Duration.to_erl(d)
{1, 2, 3}
"""
@spec to_erl(__MODULE__.t()) :: Types.timestamp()
def to_erl(%__MODULE__{} = d),
do: {d.megaseconds, d.seconds, d.microseconds}
@doc """
Converts an Erlang timestamp to a Duration
## Example
iex> Timex.Duration.from_erl({1, 2, 3})
%Timex.Duration{megaseconds: 1, seconds: 2, microseconds: 3}
"""
@spec from_erl(Types.timestamp()) :: __MODULE__.t()
def from_erl({mega, sec, micro}),
do: %__MODULE__{megaseconds: mega, seconds: sec, microseconds: micro}
@doc """
Converts a Duration to a Time if the duration fits within a 24-hour clock.
If it does not, an error tuple is returned.
## Examples
iex> d = %Timex.Duration{megaseconds: 0, seconds: 4000, microseconds: 0}
...> Timex.Duration.to_time(d)
{:ok, ~T[01:06:40]}
iex> d = %Timex.Duration{megaseconds: 1, seconds: 0, microseconds: 0}
...> Timex.Duration.to_time(d)
{:error, :invalid_time}
"""
@spec to_time(__MODULE__.t()) :: {:ok, Time.t()} | {:error, atom}
def to_time(%__MODULE__{} = d) do
{h, m, s, us} = to_clock(d)
Time.from_erl({h, m, s}, Timex.DateTime.Helpers.construct_microseconds(us, -1))
end
@doc """
Same as to_time/1, but returns the Time directly. Raises an error if the
duration does not fit within a 24-hour clock.
## Examples
iex> d = %Timex.Duration{megaseconds: 0, seconds: 4000, microseconds: 0}
...> Timex.Duration.to_time!(d)
~T[01:06:40]
iex> d = %Timex.Duration{megaseconds: 1, seconds: 0, microseconds: 0}
...> Timex.Duration.to_time!(d)
** (ArgumentError) cannot convert {277, 46, 40} to time, reason: :invalid_time
"""
@spec to_time!(__MODULE__.t()) :: Time.t() | no_return
def to_time!(%__MODULE__{} = d) do
{h, m, s, us} = to_clock(d)
Time.from_erl!({h, m, s}, Timex.DateTime.Helpers.construct_microseconds(us, -1))
end
@doc """
Converts a Time to a Duration
## Example
iex> Timex.Duration.from_time(~T[01:01:30])
%Timex.Duration{megaseconds: 0, seconds: 3690, microseconds: 0}
"""
@spec from_time(Time.t()) :: __MODULE__.t()
def from_time(%Time{} = t) do
{us, _} = t.microsecond
from_clock({t.hour, t.minute, t.second, us})
end
@doc """
Converts a Duration to a string, using the ISO standard for formatting durations.
## Examples
iex> d = %Timex.Duration{megaseconds: 0, seconds: 3661, microseconds: 0}
...> Timex.Duration.to_string(d)
"PT1H1M1S"
iex> d = %Timex.Duration{megaseconds: 102, seconds: 656013, microseconds: 33}
...> Timex.Duration.to_string(d)
"P3Y3M3DT3H33M33.000033S"
"""
@spec to_string(__MODULE__.t()) :: String.t()
def to_string(%__MODULE__{} = duration) do
Timex.Format.Duration.Formatter.format(duration)
end
@doc """
Parses a duration string (in ISO-8601 format) into a Duration struct.
"""
@spec parse(String.t()) :: {:ok, __MODULE__.t()} | {:error, term}
defdelegate parse(str), to: Timex.Parse.Duration.Parser
@doc """
Parses a duration string into a Duration struct, using the provided parser module.
"""
@spec parse(String.t(), module()) :: {:ok, __MODULE__.t()} | {:error, term}
defdelegate parse(str, module), to: Timex.Parse.Duration.Parser
@doc """
Same as parse/1, but returns the Duration unwrapped, and raises on error
"""
@spec parse!(String.t()) :: __MODULE__.t() | no_return
defdelegate parse!(str), to: Timex.Parse.Duration.Parser
@doc """
Same as parse/2, but returns the Duration unwrapped, and raises on error
"""
@spec parse!(String.t(), module()) :: __MODULE__.t() | no_return
defdelegate parse!(str, module), to: Timex.Parse.Duration.Parser
@microseconds_per_hour 3600 * 1_000_000
@doc """
Converts a Duration to a clock tuple, i.e. `{hour,minute,second,microsecond}`.
## Example
iex> d = %Timex.Duration{megaseconds: 1, seconds: 1, microseconds: 50}
...> Timex.Duration.to_clock(d)
{277, 46, 41, 50}
"""
def to_clock(%__MODULE__{} = duration) do
us = to_microseconds(duration)
hours = div(us, @microseconds_per_hour)
total_secs = div(rem(us, @microseconds_per_hour), 1_000_000)
mins = div(total_secs, 60)
secs = rem(total_secs, 60)
micros = rem(rem(us, @microseconds_per_hour), 1_000_000)
{hours, mins, secs, micros}
end
@doc """
Converts a clock tuple, i.e. `{hour, minute, second, microsecond}` to a Duration.
## Example
iex> Timex.Duration.from_clock({1, 2, 3, 4})
%Timex.Duration{megaseconds: 0, seconds: 3723, microseconds: 4}
"""
def from_clock({hours, mins, secs, us}) do
us = us + (secs + mins * 60) * 1_000_000 + hours * @microseconds_per_hour
from_microseconds(us)
end
@doc """
Converts a Duration to its value in microseconds
## Example
iex> Duration.to_microseconds(Duration.from_milliseconds(10.5))
10_500
"""
@spec to_microseconds(__MODULE__.t()) :: integer
@spec to_microseconds(__MODULE__.t(), to_options) :: integer
def to_microseconds(%Duration{megaseconds: mega, seconds: sec, microseconds: micro}) do
mega * 1_000_000_000_000 + sec * 1_000_000 + micro
end
def to_microseconds(%Duration{} = duration, _opts), do: to_microseconds(duration)
@doc """
Converts a Duration to its value in milliseconds
## Example
iex> Duration.to_milliseconds(Duration.from_seconds(1))
1000.0
iex> Duration.to_milliseconds(Duration.from_seconds(1.543))
1543.0
iex> Duration.to_milliseconds(Duration.from_seconds(1.543), truncate: true)
1543
"""
@spec to_milliseconds(__MODULE__.t()) :: float
@spec to_milliseconds(__MODULE__.t(), to_options) :: float | integer
def to_milliseconds(%__MODULE__{} = d), do: to_microseconds(d) / 1_000
def to_milliseconds(%__MODULE__{} = d, truncate: true), do: trunc(to_milliseconds(d))
def to_milliseconds(%__MODULE__{} = d, _opts), do: to_milliseconds(d)
@doc """
Converts a Duration to its value in seconds
## Example
iex> Duration.to_seconds(Duration.from_milliseconds(1500))
1.5
iex> Duration.to_seconds(Duration.from_milliseconds(1500), truncate: true)
1
"""
@spec to_seconds(__MODULE__.t()) :: float
@spec to_seconds(__MODULE__.t(), to_options) :: float | integer
def to_seconds(%__MODULE__{} = d), do: to_microseconds(d) / (1_000 * 1_000)
def to_seconds(%__MODULE__{} = d, truncate: true), do: trunc(to_seconds(d))
def to_seconds(%__MODULE__{} = d, _opts), do: to_seconds(d)
@doc """
Converts a Duration to its value in minutes
## Example
iex> Duration.to_minutes(Duration.from_seconds(90))
1.5
iex> Duration.to_minutes(Duration.from_seconds(65), truncate: true)
1
"""
@spec to_minutes(__MODULE__.t()) :: float
@spec to_minutes(__MODULE__.t(), to_options) :: float | integer
def to_minutes(%__MODULE__{} = d), do: to_microseconds(d) / (1_000 * 1_000 * 60)
def to_minutes(%__MODULE__{} = d, truncate: true), do: trunc(to_minutes(d))
def to_minutes(%__MODULE__{} = d, _opts), do: to_minutes(d)
@doc """
Converts a Duration to its value in hours
## Example
iex> Duration.to_hours(Duration.from_minutes(105))
1.75
iex> Duration.to_hours(Duration.from_minutes(105), truncate: true)
1
"""
@spec to_hours(__MODULE__.t()) :: float
@spec to_hours(__MODULE__.t(), to_options) :: float | integer
def to_hours(%__MODULE__{} = d), do: to_microseconds(d) / (1_000 * 1_000 * 60 * 60)
def to_hours(%__MODULE__{} = d, truncate: true), do: trunc(to_hours(d))
def to_hours(%__MODULE__{} = d, _opts), do: to_hours(d)
@doc """
Converts a Duration to its value in days
## Example
iex> Duration.to_days(Duration.from_hours(6))
0.25
iex> Duration.to_days(Duration.from_hours(25), truncate: true)
1
"""
@spec to_days(__MODULE__.t()) :: float
@spec to_days(__MODULE__.t(), to_options) :: float | integer
def to_days(%__MODULE__{} = d), do: to_microseconds(d) / (1_000 * 1_000 * 60 * 60 * 24)
def to_days(%__MODULE__{} = d, truncate: true), do: trunc(to_days(d))
def to_days(%__MODULE__{} = d, _opts), do: to_days(d)
@doc """
Converts a Duration to its value in weeks
## Example
iex> Duration.to_weeks(Duration.from_days(14))
2.0
iex> Duration.to_weeks(Duration.from_days(13), truncate: true)
1
"""
@spec to_weeks(__MODULE__.t()) :: float
@spec to_weeks(__MODULE__.t(), to_options) :: float | integer
def to_weeks(%__MODULE__{} = d), do: to_microseconds(d) / (1_000 * 1_000 * 60 * 60 * 24 * 7)
def to_weeks(%__MODULE__{} = d, truncate: true), do: trunc(to_weeks(d))
def to_weeks(%__MODULE__{} = d, _opts), do: to_weeks(d)
Enum.each(
[
{:microseconds, 1 / @usecs_in_sec},
{:milliseconds, 1 / @msecs_in_sec},
{:seconds, 1},
{:minutes, @secs_in_min},
{:hours, @secs_in_hour},
{:days, @secs_in_day},
{:weeks, @secs_in_week}
],
fn {type, coef} ->
@spec to_microseconds(integer | float, unquote(type)) :: float
def to_microseconds(value, unquote(type)),
do: do_round(value * unquote(coef) * @usecs_in_sec)
@spec to_milliseconds(integer | float, unquote(type)) :: float
def to_milliseconds(value, unquote(type)),
do: do_round(value * unquote(coef) * @msecs_in_sec)
@spec to_seconds(integer | float, unquote(type)) :: float
def to_seconds(value, unquote(type)),
do: do_round(value * unquote(coef))
@spec to_minutes(integer | float, unquote(type)) :: float
def to_minutes(value, unquote(type)),
do: do_round(value * unquote(coef) / @secs_in_min)
@spec to_hours(integer | float, unquote(type)) :: float
def to_hours(value, unquote(type)),
do: do_round(value * unquote(coef) / @secs_in_hour)
@spec to_days(integer | float, unquote(type)) :: float
def to_days(value, unquote(type)),
do: do_round(value * unquote(coef) / @secs_in_day)
@spec to_weeks(integer | float, unquote(type)) :: float
def to_weeks(value, unquote(type)),
do: do_round(value * unquote(coef) / @secs_in_week)
end
)
@doc """
Converts an integer value representing microseconds to a Duration
"""
@spec from_microseconds(integer) :: __MODULE__.t()
def from_microseconds(us) when is_integer(us) do
mega = div(us, 1_000_000_000_000)
sec = div(rem(us, 1_000_000_000_000), 1_000_000)
micro = rem(us, 1_000_000)
%Duration{megaseconds: mega, seconds: sec, microseconds: micro}
end
def from_microseconds(us) when is_float(us) do
from_microseconds(trunc(us))
end
@doc """
Converts an integer value representing milliseconds to a Duration
"""
@spec from_milliseconds(integer | float) :: __MODULE__.t()
def from_milliseconds(ms), do: from_microseconds(ms * @usecs_in_msec)
@doc """
Converts an integer value representing seconds to a Duration
"""
@spec from_seconds(integer | float) :: __MODULE__.t()
def from_seconds(s), do: from_microseconds(s * @usecs_in_sec)
@doc """
Converts an integer value representing minutes to a Duration
"""
@spec from_minutes(integer | float) :: __MODULE__.t()
def from_minutes(m), do: from_seconds(m * @secs_in_min)
@doc """
Converts an integer value representing hours to a Duration
"""
@spec from_hours(integer | float) :: __MODULE__.t()
def from_hours(h), do: from_seconds(h * @secs_in_hour)
@doc """
Converts an integer value representing days to a Duration
"""
@spec from_days(integer | float) :: __MODULE__.t()
def from_days(d), do: from_seconds(d * @secs_in_day)
@doc """
Converts an integer value representing weeks to a Duration
"""
@spec from_weeks(integer | float) :: __MODULE__.t()
def from_weeks(w), do: from_seconds(w * @secs_in_week)
@doc """
Add one Duration to another.
## Examples
iex> d = %Timex.Duration{megaseconds: 1, seconds: 1, microseconds: 1}
...> Timex.Duration.add(d, d)
%Timex.Duration{megaseconds: 2, seconds: 2, microseconds: 2}
iex> d = %Timex.Duration{megaseconds: 1, seconds: 750000, microseconds: 750000}
...> Timex.Duration.add(d, d)
%Timex.Duration{megaseconds: 3, seconds: 500001, microseconds: 500000}
"""
@spec add(__MODULE__.t(), __MODULE__.t()) :: __MODULE__.t()
def add(
%Duration{megaseconds: mega1, seconds: sec1, microseconds: micro1},
%Duration{megaseconds: mega2, seconds: sec2, microseconds: micro2}
) do
normalize(%Duration{
megaseconds: mega1 + mega2,
seconds: sec1 + sec2,
microseconds: micro1 + micro2
})
end
@doc """
Subtract one Duration from another.
## Example
iex> d1 = %Timex.Duration{megaseconds: 3, seconds: 3, microseconds: 3}
...> d2 = %Timex.Duration{megaseconds: 2, seconds: 2, microseconds: 2}
...> Timex.Duration.sub(d1, d2)
%Timex.Duration{megaseconds: 1, seconds: 1, microseconds: 1}
"""
@spec sub(__MODULE__.t(), __MODULE__.t()) :: __MODULE__.t()
def sub(
%Duration{megaseconds: mega1, seconds: sec1, microseconds: micro1},
%Duration{megaseconds: mega2, seconds: sec2, microseconds: micro2}
) do
normalize(%Duration{
megaseconds: mega1 - mega2,
seconds: sec1 - sec2,
microseconds: micro1 - micro2
})
end
@doc """
Scale a Duration by some coefficient value, i.e. a scale of 2 is twice is long.
## Example
iex> d = %Timex.Duration{megaseconds: 1, seconds: 1, microseconds: 1}
...> Timex.Duration.scale(d, 2)
%Timex.Duration{megaseconds: 2, seconds: 2, microseconds: 2}
"""
@spec scale(__MODULE__.t(), coefficient :: integer | float) :: __MODULE__.t()
def scale(%Duration{megaseconds: mega, seconds: secs, microseconds: micro}, coef) do
mega_s = mega * coef
s_diff = mega_s * 1_000_000 - trunc(mega_s) * 1_000_000
secs_s = s_diff + secs * coef
us_diff = secs_s * 1_000_000 - trunc(secs_s) * 1_000_000
us_s = us_diff + micro * coef
extra_mega = div(trunc(secs_s), 1_000_000)
mega_final = trunc(mega_s) + extra_mega
extra_secs = div(trunc(us_s), 1_000_000)
secs_final = trunc(secs_s) - extra_mega * 1_000_000 + extra_secs
us_final = trunc(us_s) - extra_secs * 1_000_000
normalize(%Duration{megaseconds: mega_final, seconds: secs_final, microseconds: us_final})
end
@doc """
Invert a Duration, i.e. a positive duration becomes a negative one, and vice versa
## Example
iex> d = %Timex.Duration{megaseconds: -1, seconds: -2, microseconds: -3}
...> Timex.Duration.invert(d)
%Timex.Duration{megaseconds: 1, seconds: 2, microseconds: 3}
"""
@spec invert(__MODULE__.t()) :: __MODULE__.t()
def invert(%Duration{megaseconds: mega, seconds: sec, microseconds: micro}) do
%Duration{megaseconds: -mega, seconds: -sec, microseconds: -micro}
end
@doc """
Returns the absolute value of the provided Duration.
## Example
iex> d = %Timex.Duration{megaseconds: -1, seconds: -2, microseconds: -3}
...> Timex.Duration.abs(d)
%Timex.Duration{megaseconds: 1, seconds: 2, microseconds: 3}
"""
@spec abs(__MODULE__.t()) :: __MODULE__.t()
def abs(%Duration{} = duration) do
us = to_microseconds(duration)
if us < 0 do
from_microseconds(-us)
else
duration
end
end
@doc """
Return a timestamp representing a time lapse of length 0.
iex> Timex.Duration.zero |> Timex.Duration.to_seconds
0.0
Can be useful for operations on collections of durations. For instance,
Enum.reduce(durations, Duration.zero, Duration.add(&1, &2))
Can also be used to represent the timestamp of the start of the UNIX epoch,
as all Erlang timestamps are relative to this point.
"""
@spec zero() :: __MODULE__.t()
def zero, do: %Duration{megaseconds: 0, seconds: 0, microseconds: 0}
@doc """
Returns the duration since the first day of year 0 to Epoch.
## Example
iex> Timex.Duration.epoch()
%Timex.Duration{megaseconds: 62_167, seconds: 219_200, microseconds: 0}
"""
@spec epoch() :: __MODULE__.t()
def epoch() do
epoch(nil)
end
@doc """
Returns the amount of time since the first day of year 0 to Epoch.
The argument is an atom indicating the type of time units to return.
The allowed unit type atoms are:
- :microseconds
- :milliseconds
- :seconds
- :minutes
- :hours
- :days
- :weeks
## Examples
iex> Timex.Duration.epoch(:seconds)
62_167_219_200
If the specified type is nil, a duration since the first day of year 0 to Epoch
is returned.
iex> Timex.Duration.epoch(nil)
%Timex.Duration{megaseconds: 62_167, seconds: 219_200, microseconds: 0}
"""
@spec epoch(nil) :: __MODULE__.t()
@spec epoch(units) :: non_neg_integer
def epoch(type) do
seconds = :calendar.datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}})
case type do
nil ->
from_seconds(seconds)
:microseconds ->
seconds |> from_seconds |> to_microseconds
:milliseconds ->
seconds |> from_seconds |> to_milliseconds
:seconds ->
seconds
:minutes ->
seconds |> from_seconds |> to_minutes
:hours ->
seconds |> from_seconds |> to_hours
:days ->
seconds |> from_seconds |> to_days
:weeks ->
seconds |> from_seconds |> to_weeks
end
end
@doc """
Returns the amount of time since Epoch.
The argument is an atom indicating the type of time units to return.
The allowed unit type atoms are:
- :microsecond(s)
- :millisecond(s)
- :second(s)
- :minutes
- :hours
- :days
- :weeks
## Examples
iex> Timex.Duration.now(:seconds)
1483141644
When the argument is omitted or nil, a Duration is returned.
iex> Timex.Duration.now
%Timex.Duration{megaseconds: 1483, seconds: 141562, microseconds: 536938}
"""
@spec now() :: __MODULE__.t()
@spec now(nil) :: __MODULE__.t()
@spec now(units) :: non_neg_integer
def now(type \\ nil)
@from_micros_units [:native, :nanosecond, :nanoseconds, :microsecond, :microseconds]
def now(nil), do: from_microseconds(now(:microsecond))
def now(unit) when unit in @from_micros_units,
do: System.system_time(:microsecond)
def now(ms) when ms in [:millisecond, :milliseconds],
do: System.system_time(:millisecond)
def now(s) when s in [:second, :seconds], do: System.system_time(:second)
def now(:minutes), do: to_minutes(now(:microsecond))
def now(:hours), do: to_hours(now(:microsecond))
def now(:days), do: to_days(now(:microsecond))
def now(:weeks), do: to_weeks(now(:microsecond))
@doc """
An alias for `Duration.diff/3`
"""
defdelegate elapsed(duration, ref \\ nil, type \\ nil), to: __MODULE__, as: :diff
@doc """
This function determines the difference in time between two timestamps
(represented by Duration structs). If the second timestamp is omitted,
`Duration.now` will be used as the reference timestamp. If the first
timestamp argument occurs before the second, the resulting measurement will
be a negative value.
The type argument is an atom indicating the units the measurement should be
returned in. If no type argument is provided, a Duration will be returned.
Valid measurement units for this function are:
:microseconds, :milliseconds, :seconds, :minutes, :hours, :days, or :weeks
## Examples
iex> alias Timex.Duration
...> d = Duration.from_erl({1457, 136000, 785000})
...> Duration.diff(d, Duration.zero, :days)
16865
"""
def diff(t1, t2, type \\ nil)
def diff(%Duration{} = t1, nil, type), do: diff(t1, now(), type)
def diff(%Duration{} = t1, %Duration{} = t2, type) do
delta = do_diff(t1, t2)
case type do
nil -> delta
:microseconds -> to_microseconds(delta, truncate: true)
:milliseconds -> to_milliseconds(delta, truncate: true)
:seconds -> to_seconds(delta, truncate: true)
:minutes -> to_minutes(delta, truncate: true)
:hours -> to_hours(delta, truncate: true)
:days -> to_days(delta, truncate: true)
:weeks -> to_weeks(delta, truncate: true)
end
end
defp do_diff(%Duration{} = t1, %Duration{} = t2) do
microsecs = :timer.now_diff(to_erl(t1), to_erl(t2))
from_microseconds(microsecs)
end
@doc """
Evaluates fun() and measures the elapsed time.
Returns `{Duration.t, result}`.
## Example
iex> {_timestamp, result} = Duration.measure(fn -> 2 * 2 end)
...> result == 4
true
"""
@spec measure((() -> any)) :: {__MODULE__.t(), any}
def measure(fun) when is_function(fun) do
{time, result} = :timer.tc(fun, [])
{Duration.from_microseconds(time), result}
end
@doc """
Evaluates `apply(fun, args)`, and measures execution time.
Returns `{Duration.t, result}`.
## Example
iex> {_timestamp, result} = Duration.measure(fn x, y -> x * y end, [2, 4])
...> result == 8
true
"""
@spec measure(fun, [any]) :: {__MODULE__.t(), any}
def measure(fun, args) when is_function(fun) and is_list(args) do
{time, result} = :timer.tc(fun, args)
{Duration.from_microseconds(time), result}
end
@doc """
Evaluates `apply(module, fun, args)`, and measures execution time.
Returns `{Duration.t, result}`.
## Example
iex> {_timestamp, result} = Duration.measure(Enum, :map, [[1,2], &(&1*2)])
...> result == [2, 4]
true
"""
@spec measure(module, atom, [any]) :: {__MODULE__.t(), any}
def measure(module, fun, args)
when is_atom(module) and is_atom(fun) and is_list(args) do
{time, result} = :timer.tc(module, fun, args)
{Duration.from_microseconds(time), result}
end
def normalize(%Duration{megaseconds: mega, seconds: sec, microseconds: micro}) do
normalized = mega * 1_000_000_000_000 + sec * 1_000_000 + micro
mega = div(normalized, 1_000_000_000_000)
sec = div(rem(normalized, 1_000_000_000_000), 1_000_000)
micro = rem(normalized, 1_000_000)
%Duration{megaseconds: mega, seconds: sec, microseconds: micro}
end
defp do_round(value) when is_integer(value), do: value
defp do_round(value) when is_float(value), do: Float.round(value, 6)
end