defmodule Timex.Format.Duration.Formatters.Default do @moduledoc """ Handles formatting Duration values as ISO 8601 durations as described below. Durations are represented by the format P[n]Y[n]M[n]DT[n]H[n]M[n]S. In this representation, the [n] is replaced by the value for each of the date and time elements that follow the [n]. Leading zeros are not required, but the maximum number of digits for each element should be agreed to by the communicating parties. The capital letters P, Y, M, W, D, T, H, M, and S are designators for each of the date and time elements and are not replaced. - P is the duration designator (historically called "period") placed at the start of the duration representation. - Y is the year designator that follows the value for the number of years. - M is the month designator that follows the value for the number of months. - D is the day designator that follows the value for the number of days. - T is the time designator that precedes the time components of the representation. - H is the hour designator that follows the value for the number of hours. - M is the minute designator that follows the value for the number of minutes. - S is the second designator that follows the value for the number of seconds. """ use Timex.Format.Duration.Formatter alias Timex.Translator @minute 60 @hour @minute * 60 @day @hour * 24 @month @day * 30 @year @day * 365 @microsecond 1_000_000 @doc """ Return a human readable string representing the absolute value of duration (i.e. would return the same output for both negative and positive representations of a given duration) ## Examples iex> use Timex ...> Duration.from_erl({0, 1, 1_000_000}) |> #{__MODULE__}.format "PT2S" iex> use Timex ...> Duration.from_erl({0, 1, 1_000_100}) |> #{__MODULE__}.format "PT2.0001S" iex> use Timex ...> Duration.from_erl({0, 65, 0}) |> #{__MODULE__}.format "PT1M5S" iex> use Timex ...> Duration.from_erl({0, -65, 0}) |> #{__MODULE__}.format "PT1M5S" iex> use Timex ...> Duration.from_erl({1435, 180354, 590264}) |> #{__MODULE__}.format "P45Y6M5DT21H12M34.590264S" iex> use Timex ...> Duration.from_erl({0, 0, 0}) |> #{__MODULE__}.format "PT0S" """ @spec format(Duration.t()) :: String.t() | {:error, term} def format(%Duration{} = duration), do: lformat(duration, Translator.current_locale()) def format(_), do: {:error, :invalid_timestamp} def lformat(%Duration{} = duration, _locale) do duration |> deconstruct |> do_format end def lformat(_, _locale), do: {:error, :invalid_duration} defp do_format(components), do: do_format(components, <>) defp do_format([], "P"), do: "PT0S" defp do_format([], str), do: str defp do_format([{unit, _} = component | rest], str) do cond do unit in [:hours, :minutes, :seconds] && String.contains?(str, "T") -> do_format(rest, format_component(component, str)) unit in [:hours, :minutes, :seconds] -> do_format(rest, format_component(component, str <> "T")) true -> do_format(rest, format_component(component, str)) end end defp format_component({_, 0}, str), do: str defp format_component({:years, y}, str), do: str <> "#{y}Y" defp format_component({:months, m}, str), do: str <> "#{m}M" defp format_component({:days, d}, str), do: str <> "#{d}D" defp format_component({:hours, h}, str), do: str <> "#{h}H" defp format_component({:minutes, m}, str), do: str <> "#{m}M" defp format_component({:seconds, s}, str), do: str <> "#{s}S" defp deconstruct(duration) do micros = Duration.to_microseconds(duration) |> abs deconstruct({div(micros, @microsecond), rem(micros, @microsecond)}, []) end defp deconstruct({0, 0}, components), do: Enum.reverse(components) defp deconstruct({seconds, us}, components) do cond do seconds >= @year -> deconstruct({rem(seconds, @year), us}, [{:years, div(seconds, @year)} | components]) seconds >= @month -> deconstruct({rem(seconds, @month), us}, [{:months, div(seconds, @month)} | components]) seconds >= @day -> deconstruct({rem(seconds, @day), us}, [{:days, div(seconds, @day)} | components]) seconds >= @hour -> deconstruct({rem(seconds, @hour), us}, [{:hours, div(seconds, @hour)} | components]) seconds >= @minute -> deconstruct({rem(seconds, @minute), us}, [{:minutes, div(seconds, @minute)} | components]) true -> get_fractional_seconds(seconds, us, components) end end defp get_fractional_seconds(seconds, 0, components), do: deconstruct({0, 0}, [{:seconds, seconds} | components]) defp get_fractional_seconds(seconds, micro, components) do millis = micro |> Duration.from_microseconds() |> Duration.to_milliseconds() cond do millis >= 1.0 -> deconstruct({0, 0}, [{:seconds, seconds + millis * :math.pow(10, -3)} | components]) true -> deconstruct({0, 0}, [{:seconds, seconds + micro * :math.pow(10, -6)} | components]) end end end