defmodule Timex.Format.Duration.Formatters.Humanized do @moduledoc """ Handles formatting timestamp values as human readable strings. For formatting timestamps as points in time rather than intervals, use `Timex.format` """ use Timex.Format.Duration.Formatter alias Timex.Translator @minute 60 @hour @minute * 60 @day @hour * 24 @week @day * 7 @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 "2 seconds" iex> use Timex ...> Duration.from_erl({0, 1, 1_000_100}) |> #{__MODULE__}.format "2 seconds, 100 microseconds" iex> use Timex ...> Duration.from_erl({0, 65, 0}) |> #{__MODULE__}.format "1 minute, 5 seconds" iex> use Timex ...> Duration.from_erl({0, -65, 0}) |> #{__MODULE__}.format "1 minute, 5 seconds" iex> use Timex ...> Duration.from_erl({1435, 180354, 590264}) |> #{__MODULE__}.format "45 years, 6 months, 5 days, 21 hours, 12 minutes, 34 seconds, 590.264 milliseconds" """ @spec format(Duration.t()) :: String.t() | {:error, term} def format(%Duration{} = duration), do: lformat(duration, Translator.current_locale()) def format(_), do: {:error, :invalid_duration} @doc """ Return a human readable string representing the time interval, translated to the given locale ## Examples iex> use Timex ...> Duration.from_erl({0, 65, 0}) |> #{__MODULE__}.lformat("ru") "1 минута, 5 секунд" iex> use Timex ...> Duration.from_erl({1435, 180354, 590264}) |> #{__MODULE__}.lformat("ru") "45 лет, 6 месяцев, 5 дней, 21 час, 12 минут, 34 секунды, 590.264 миллисекунд" """ @spec lformat(Duration.t(), String.t()) :: String.t() | {:error, term} def lformat(%Duration{} = duration, locale) do duration |> deconstruct |> do_format(locale) end def lformat(_, _locale), do: {:error, :invalid_duration} defp do_format(components, locale), do: do_format(components, <<>>, locale) defp do_format([], str, _locale), do: str defp do_format([{unit, value} | rest], str, locale) do unit = Atom.to_string(unit) count = trunc(value) unit_with_value = Translator.translate_plural(locale, "units", "%{count} #{unit}", "%{count} #{unit}s", count) |> String.replace(to_string(count), to_string(value)) separator = Translator.translate(locale, "symbols", ",") case str do <<>> -> do_format(rest, "#{unit_with_value}", locale) _ -> do_format(rest, str <> "#{separator} #{unit_with_value}", locale) end end defp deconstruct(duration) do micros = Duration.to_microseconds(duration) |> abs deconstruct({div(micros, @microsecond), rem(micros, @microsecond)}, []) end defp deconstruct({0, 0}, []), do: deconstruct({0, 0}, microsecond: 0) defp deconstruct({0, 0}, components), do: Enum.reverse(components) defp deconstruct({seconds, us}, components) when seconds > 0 do cond do seconds >= @year -> deconstruct({rem(seconds, @year), us}, [{:year, div(seconds, @year)} | components]) seconds >= @month -> deconstruct({rem(seconds, @month), us}, [{:month, div(seconds, @month)} | components]) seconds >= @week -> deconstruct({rem(seconds, @week), us}, [{:week, div(seconds, @week)} | components]) seconds >= @day -> deconstruct({rem(seconds, @day), us}, [{:day, div(seconds, @day)} | components]) seconds >= @hour -> deconstruct({rem(seconds, @hour), us}, [{:hour, div(seconds, @hour)} | components]) seconds >= @minute -> deconstruct({rem(seconds, @minute), us}, [{:minute, div(seconds, @minute)} | components]) true -> deconstruct({0, us}, [{:second, seconds} | components]) end end defp deconstruct({0, micro}, components) do millis = micro |> Duration.from_microseconds() |> Duration.to_milliseconds() cond do millis >= 1 -> deconstruct({0, 0}, [{:millisecond, millis} | components]) true -> deconstruct({0, 0}, [{:microsecond, micro} | components]) end end end