lib/duration.ex

defmodule Moar.Duration do
  # @related [test](/test/duration_test.exs)

  @moduledoc """
  A duration is a `{time, unit}` tuple.

  The time is a number and the unit is one of:
  `:nanosecond`, `:microsecond`, `:millisecond`, `:second`, `:minute`, `:hour`, `:day`.
  """

  @seconds_per_minute 60
  @seconds_per_hour 60 * 60
  @seconds_per_day 60 * 60 * 24

  @type t() :: {time :: number(), unit :: time_unit()}
  @type time_unit() :: :nanosecond | :microsecond | :millisecond | :second | :minute | :hour | :day

  @doc """
  Converts a `{duration, time_unit}` tuple into a numeric duration, rounding down to the nearest whole number.

  Uses `System.convert_time_unit/3` under the hood; see its documentation for more details.

  ```elixir
  iex> Moar.Duration.convert({121, :second}, :minute)
  2
  ```
  """
  @spec convert(from :: t(), to :: time_unit()) :: number()
  def convert({time, :minute}, to_unit), do: convert({time * @seconds_per_minute, :second}, to_unit)
  def convert({time, :hour}, to_unit), do: convert({time * @seconds_per_hour, :second}, to_unit)
  def convert({time, :day}, to_unit), do: convert({time * @seconds_per_day, :second}, to_unit)
  def convert(duration, :minute), do: convert(duration, :second) |> Integer.floor_div(@seconds_per_minute)
  def convert(duration, :hour), do: convert(duration, :second) |> Integer.floor_div(@seconds_per_hour)
  def convert(duration, :day), do: convert(duration, :second) |> Integer.floor_div(@seconds_per_day)
  def convert({time, from_unit}, to_unit), do: System.convert_time_unit(time, from_unit, to_unit)

  @doc """
  Converts a `{duration, time_unit}` tuple into a string.

  ```elixir
  iex> Moar.Duration.to_string({1, :second})
  "1 second"

  iex> Moar.Duration.to_string({25, :millisecond})
  "25 milliseconds"
  ```
  """
  @spec to_string({duration :: t(), unit :: time_unit()}) :: String.t()
  def to_string({1, unit}), do: "1 #{unit}"
  def to_string({-1, unit}), do: "-1 #{unit}"
  def to_string({time, unit}), do: "#{time} #{unit}s"
end