Current section

Files

Jump to
timex lib format datetime formatters relative.ex
Raw

lib/format/datetime/formatters/relative.ex

defmodule Timex.Format.DateTime.Formatters.Relative do
@moduledoc """
Relative time, based on Moment.js
Uses localized strings.
The format string should contain {relative}, which is where the phrase will be injected.
| Range | Sample Output
---------------------------------------------------------------------
| 0 seconds | now
| 1 to 45 seconds | a few seconds ago
| 45 to 90 seconds | a minute ago
| 90 seconds to 45 minutes | 2 minutes ago ... 45 minutes ago
| 45 to 90 minutes | an hour ago
| 90 minutes to 22 hours | 2 hours ago ... 22 hours ago
| 22 to 36 hours | a day ago
| 36 hours to 25 days | 2 days ago ... 25 days ago
| 25 to 45 days | a month ago
| 45 to 345 days | 2 months ago ... 11 months ago
| 345 to 545 days (1.5 years)| a year ago
| 546 days+ | 2 years ago ... 20 years ago
"""
use Timex.Format.DateTime.Formatter
use Combine
alias Timex.Format.FormatError
alias Timex.{Types, Translator}
@spec tokenize(String.t) :: {:ok, [Directive.t]} | {:error, term}
def tokenize(format_string) do
case Combine.parse(format_string, relative_parser()) do
results when is_list(results) ->
directives = results |> List.flatten |> Enum.filter(fn x -> x !== nil end)
case Enum.any?(directives, fn %Directive{type: type} -> type != :literal end) do
false -> {:error, "Invalid format string, must contain at least one directive."}
true -> {:ok, directives}
end
{:error, _} = err -> err
end
end
@doc """
Formats a date/time as a relative time formatted string
## Examples
iex> #{__MODULE__}.format(Timex.shift(Timex.now, minutes: -1), "{relative}")
{:ok, "1 minute ago"}
"""
@spec format(Types.calendar_types, String.t) :: {:ok, String.t} | {:error, term}
def format(date, format_string), do: lformat(date, format_string, Translator.default_locale)
@spec format!(Types.calendar_types, String.t) :: String.t | no_return
def format!(date, format_string), do: lformat!(date, format_string, Translator.default_locale)
@spec lformat(Types.calendar_types, String.t, String.t) :: {:ok, String.t} | {:error, term}
def lformat(date, format_string, locale) do
case tokenize(format_string) do
{:ok, []} ->
{:error, "There were no formatting directives in the provided string."}
{:ok, dirs} when is_list(dirs) ->
do_format(locale, Timex.to_naive_datetime(date), Timex.Protocol.NaiveDateTime.now(), dirs, <<>>)
{:error, reason} -> {:error, {:format, reason}}
end
end
@spec lformat!(Types.calendar_types, String.t, String.t) :: String.t | no_return
def lformat!(date, format_string, locale) do
case lformat(date, format_string, locale) do
{:ok, result} -> result
{:error, reason} -> raise FormatError, message: reason
end
end
def relative_to(date, relative_to, format_string) do
relative_to(date, relative_to, format_string, Translator.default_locale)
end
def relative_to(date, relative_to, format_string, locale) do
case tokenize(format_string) do
{:ok, []} ->
{:error, "There were no formatting directives in the provided string."}
{:ok, dirs} when is_list(dirs) ->
do_format(locale, Timex.to_naive_datetime(date), Timex.to_naive_datetime(relative_to), dirs, <<>>)
{:error, reason} -> {:error, {:format, reason}}
end
end
@minute 60
@hour @minute * 60
@day @hour * 24
@month @day * 30
@year @month * 12
defp do_format(_locale, _date, _relative, [], result), do: {:ok, result}
defp do_format(locale, date, relative, [%Directive{type: :literal, value: char} | dirs], result) when is_binary(char) do
do_format(locale, date, relative, dirs, <<result::binary, char::binary>>)
end
defp do_format(locale, date, relative, [%Directive{type: :relative} | dirs], result) do
diff = Timex.diff(date, relative, :seconds)
phrase = cond do
# future
diff == 0 ->
Translator.translate(locale, "relative_time", "now")
diff > 0 && diff <= 45 ->
Translator.translate_plural(locale, "relative_time", "in %{count} second", "in %{count} seconds", diff)
diff > 45 && diff < @minute * 2 ->
Translator.translate_plural(locale, "relative_time", "in %{count} minute", "in %{count} minutes", 1)
diff >= (@minute * 2) && diff < @hour ->
Translator.translate_plural(locale, "relative_time", "in %{count} minute", "in %{count} minutes", div(diff, @minute))
diff >= @hour && diff < (@hour * 2) ->
Translator.translate_plural(locale, "relative_time", "in %{count} hour", "in %{count} hours", 1)
diff >= (@hour * 2) && diff < @day ->
Translator.translate_plural(locale, "relative_time", "in %{count} hour", "in %{count} hours", div(diff, @hour))
diff >= @day && diff < (@day * 2) ->
Translator.translate_plural(locale, "relative_time", "in %{count} day", "in %{count} days", 1)
diff >= (@day * 2) && diff < @month ->
Translator.translate_plural(locale, "relative_time", "in %{count} day", "in %{count} days", div(diff, @day))
diff >= @month && diff < (@month * 2) ->
Translator.translate_plural(locale, "relative_time", "in %{count} month", "in %{count} months", 1)
diff >= (@month * 2) && diff < @year ->
Translator.translate_plural(locale, "relative_time", "in %{count} month", "in %{count} months", div(diff, @month))
diff >= @year && diff < (@year * 2) ->
Translator.translate_plural(locale, "relative_time", "in %{count} year", "in %{count} years", 1)
diff >= (@year * 2) ->
Translator.translate_plural(locale, "relative_time", "in %{count} year", "in %{count} years", div(diff, @year))
# past
diff < 0 && diff >= -45 ->
Translator.translate_plural(locale, "relative_time", "%{count} second ago", "%{count} seconds ago", diff * -1)
diff < -45 && diff > @minute * 2 * -1 ->
Translator.translate_plural(locale, "relative_time", "%{count} minute ago", "%{count} minutes ago", 1)
diff <= (@minute * 2) && diff > @hour * -1 ->
Translator.translate_plural(locale, "relative_time", "%{count} minute ago", "%{count} minutes ago", div(diff * -1, @minute))
diff <= @hour && diff > (@hour * 2 * -1) ->
Translator.translate_plural(locale, "relative_time", "%{count} hour ago", "%{count} hours ago", 1)
diff <= (@hour * 2) && diff > (@day * -1) ->
Translator.translate_plural(locale, "relative_time", "%{count} hour ago", "%{count} hours ago", div(diff * -1, @hour))
diff <= @day && diff > (@day * 2 * -1) ->
Translator.translate_plural(locale, "relative_time", "%{count} day ago", "%{count} days ago", 1)
diff <= (@day * 2) && diff > (@month * -1) ->
Translator.translate_plural(locale, "relative_time", "%{count} day ago", "%{count} days ago", div(diff * -1, @day))
diff <= @month && diff > (@month * 2 * -1) ->
Translator.translate_plural(locale, "relative_time", "%{count} month ago", "%{count} months ago", 1)
diff <= (@month * 2) && diff > (@year * -1) ->
Translator.translate_plural(locale, "relative_time", "%{count} month ago", "%{count} months ago", div(diff * -1, @month))
diff <= @year && diff > (@year * 2 * -1) ->
Translator.translate_plural(locale, "relative_time", "%{count} year ago", "%{count} years ago", 1)
diff <= (@year * 2 * -1) ->
Translator.translate_plural(locale, "relative_time", "%{count} year ago", "%{count} years ago", div(diff * -1, @year))
end
do_format(locale, date, relative, dirs, <<result::binary, phrase::binary>>)
end
defp do_format(locale, date, relative, [%Directive{type: type, modifiers: mods, flags: flags, width: width} | dirs], result) do
case format_token(locale, type, date, mods, flags, width) do
{:error, _} = err -> err
formatted -> do_format(locale, date, relative, dirs, <<result::binary, formatted::binary>>)
end
end
# Token parser
defp relative_parser do
many1(choice([
between(char(?{), map(one_of(word(), ["relative"]), &map_directive/1), char(?})),
map(none_of(char(), ["{", "}"]), &map_literal/1)
]))
end
# Gets/builds the Directives for a given token
defp map_directive("relative"),
do: %Directive{:type => :relative, :value => "relative"}
# Generates directives for literal characters
defp map_literal([]), do: nil
defp map_literal(literals)
when is_list(literals), do: Enum.map(literals, &map_literal/1)
defp map_literal(literal), do: %Directive{type: :literal, value: literal, parser: char(literal)}
end