Packages
ex_cldr_dates_times
2.19.2
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/time.ex
defmodule Cldr.Time do
@moduledoc """
Provides localization and formatting of a time.
A time is any `t:Time.t/0` struct or any map with one or more of
the keys `:hour`, `:minute`, `:second` and optionally `:time_zone`,
`:zone_abbr`, `:utc_offset`, `:std_offset` and `:microsecond`.
`Cldr.Time` provides support for the built-in calendar
`Calendar.ISO` or any calendars defined with
[ex_cldr_calendars](https://hex.pm/packages/ex_cldr_calendars)
CLDR provides standard format strings for `Time` which
are represented by the names `:short`, `:medium`, `:long`
and `:full`. This allows for locale-independent
formatting since each locale may define the underlying
format string as appropriate.
"""
alias Cldr.LanguageTag
alias Cldr.Locale
import Cldr.DateTime,
only: [resolve_plural_format: 4, apply_unicode_or_ascii_preference: 2]
@format_types [:short, :medium, :long, :full]
@default_format_type :medium
@default_prefer :unicode
@field_map %{
hour: "h",
minute: "m",
second: "s",
time_zone: "v",
zone_abbr: "V"
}
@field_names Map.keys(@field_map)
# TODO Do we need microseconds here too? Are there any standard formats that use it?
# have we got the formatting right for fractional seconds?
# have we got derived formats working for microseconds?
defguard is_full_time(time)
when is_map_key(time, :hour) and is_map_key(time, :minute) and is_map_key(time, :second)
defmodule Formats do
@moduledoc false
defstruct Module.get_attribute(Cldr.Time, :format_types)
end
@doc """
Formats a time according to a format string
as defined in CLDR and described in [TR35](http://unicode.org/reports/tr35/tr35-dates.html).
### Returns
* `{:ok, formatted_time}` or
* `{:error, reason}`.
### Arguments
* `time` is a `t:Time.t/0` struct or any map that contains
one or more of the keys `:hour`, `:minute`, `:second` and optionally `:microsecond`,
`:time_zone`, `:zone_abbr`, `:utc_offset` and `:std_offset`.
* `backend` is any module that includes `use Cldr` and therefore
is a `Cldr` backend module. The default is `Cldr.default_backend!/0`.
* `options` is a keyword list of options for formatting.
### Options
* `:format` is one of `:short`, `:medium`, `:long`, `:full`, or a format id
or a format string. The default is `:medium` for full times (that is,
times having `:hour`, `:minute` and `:second` fields). The
default for partial times is to derive a candidate format from the time and
find the best match from the formats returned by
`Cldr.Time.available_formats/3`. See [here](README.md#date-time-and-datetime-localization-formats)
for more information about specifying formats.
* `:locale` any locale returned by `Cldr.known_locale_names/1`. The default is
`Cldr.get_locale/0`.
* `:number_system` a number system into which the formatted date digits should
be transliterated.
* `:prefer` is either `:unicode` (the default) or `:ascii`. A small number of
formats have two variants - one using Unicode spaces (typically non-breaking space) and
another using only ASCII whitespace. The `:ascii` format is primarily to support legacy
use cases and is not recommended. See `Cldr.Time.available_formats/3`
to see which formats have these variants.
* `period: :variant` will use a variant for the time period and flexible time period if
one is available in the locale. For example, in the `:en` locale, `period: :variant` will
return "pm" instead of "PM".
### Examples
iex> Cldr.Time.to_string(~T[07:35:13.215217], MyApp.Cldr)
{:ok, "7:35:13 AM"}
iex> Cldr.Time.to_string(~T[07:35:13.215217], MyApp.Cldr, format: :short)
{:ok, "7:35 AM"}
iex> Cldr.Time.to_string(~T[07:35:13.215217], MyApp.Cldr, format: :short, period: :variant)
{:ok, "7:35 am"}
iex> Cldr.Time.to_string(~T[07:35:13.215217], MyApp.Cldr, format: :medium, locale: "fr")
{:ok, "07:35:13"}
iex> Cldr.Time.to_string(~T[07:35:13.215217], MyApp.Cldr, format: :medium)
{:ok, "7:35:13 AM"}
iex> {:ok, datetime} = DateTime.from_naive(~N[2000-01-01 23:59:59.0], "Etc/UTC")
iex> Cldr.Time.to_string datetime, MyApp.Cldr, format: :long
{:ok, "11:59:59 PM UTC"}
# A partial time with a best match CLDR-defined format
iex> Cldr.Time.to_string(%{hour: 23, minute: 11})
{:ok, "11:11 PM"}
# Sometimes the available time fields can't be mapped to an available
# CLDR-defined format.
iex> Cldr.Time.to_string(%{minute: 11})
{:error,
{Cldr.DateTime.UnresolvedFormat, "No available format resolved for :m"}}
"""
@spec to_string(map, Cldr.backend() | Keyword.t(), Keyword.t()) ::
{:ok, String.t()} | {:error, {module, String.t()}}
def to_string(time, backend \\ Cldr.Date.default_backend(), options \\ [])
def to_string(%{calendar: Calendar.ISO} = time, backend, options) do
%{time | calendar: Cldr.Calendar.Gregorian}
|> to_string(backend, options)
end
def to_string(time, options, []) when is_list(options) do
{locale, backend} = Cldr.locale_and_backend_from(options)
options = Keyword.put_new(options, :locale, locale)
to_string(time, backend, options)
end
def to_string(%{} = time, backend, options) do
options = normalize_options(time, backend, options)
format_backend = Module.concat(backend, DateTime.Formatter)
calendar = Map.get(time, :calendar, Cldr.Calendar.Gregorian)
time = Map.put_new(time, :calendar, calendar)
number_system = Map.get(options, :number_system)
locale = options.locale
format = options.format
with {:ok, locale} <- Cldr.validate_locale(locale, backend),
{:ok, cldr_calendar} <- Cldr.DateTime.type_from_calendar(calendar),
{:ok, _} <- Cldr.Number.validate_number_system(locale, number_system, backend),
{:ok, format} <- find_format(time, format, locale, cldr_calendar, backend),
{:ok, format} <- apply_unicode_or_ascii_preference(format, options.prefer),
{:ok, format_string} <- resolve_plural_format(format, time, backend, options) do
format_backend.format(time, format_string, locale, options)
end
rescue
e in [Cldr.DateTime.FormatError] ->
{:error, {e.__struct__, e.message}}
end
def to_string(time, _backend, _options) do
error_return(time, [:hour, :minute, :second])
end
@doc """
Formats a time according to a format string
as defined in CLDR and described in [TR35](http://unicode.org/reports/tr35/tr35-dates.html).
### Arguments
* `time` is a `t:Time.t/0` struct or any map that contains
one or more of the keys `:hour`, `:minute`, `:second` and optionally `:microsecond`,
`:time_zone`, `:zone_abbr`, `:utc_offset` and `:std_offset`.
* `backend` is any module that includes `use Cldr` and therefore
is a `Cldr` backend module. The default is `Cldr.default_backend!/0`.
* `options` is a keyword list of options for formatting.
### Options
* `:format` is one of `:short`, `:medium`, `:long`, `:full`, or a format id
or a format string. The default is `:medium` for full times (that is,
times having `:hour`, `:minute` and `:second` fields). The
default for partial times is to derive a candidate format from the time and
find the best match from the formats returned by
`Cldr.Time.available_formats/3`. See [here](README.md#date-time-and-datetime-localization-formats)
for more information about specifying formats.
* `locale` is any valid locale name returned by `Cldr.known_locale_names/0`
or a `Cldr.LanguageTag` struct. The default is `Cldr.get_locale/0`.
* `:number_system` a number system into which the formatted time digits should
be transliterated.
* `:prefer` is either `:unicode` (the default) or `:ascii`. A small number of
formats have two variants - one using Unicode spaces (typically non-breaking space) and
another using only ASCII whitespace. The `:ascii` format is primarily to support legacy
use cases and is not recommended. See `Cldr.Time.available_formats/3`
to see which formats have these variants.
* `period: :variant` will use a variant for the time period and flexible time period if
one is available in the locale. For example, in the `:en` locale `period: :variant` will
return "pm" instead of "PM"
### Returns
* `formatted_time_string` or
* raises an exception.
### Examples
iex> Cldr.Time.to_string!(~T[07:35:13.215217], MyApp.Cldr)
"7:35:13 AM"
iex> Cldr.Time.to_string!(~T[07:35:13.215217], MyApp.Cldr, format: :short)
"7:35 AM"
iex> Cldr.Time.to_string!(~T[07:35:13.215217], MyApp.Cldr, format: :short, period: :variant)
"7:35 am"
iex> Cldr.Time.to_string!(~T[07:35:13.215217], MyApp.Cldr, format: :medium, locale: "fr")
"07:35:13"
iex> Cldr.Time.to_string!(~T[07:35:13.215217], MyApp.Cldr, format: :medium)
"7:35:13 AM"
iex> {:ok, datetime} = DateTime.from_naive(~N[2000-01-01 23:59:59.0], "Etc/UTC")
iex> Cldr.Time.to_string!(datetime, MyApp.Cldr, format: :long)
"11:59:59 PM UTC"
# A partial time with a best match CLDR-defined format
iex> Cldr.Time.to_string!(%{hour: 23, minute: 11})
"11:11 PM"
"""
@spec to_string!(map, Cldr.backend() | Keyword.t(), Keyword.t()) :: String.t() | no_return
def to_string!(time, backend \\ Cldr.Date.default_backend(), options \\ [])
def to_string!(time, backend, options) do
case to_string(time, backend, options) do
{:ok, string} -> string
{:error, {exception, message}} -> raise exception, message
end
end
# TODO deprecate :style in version 3.0
defp normalize_options(_time, _backend, %{} = options) do
options
end
defp normalize_options(time, backend, []) do
{locale, _backend} = Cldr.locale_and_backend_from(nil, backend)
number_system = Cldr.Number.System.number_system_from_locale(locale, backend)
format = format_from_options(time, nil, @default_format_type, @default_prefer)
%{locale: locale, number_system: number_system, format: format, prefer: @default_prefer}
end
defp normalize_options(time, backend, options) do
{locale, _backend} = Cldr.locale_and_backend_from(options[:locale], backend)
locale_number_system = Cldr.Number.System.number_system_from_locale(locale, backend)
number_system = Keyword.get(options, :number_system, locale_number_system)
prefer = Keyword.get(options, :prefer, @default_prefer)
format_option = options[:time_format] || options[:format] || options[:style]
format = format_from_options(time, format_option, @default_format_type, prefer)
options
|> Keyword.put(:locale, locale)
|> Keyword.put(:format, format)
|> Keyword.put(:prefer, prefer)
|> Keyword.delete(:style)
|> Keyword.put_new(:number_system, number_system)
|> Map.new()
end
# Full date, no option, use the default format
defp format_from_options(time, nil, default_format, _prefer) when is_full_time(time) do
default_format
end
# Partial date, no option, derive the format from the date
defp format_from_options(time, nil, _default_format, _prefer) do
derive_format_id(time)
end
# If a format is requested, use it
defp format_from_options(_time, format, _default_format, prefer) do
{:ok, format} = apply_unicode_or_ascii_preference(format, prefer)
format
end
@doc false
def derive_format_id(time) do
Cldr.DateTime.derive_format_id(time, @field_map, @field_names)
end
# If its a full time we can use one of the standard formats (:short, :medium, :long)
# and if its a full date and no format is specified then the default :medium will be
# applied.
@doc false
def find_format(time, format, locale, calendar, backend)
when format in @format_types and is_full_time(time) do
%LanguageTag{cldr_locale_name: locale_name} = locale
with {:ok, time_formats} <- formats(locale_name, calendar, backend) do
{:ok, Map.fetch!(time_formats, format)}
end
end
# If its a partial date and a standard format is requested, its an error
def find_format(time, format, _locale, _calendar, _backend)
when format in @format_types and not is_full_time(time) do
{:error,
{
Cldr.DateTime.UnresolvedFormat,
"Standard formats are not accepted for partial times"
}}
end
def find_format(time, %{format: format} = format_map, locale, calendar, backend) do
%{number_system: number_system} = format_map
{:ok, format_string} = find_format(time, format, locale, calendar, backend)
{:ok, %{number_system: number_system, format: format_string}}
end
# If its an atom format it means we want to use one of the available formats. Since
# these are map keys they can be used in a locale-independent way. If the requested
# format is a direct match, use it. If not - try to find the best match between the
# requested format and available formats.
def find_format(_time, format, locale, calendar, backend) when is_atom(format) do
{:ok, available_formats} = available_formats(locale, calendar, backend)
if Map.has_key?(available_formats, format) do
Map.fetch(available_formats, format)
else
resolve_format(format, available_formats, locale, calendar, backend)
end
end
# If its a binary then its considered a format string so we use
# it directly.
def find_format(_time, format_string, _locale, _calendar, _backend)
when is_binary(format_string) do
{:ok, format_string}
end
@doc false
defdelegate resolve_format(format, available_formats, locale, calendar, backend), to: Cldr.Date
@doc """
Returns a map of the standard time formats for a given
locale and calendar.
### Arguments
* `locale` is any locale returned by `Cldr.known_locale_names/0`
or a `t:Cldr.LanguageTag.t/0`. The default is `Cldr.get_locale/0`.
* `calendar` is any calendar returned by `Cldr.DateTime.Format.calendars_for/1`
The default is `:gregorian`.
* `backend` is any module that includes `use Cldr` and therefore
is a `Cldr` backend module. The default is `Cldr.default_backend/0`.
### Examples:
iex> Cldr.Time.formats(:en, :gregorian, MyApp.Cldr)
{:ok,
%Cldr.Time.Formats{
short: "h:mm a",
medium: "h:mm:ss a",
long: "h:mm:ss a z",
full: "h:mm:ss a zzzz"
}}
iex> Cldr.Time.formats(:en, :buddhist, MyApp.Cldr)
{:ok,
%Cldr.Time.Formats{
short: "h:mm a",
medium: "h:mm:ss a",
long: "h:mm:ss a z",
full: "h:mm:ss a zzzz"
}}
"""
@spec formats(
Locale.locale_reference(),
Cldr.Calendar.calendar(),
Cldr.backend()
) ::
{:ok, Cldr.DateTime.Format.standard_formats()} | {:error, {atom, String.t()}}
def formats(
locale \\ Cldr.get_locale(),
calendar \\ Cldr.Calendar.default_cldr_calendar(),
backend \\ Cldr.Date.default_backend()
) do
Cldr.DateTime.Format.time_formats(locale, calendar, backend)
end
@doc """
Returns a map of the available date formats for a
given locale and calendar.
### Arguments
* `locale` is any locale returned by `Cldr.known_locale_names/0`
or a `t:Cldr.LanguageTag.t/0`. The default is `Cldr.get_locale/0`.
* `calendar` is any calendar returned by `Cldr.DateTime.Format.calendars_for/1`
The default is `:gregorian`.
* `backend` is any module that includes `use Cldr` and therefore
is a `Cldr` backend module. The default is `Cldr.default_backend/0`.
### Examples:
iex> Cldr.Time.available_formats(:en)
{:ok,
%{
h: %{unicode: "h a", ascii: "h a"},
hms: %{unicode: "h:mm:ss a", ascii: "h:mm:ss a"},
ms: "mm:ss",
H: "HH",
Hm: "HH:mm",
Hms: "HH:mm:ss",
Hmsv: "HH:mm:ss v",
Hmv: "HH:mm v",
hm: %{unicode: "h:mm a", ascii: "h:mm a"},
hmsv: %{unicode: "h:mm:ss a v", ascii: "h:mm:ss a v"},
hmv: %{unicode: "h:mm a v", ascii: "h:mm a v"}
}}
"""
@spec available_formats(
Locale.locale_reference(),
Cldr.Calendar.calendar(),
Cldr.backend()
) :: {:ok, map()} | {:error, {atom, String.t()}}
def available_formats(
locale \\ Cldr.get_locale(),
calendar \\ Cldr.Calendar.default_cldr_calendar(),
backend \\ Cldr.Date.default_backend()
) do
backend = Module.concat(backend, DateTime.Format)
backend.time_available_formats(locale, calendar)
end
@doc """
Return the preferred time format for a locale.
### Arguments
* `language_tag` is any language tag returned by `Cldr.Locale.new/2`
or any `locale_name` returned by `Cldr.known_locale_names/1`
### Returns
* The hour format as an atom to be used for localization purposes. The
return value is used as a function name in `Cldr.DateTime.Formatter`
### Notes
* The `hc` key of the `u` extension is honoured and will
override the default preferences for a locale or territory.
See the last example below.
* Different locales and territories present the hour
of day in different ways. These are represented
in `Cldr.DateTime.Formatter` in the following way:
| Symbol | Midn. | Morning | Noon | Afternoon | Midn. |
| :----: | :---: | :-----: | :--: | :--------: | :---: |
| h | 12 | 1...11 | 12 | 1...11 | 12 |
| K | 0 | 1...11 | 0 | 1...11 | 0 |
| H | 0 | 1...11 | 12 | 13...23 | 0 |
| k | 24 | 1...11 | 12 | 13...23 | 24 |
### Examples
iex> Cldr.Time.hour_format_from_locale("en-AU")
:h12
iex> Cldr.Time.hour_format_from_locale("fr")
:h23
iex> Cldr.Time.hour_format_from_locale("fr-u-hc-h12")
:h12
"""
def hour_format_from_locale(%LanguageTag{locale: %{hc: hour_cycle}})
when not is_nil(hour_cycle) do
hour_cycle
end
def hour_format_from_locale(%LanguageTag{} = locale) do
preferences = time_preferences()
territory = Cldr.Locale.territory_from_locale(locale)
preference =
preferences[locale.cldr_locale_name] ||
preferences[territory] ||
preferences[Cldr.the_world()]
Map.fetch!(time_symbols(), preference.preferred)
end
def hour_format_from_locale(locale_name, backend \\ Cldr.Date.default_backend()) do
with {:ok, locale} <- Cldr.validate_locale(locale_name, backend) do
hour_format_from_locale(locale)
end
end
@doc false
@time_preferences Cldr.Config.time_preferences()
def time_preferences do
@time_preferences
end
# | Symbol | Midn. | Morning | Noon | Afternoon | Midn. | Code
# | :----: | :---: | :-----: | :--: | :--------: | :---: | :--:
# | h | 12 | 1...11 | 12 | 1...11 | 12 | :h12
# | K | 0 | 1...11 | 0 | 1...11 | 0 | :h11
# | H | 0 | 1...11 | 12 | 13...23 | 0 | :h23
# | k | 24 | 1...11 | 12 | 13...23 | 24 | :h24
#
defp time_symbols do
%{
# :hour_1_12,
"h" => :h12,
# :hour_0_11,
"K" => :h11,
# :hour_0_23,
"H" => :h23,
# :hour_1_24.
"k" => :h24
}
end
defp error_return(map, requirements) do
requirements =
requirements
|> Enum.map(&inspect/1)
|> Cldr.DateTime.Formatter.join_requirements()
{:error,
{ArgumentError,
"Invalid time. Time is a map that contains at least #{requirements} fields. " <>
"Found: #{inspect(map)}"}}
end
end