Packages
ex_cldr_dates_times
2.0.0
2.25.6
2.25.5
2.25.4
2.25.3
2.25.2
2.25.1
2.25.0
2.24.2
2.24.1
2.24.0
2.23.0
2.22.0
2.21.0
2.20.3
2.20.2
2.20.0
2.19.2
2.19.1
2.19.0
retired
2.18.1
2.18.0
2.17.1
2.17.0
2.16.0
2.15.0
2.14.3
2.14.2
2.14.1
2.14.0
2.13.3
2.13.2
2.13.1
2.13.0
2.12.0
2.11.0
2.10.2
2.10.1
2.10.0
2.10.0-rc.3
2.10.0-rc.2
2.10.0-rc.1
2.10.0-rc.0
2.9.4
2.9.3
2.9.2
2.9.1
2.9.0
2.8.0
2.7.2
2.7.1
retired
2.7.0
2.7.0-rc.0
2.6.4
2.6.3
2.6.2
2.6.1
retired
2.6.0
2.6.0-rc.0
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.4.0
2.4.0-rc.0
2.3.0
2.2.4
2.2.3
2.2.2
2.2.1
2.2.0
2.1.0
2.0.2
2.0.1
2.0.0
1.4.0
1.3.1
1.3.0
1.2.1
1.2.0
1.0.1
1.0.0
1.0.0-rc.1
1.0.0-rc.0
retired
0.3.3
0.3.2
retired
0.3.1
retired
0.3.0
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Date, Time and DateTime localization, internationalization and formatting functions using the Common Locale Data Repository (CLDR).
Current section
Files
Jump to
Current section
Files
lib/cldr/datetime/datetime_relative.ex
defmodule Cldr.DateTime.Relative do
@moduledoc """
Functions to support the string formatting of relative time/datetime numbers.
This module provides formatting of numbers (as integers, floats, Dates or DateTimes)
as "ago" or "in" with an appropriate time unit. For example, "2 days ago" or
"in 10 seconds"
"""
@second 1
@minute 60
@hour 3600
@day 86400
@week 604_800
@month 2_629_743.83
@year 31_556_926
@unit %{
second: @second,
minute: @minute,
hour: @hour,
day: @day,
week: @week,
month: @month,
year: @year
}
@other_units [:mon, :tue, :wed, :thu, :fri, :sat, :sun, :quarter]
@unit_keys Map.keys(@unit) ++ @other_units
@doc """
Returns a `{:ok, string}` representing a relative time (ago, in) for a given
number, Date or Datetime. Returns `{:error, reason}` when errors are detected.
* `relative` is a number or Date/Datetime representing the time distance from `now` or from
options[:relative_to]
* `options` is a `Keyword` list of options which are:
## Options
* `:locale` is the locale in which the binary is formatted.
The default is `Cldr.get_locale/0`
* `:format` is the format of the binary. Format may be `:default`, `:narrow` or `:short`
* `:unit` is the time unit for the formatting. The allowable units are `:second`, `:minute`,
`:hour`, `:day`, `:week`, `:month`, `:year`, `:mon`, `:tue`, `:wed`, `:thu`, `:fri`, `:sat`,
`:sun`, `:quarter`
* `:relative_to` is the baseline Date or Datetime from which the difference from `relative` is
calculated when `relative` is a Date or a DateTime. The default for a Date is `Date.utc_today`,
for a DateTime it is `DateTime.utc_now`
### Notes
When `options[:unit]` is not specified, `Cldr.DateTime.Relative.to_string/2` attempts to identify
the appropriate unit based upon the magnitude of `relative`. For example, given a parameter
of less than `60`, then `to_string/2` will assume `:seconds` as the unit. See `unit_from_relative_time/1`.
## Examples
iex> Cldr.DateTime.Relative.to_string(-1, MyApp.Cldr)
{:ok, "1 second ago"}
iex> Cldr.DateTime.Relative.to_string(1, MyApp.Cldr)
{:ok, "in 1 second"}
iex> Cldr.DateTime.Relative.to_string(1, MyApp.Cldr, unit: :day)
{:ok, "tomorrow"}
iex> Cldr.DateTime.Relative.to_string(1, MyApp.Cldr, unit: :day, locale: "fr")
{:ok, "demain"}
iex> Cldr.DateTime.Relative.to_string(1, MyApp.Cldr, unit: :day, format: :narrow)
{:ok, "tomorrow"}
iex> Cldr.DateTime.Relative.to_string(1234, MyApp.Cldr, unit: :year)
{:ok, "in 1,234 years"}
iex> Cldr.DateTime.Relative.to_string(1234, MyApp.Cldr, unit: :year, locale: "fr")
{:ok, "dans 1 234 ans"}
iex> Cldr.DateTime.Relative.to_string(31, MyApp.Cldr)
{:ok, "in 31 seconds"}
iex> Cldr.DateTime.Relative.to_string(~D[2017-04-29], MyApp.Cldr, relative_to: ~D[2017-04-26])
{:ok, "in 3 days"}
iex> Cldr.DateTime.Relative.to_string(310, MyApp.Cldr, format: :short, locale: "fr")
{:ok, "dans 5Â min"}
iex> Cldr.DateTime.Relative.to_string(310, MyApp.Cldr, format: :narrow, locale: "fr")
{:ok, "+5 min"}
iex> Cldr.DateTime.Relative.to_string 2, MyApp.Cldr, unit: :wed, format: :short, locale: "en"
{:ok, "in 2 Wed."}
iex> Cldr.DateTime.Relative.to_string 1, MyApp.Cldr, unit: :wed, format: :short
{:ok, "next Wed."}
iex> Cldr.DateTime.Relative.to_string -1, MyApp.Cldr, unit: :wed, format: :short
{:ok, "last Wed."}
iex> Cldr.DateTime.Relative.to_string -1, MyApp.Cldr, unit: :wed
{:ok, "last Wednesday"}
iex> Cldr.DateTime.Relative.to_string -1, MyApp.Cldr, unit: :quarter
{:ok, "last quarter"}
iex> Cldr.DateTime.Relative.to_string -1, MyApp.Cldr, unit: :mon, locale: "fr"
{:ok, "lundi dernier"}
iex> Cldr.DateTime.Relative.to_string(~D[2017-04-29], MyApp.Cldr, unit: :ziggeraut)
{:error, {Cldr.UnknownTimeUnit,
"Unknown time unit :ziggeraut. Valid time units are [:day, :hour, :minute, :month, :second, :week, :year, :mon, :tue, :wed, :thu, :fri, :sat, :sun, :quarter]"}}
"""
@spec to_string(integer | float | Date.t() | DateTime.t(), Cldr.backend(), Keyword.t()) ::
{:ok, String.t()} | {:error, {atom, String.t()}}
def to_string(relative, backend \\ Cldr.default_backend(), options \\ []) do
options = Keyword.merge(default_options(), options)
locale = Keyword.get(options, :locale)
{unit, options} = Keyword.pop(options, :unit)
with {:ok, locale} <- Cldr.validate_locale(locale),
{:ok, unit} <- validate_unit(unit),
{relative, unit} = define_unit_and_relative_time(relative, unit, options[:relative_to]),
string <- to_string(relative, unit, locale, backend, options) do
{:ok, string}
else
{:error, reason} -> {:error, reason}
end
end
defp default_options do
[locale: Cldr.get_locale(), format: :default]
end
defp define_unit_and_relative_time(relative, nil, nil) when is_number(relative) do
unit = unit_from_relative_time(relative)
relative = scale_relative(relative, unit)
{relative, unit}
end
defp define_unit_and_relative_time(
%{year: _, month: _, day: _, hour: _, minute: _, second: _, calendar: Calendar.ISO} =
relative,
unit,
relative_to
) do
now = (relative_to || DateTime.utc_now()) |> DateTime.to_unix()
then = DateTime.to_unix(relative)
relative_time = then - now
define_unit_and_relative_time(relative_time, unit, nil)
end
defp define_unit_and_relative_time(
%{year: _, month: _, day: _, calendar: Calendar.ISO} = relative,
unit,
relative_to
) do
today =
(relative_to || Date.utc_today())
|> Date.to_erl()
|> :calendar.date_to_gregorian_days()
|> Kernel.*(@day)
then =
relative
|> Date.to_erl()
|> :calendar.date_to_gregorian_days()
|> Kernel.*(@day)
relative_time = then - today
define_unit_and_relative_time(relative_time, unit, nil)
end
defp define_unit_and_relative_time(relative_time, unit, _relative_to) do
{relative_time, unit}
end
@doc """
Returns a `{:ok, string}` representing a relative time (ago, in) for a given
number, Date or Datetime or raises an exception on error.
## Arguments
* `relative` is a number or Date/Datetime representing the time distance from `now` or from
options[:relative_to]
* `options` is a `Keyword` list of options
## Options
* `:locale` is the locale in which the binary is formatted.
The default is `Cldr.get_locale/0`
* `:format` is the format of the binary. Format may be `:default`, `:narrow` or `:short`
* `:unit` is the time unit for the formatting. The allowable units are `:second`, `:minute`,
`:hour`, `:day`, `:week`, `:month`, `:year`, `:mon`, `:tue`, `:wed`, `:thu`, `:fri`, `:sat`,
`:sun`, `:quarter`
* `:relative_to` is the baseline Date or Datetime from which the difference from `relative` is
calculated when `relative` is a Date or a DateTime. The default for a Date is `Date.utc_today`,
for a DateTime it is `DateTime.utc_now`
See `to_string/2`
"""
def to_string!(relative, options \\ []) do
case to_string(relative, options) do
{:ok, string} -> string
{:error, {exception, reason}} -> raise exception, reason
end
end
defp to_string(relative, unit, locale, backend, options)
when is_integer(relative) and relative in [-1, 0, +1] do
result =
locale
|> get_locale(backend)
|> get_in([unit, options[:format], :relative_ordinal])
|> Enum.at(relative + 1)
if is_nil(result), do: to_string(relative / 1, unit, locale, backend, options), else: result
end
defp to_string(relative, unit, locale, backend, options) when is_number(relative) do
direction = if relative > 0, do: :relative_future, else: :relative_past
rules =
locale
|> get_locale(backend)
|> get_in([unit, options[:format], direction])
rule = Module.concat(backend, Number.Cardinal).pluralize(trunc(relative), locale, rules)
relative
|> abs
|> Cldr.Number.to_string!(backend, locale: locale)
|> Cldr.Substitution.substitute(rule)
|> Enum.join()
end
defp to_string(span, unit, locale, backend, options) do
do_to_string(span, unit, locale, backend, options)
end
defp do_to_string(seconds, unit, locale, backend, options) do
seconds
|> scale_relative(unit)
|> to_string(unit, locale, backend, options)
end
defp time_unit_error(unit) do
{Cldr.UnknownTimeUnit,
"Unknown time unit #{inspect(unit)}. Valid time units are #{inspect(@unit_keys)}"}
end
@doc """
Returns an estimate of the appropriate time unit for an integer of a given
magnitude of seconds.
## Examples
iex> Cldr.DateTime.Relative.unit_from_relative_time(1234)
:minute
iex> Cldr.DateTime.Relative.unit_from_relative_time(12345)
:hour
iex> Cldr.DateTime.Relative.unit_from_relative_time(123456)
:day
iex> Cldr.DateTime.Relative.unit_from_relative_time(1234567)
:week
iex> Cldr.DateTime.Relative.unit_from_relative_time(12345678)
:month
iex> Cldr.DateTime.Relative.unit_from_relative_time(123456789)
:year
"""
def unit_from_relative_time(time) when is_number(time) do
case abs(time) do
i when i < @minute -> :second
i when i < @hour -> :minute
i when i < @day -> :hour
i when i < @week -> :day
i when i < @month -> :week
i when i < @year -> :month
_ -> :year
end
end
def unit_from_relative_time(time) do
time
end
@doc """
Calculates the time span in the given `unit` from the time given in seconds.
## Examples
iex> Cldr.DateTime.Relative.scale_relative(1234, :second)
1234
iex> Cldr.DateTime.Relative.scale_relative(1234, :minute)
21
iex> Cldr.DateTime.Relative.scale_relative(1234, :hour)
0
"""
def scale_relative(time, unit) when is_number(time) and is_atom(unit) do
(time / @unit[unit])
|> Float.round()
|> trunc
end
@doc """
Returns a list of the valid unit keys for `to_string/2`
## Example
iex> Cldr.DateTime.Relative.known_units
[:day, :hour, :minute, :month, :second, :week, :year, :mon, :tue, :wed, :thu,
:fri, :sat, :sun, :quarter]
"""
def known_units do
@unit_keys
end
def validate_unit(unit) when unit in @unit_keys or is_nil(unit) do
{:ok, unit}
end
def validate_unit(unit) do
{:error, time_unit_error(unit)}
end
defp get_locale(locale, backend) do
backend = Module.concat(backend, DateTime.Relative)
backend.get_locale(locale)
end
end