Packages
ex_cldr_dates_times
2.25.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/date_time.ex
defmodule Cldr.DateTime do
@moduledoc """
Provides localized formatting of full or partial date_times.
A date_time is any `t:DateTime.t/0` or `t:NaiveDateTime.t/0`
struct or any map that contains one or more of the keys `:year`, `:month`, `:day`,
`:hour`, `:minute` and `:second` or `:microsecond` with optional `:time_zone`, `:zone_abbr`,
`:utc_offset`, `:std_offset` and `:calendar` fields.
`Cldr.DateTime` provides support for the built-in calendar
`Calendar.ISO` or any calendars defined with
[ex_cldr_calendars](https://hex.pm/packages/ex_cldr_calendars)
For information about specifying formats, see `Cldr.DateTime.Format`.
"""
alias Cldr.DateTime.Format.Compiler
alias Cldr.DateTime.Format.Match
alias Cldr.DateTime.Format
alias Cldr.Locale
@typep options :: Keyword.t() | map()
@standard_formats Format.standard_formats()
@default_standard_format :medium
@default_style :default
@default_prefer :unicode
@default_separators :standard
@doc """
Indicates if a given map fulfills the requirements
for a naive date time.
"""
defguard is_naive_date_time(date_time)
when is_map_key(date_time, :year) and
is_map_key(date_time, :month) and
is_map_key(date_time, :day) and
is_map_key(date_time, :hour) and
is_map_key(date_time, :minute) and
is_map_key(date_time, :second)
@doc """
Indicates if a given map fulfills the requirements
for a date time.
"""
defguard is_date_time(date_time)
when is_naive_date_time(date_time) and
is_map_key(date_time, :time_zone) and
is_map_key(date_time, :zone_abbr)
@doc """
Indicates if a given map fulfills the requirements
for a naive date time or date time.
"""
defguard is_any_date_time(date_time)
when is_date_time(date_time) or is_naive_date_time(date_time)
@doc """
Guards whether the given date_time has components of
a date.
"""
defguard has_date(date_time)
when is_map_key(date_time, :year) or is_map_key(date_time, :month) or
is_map_key(date_time, :day)
@doc """
Guards whether the given date_time has components of
a time.
"""
defguard has_time(date_time)
when is_map_key(date_time, :hour) or is_map_key(date_time, :minute) or
is_map_key(date_time, :second)
@doc """
Guard whether the given date_time has components of
both a date and a time.
"""
defguard has_date_and_time(date_time)
when has_date(date_time) and has_time(date_time)
@doc """
Guard whether a format is a format skeleton
"""
defguard is_skeleton(format) when is_atom(format) and not is_nil(format)
defmodule Formats do
@moduledoc false
defstruct Module.get_attribute(Cldr.DateTime, :standard_formats)
end
@doc """
Formats a `t:DateTime.t/0` or `t:NaiveDateTime.t/0` according to the formats
defined in CLDR and described in [TR35](http://unicode.org/reports/tr35/tr35-dates.html).
### Arguments
* `date_time` is a `t:DateTime.t/0` or `t:NaiveDateTime.t/0` struct or any map that contains
one or more of the keys `:year`, `:month`, `:day`, `:hour`, `:minute` and `:second` or
`:microsecond` with optional `:time_zone`, `:zone_abbr`, `:utc_offset`, `:std_offset`
and `:calendar` fields.
* `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 either a [standard format](Cldr.DateTime.Format.html#module-standard-formats)
(one of `:short`, `:medium`, `:long`, `:full`), a [format skeleton](Cldr.DateTime.Format.html#module-format-skeletons)
or a [format pattern](Cldr.DateTime.Format.html#module-format-patterns).
* The default is `:medium` for full *date_times* (that is, *dates_times* having `:year`,
`:month`, `:day`, `:hour`, `:minutes`, `:second` and `:calendar` fields).
* The default for partial *date_times* is to derive a format skeleton from the
*date_time* and find the best match from the formats returned by
`Cldr.DateTime.available_formats/3`.
* See `Cldr.DateTime.Format` for more information about specifying formats.
* `:date_format` is used to format the *date* part of a *date_time* and is either a standard
format or a format skeleton.
* If `:date_format` is not specified then the *date* format is defined by the `:format`
option.
* If `:date_format` is a format skeleton it may only include format fields
appropriate for a *date*.
* :date_format` may only be specified if `:format` is a standard format.
* `:time_format` is used to format the *time* part of a *date_time* and is either a standard
format or a format skeleton.
* If `:time_format` is not specified then the *time* format is defined by the `:format`
option.
* If `:time_format` is a format skeleton it may only include format fields
appropriate for a *time*.
* :time_format` may only be specified if `:format` is a standard format.
* `:style` is either `:at` or `:default`. When set to `:at` the *date_time* may
be formatted with a localised string representing `<date> at <time>` if such
a format exists. See `Cldr.DateTime.Format.date_time_at_formats/2`.
* `: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.DateTime.available_formats/3`
to see which formats have these variants. See [Variant Preference](#variant-preference)
below for more information.
* `:locale` is any valid locale name returned by `Cldr.known_locale_names/0`
or a `t:Cldr.LanguageTag.t/0` struct. The default is `Cldr.get_locale/0`.
* `:number_system` a number system into which the formatted *date_time* digits should
be transliterated. See `Cldr.known_number_systems/0`. The default is
the number system associated with the `:locale`.
* `:separators` selects which of the available symbol
sets should be used when formatting fractional seconds (format
character `S`). The default is `:standard`. Some limited locales have an alternative `:us`
variant that can be used. See `Cldr.Number.Symbol.number_symbols_for/3`
for the symbols supported for a given locale and number system.
* `:era` which, if set to `:variant`, will use a variant for the era if one
is available in the requested locale. In the `:en` locale, for example, `era: :variant`
will return `CE` instead of `AD` and `BCE` instead of `BC`.
* `:period` which, if set to `: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".
### Variant Preference
* A small number of formats have one of two different alternatives, each with their own
preference specifier. The preferences are specified with the `:prefer` option to
`Cldr.Date.to_string/3`. The preference is expressed as an atom, or a list of one or two
atoms with one atom being either `:unicode` or `:ascii` and one atom being either
`:default` or `:variant`.
* Some formats (at the time of publishng only *time* formats but that
may change in the future) have `:unicode` and `:ascii` versions of the format. The
difference is the use of ascii space (0x20) as a separateor in the `:ascii` verison
whereas the `:unicode` version may use non-breaking or other space characters. The
default is `:unicode` and this is the strongly preferred option. 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.
* Some formats (at the time of publishing, only *date* and *date_time* formats) have
`:default` and `:variant` versions of the format. These variant formats are only
included in a small number of locales. For example, the `:"en-CA"` locale, which has
a `:default` format respecting typical Canadian formatting and a `:variant` that is
more closely aligned to US formatting. The default is `:default`.
### Notes
* If the provided `date_time` contains only *date* fields, the call is delegated to
`Cldr.Date.to_string/2`.
* If the provided `date_time` contains only *time* fields, the call is delegated to
`Cldr.Time.to_string/2`.
### Returns
* `{:ok, formatted_date_time}` or
* `{:error, reason}`
### Examples
iex> {:ok, date_time} = DateTime.from_naive(~N[2000-01-01 23:59:59.0], "Etc/UTC")
iex> Cldr.DateTime.to_string(date_time)
{:ok, "Jan 1, 2000, 11:59:59 PM"}
iex> Cldr.DateTime.to_string(date_time, MyApp.Cldr, locale: :en)
{:ok, "Jan 1, 2000, 11:59:59 PM"}
iex> Cldr.DateTime.to_string(date_time, MyApp.Cldr, format: :long, locale: :en)
{:ok, "January 1, 2000, 11:59:59 PM UTC"}
iex> Cldr.DateTime.to_string(date_time, MyApp.Cldr, format: :hms, locale: :en)
{:ok, "11:59:59 PM"}
iex> Cldr.DateTime.to_string(date_time, MyApp.Cldr, format: :full, locale: :en)
{:ok, "Saturday, January 1, 2000, 11:59:59 PM Coordinated Universal Time"}
iex> Cldr.DateTime.to_string(date_time, MyApp.Cldr, format: :full, locale: :fr)
{:ok, "samedi 1 janvier 2000, 23:59:59 temps universel coordonné"}
iex> Cldr.DateTime.to_string(date_time, MyApp.Cldr, format: :full, style: :at, locale: :en)
{:ok, "Saturday, January 1, 2000 at 11:59:59 PM Coordinated Universal Time"}
iex> Cldr.DateTime.to_string(date_time, MyApp.Cldr, format: :full, style: :at, locale: :fr)
{:ok, "samedi 1 janvier 2000 à 23:59:59 temps universel coordonné"}
iex> Cldr.DateTime.to_string(date_time, MyApp.Cldr, format: :MMMMW, locale: :fr)
{:ok, "semaine 1 (janvier)"}
iex> Cldr.DateTime.to_string(date_time, MyApp.Cldr, format: :yw, locale: :fr)
{:ok, "semaine 1 de 2000"}
iex> Cldr.DateTime.to_string(date_time, MyApp.Cldr, format: :full, date_format: :yMd, time_format: :hms)
{:ok, "1/1/2000, 11:59:59 PM"}
"""
@spec to_string(Cldr.Calendar.any_date_time(), Cldr.backend(), options()) ::
{:ok, String.t()} | {:error, {module, String.t()}}
@spec to_string(Cldr.Calendar.any_date_time(), options(), []) ::
{:ok, String.t()} | {:error, {module, String.t()}}
def to_string(date_time, backend \\ Cldr.Date.default_backend(), options \\ [])
def to_string(%{calendar: Calendar.ISO} = date_time, backend, options) do
%{date_time | calendar: Cldr.Calendar.Gregorian}
|> to_string(backend, options)
end
def to_string(date_time, options, []) when is_list(options) do
{locale, backend} = Cldr.locale_and_backend_from(options)
options = Keyword.put_new(options, :locale, locale)
to_string(date_time, backend, options)
end
def to_string(%{} = date_time, backend, options)
when is_atom(backend) and has_date_and_time(date_time) do
format_backend = Module.concat(backend, DateTime.Formatter)
with {:ok, date_time, options} <- normalize_options(date_time, backend, options),
{:ok, locale} <- Cldr.validate_locale(options.locale, backend),
{:ok, cldr_calendar} <- Cldr.DateTime.type_from_calendar(date_time.calendar),
{:ok, _} <- Cldr.Number.validate_number_system(locale, options.number_system, backend),
{:ok, format, options} <-
find_format(date_time, options.format, locale, cldr_calendar, backend, options),
{:ok, format} <- apply_preference(format, options.prefer),
{:ok, format_string} <- resolve_plural_format(format, date_time, backend, options) do
format_backend.format(date_time, format_string, locale, options)
end
rescue
e in [Cldr.DateTime.FormatError] ->
{:error, {e.__struct__, e.message}}
end
def to_string(%{} = date_time, backend, options)
when is_atom(backend) and has_date(date_time) do
Cldr.Date.to_string(date_time, backend, options)
end
def to_string(%{} = date_time, backend, options)
when is_atom(backend) and has_time(date_time) do
Cldr.Time.to_string(date_time, backend, options)
end
def to_string(date_time, value, []) when is_map(date_time) do
{:error,
{ArgumentError, "Unexpected option value #{inspect(value)}. Options must be a keyword list"}}
end
def to_string(date_time, _backend, _options) do
error_return(date_time, [:year, :month, :day, :hour, :minute, :second, :calendar])
end
@doc """
Formats a `t:DateTime.t/0` or `t:NaiveDateTime.t/0` according to the formats
defined in CLDR and described in [TR35](http://unicode.org/reports/tr35/tr35-dates.html)
or raises on error.
### Arguments
* `date_time` is a `t:DateTime.t/0` or `t:NaiveDateTime.t/0` struct or any map that contains
one or more of the keys `:year`, `:month`, `:day`, `:hour`, `:minute` and `:second` or
`:microsecond` with optional `:time_zone`, `:zone_abbr`, `:utc_offset`, `:std_offset`
and `:calendar` fields.
* `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 either a [standard format](Cldr.DateTime.Format.html#module-standard-formats)
(one of `:short`, `:medium`, `:long`, `:full`), a [format skeleton](Cldr.DateTime.Format.html#module-format-skeletons)
or a [format pattern](Cldr.DateTime.Format.html#module-format-patterns).
* The default is `:medium` for full *date_times* (that is, *dates_times* having `:year`,
`:month`, `:day`, `:hour`, `:minutes`, `:second` and `:calendar` fields).
* The default for partial *date_times* is to derive a format skeleton from the
*date_time* and find the best match from the formats returned by
`Cldr.DateTime.available_formats/3`.
* See `Cldr.DateTime.Format` for more information about specifying formats.
* `:date_format` is used to format the *date* part of a *date_time* and is either a standard
format or a format skeleton.
* If `:date_format` is not specified then the *date* format is defined by the `:format`
option.
* If `:date_format` is a format skeleton it may only include format fields
appropriate for a *date*.
* :date_format` may only be specified if `:format` is a standard format.
* `:time_format` is used to format the *time* part of a *date_time* and is either a standard
format or a format skeleton.
* If `:time_format` is not specified then the *time* format is defined by the `:format`
option.
* If `:time_format` is a format skeleton it may only include format fields
appropriate for a *time*.
* :time_format` may only be specified if `:format` is a standard format.
* `:style` is either `:at` or `:default`. When set to `:at` the *date_time* may
be formatted with a localised string representing `<date> at <time>` if such
a format exists. See `Cldr.DateTime.Format.date_time_at_formats/2`.
* `: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.DateTime.available_formats/3`
to see which formats have these variants. See [Variant Preference](#variant-preference)
below for more information.
* `:locale` is any valid locale name returned by `Cldr.known_locale_names/0`
or a `t:Cldr.LanguageTag.t/0` struct. The default is `Cldr.get_locale/0`.
* `:number_system` a number system into which the formatted *date_time* digits should
be transliterated. See `Cldr.known_number_systems/0`. The default is
the number system associated with the `:locale`.
* `:separators` selects which of the available symbol
sets should be used when formatting fractional seconds (format
character `S`). The default is `:standard`. Some limited locales have an alternative `:us`
variant that can be used. See `Cldr.Number.Symbol.number_symbols_for/3`
for the symbols supported for a given locale and number system.
* `:era` which, if set to `:variant`, will use a variant for the era if one
is available in the requested locale. In the `:en` locale, for example, `era: :variant`
will return `CE` instead of `AD` and `BCE` instead of `BC`.
* `:period` which, if set to `: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".
### Variant Preference
* A small number of formats have one of two different alternatives, each with their own
preference specifier. The preferences are specified with the `:prefer` option to
`Cldr.Date.to_string/3`. The preference is expressed as an atom, or a list of one or two
atoms with one atom being either `:unicode` or `:ascii` and one atom being either
`:default` or `:variant`.
* Some formats (at the time of publishng only *time* formats but that
may change in the future) have `:unicode` and `:ascii` versions of the format. The
difference is the use of ascii space (0x20) as a separateor in the `:ascii` verison
whereas the `:unicode` version may use non-breaking or other space characters. The
default is `:unicode` and this is the strongly preferred option. 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.
* Some formats (at the time of publishing, only *date* and *date_time* formats) have
`:default` and `:variant` versions of the format. These variant formats are only
included in a small number of locales. For example, the `:"en-CA"` locale, which has
a `:default` format respecting typical Canadian formatting and a `:variant` that is
more closely aligned to US formatting. The default is `:default`.
### Notes
* If the provided `date_time` contains only *date* fields, the call is delegated to
`Cldr.Date.to_string/2`.
* If the provided `date_time` contains only *time* fields, the call is delegated to
`Cldr.Time.to_string/2`.
### Returns
* `formatted_date_time` or
* raises an exception.
### Examples
iex> {:ok, date_time} = DateTime.from_naive(~N[2000-01-01 23:59:59.0], "Etc/UTC")
iex> Cldr.DateTime.to_string!(date_time, MyApp.Cldr, locale: :en)
"Jan 1, 2000, 11:59:59 PM"
iex> Cldr.DateTime.to_string!(date_time, MyApp.Cldr, format: :long, locale: :en)
"January 1, 2000, 11:59:59 PM UTC"
iex> Cldr.DateTime.to_string!(date_time, MyApp.Cldr, format: :full, locale: :en)
"Saturday, January 1, 2000, 11:59:59 PM Coordinated Universal Time"
iex> Cldr.DateTime.to_string!(date_time, MyApp.Cldr, format: :full, locale: :fr)
"samedi 1 janvier 2000, 23:59:59 temps universel coordonné"
iex> Cldr.DateTime.to_string!(date_time, MyApp.Cldr, format: :MMMMW, locale: :fr)
"semaine 1 (janvier)"
iex> Cldr.DateTime.to_string!(date_time, MyApp.Cldr, format: :yw, locale: :fr)
"semaine 1 de 2000"
iex> Cldr.DateTime.to_string!(date_time, MyApp.Cldr, format: :full, date_format: :yMd, time_format: :hms)
"1/1/2000, 11:59:59 PM"
"""
@spec to_string!(Cldr.Calendar.any_date_time(), Cldr.backend(), options()) ::
String.t() | no_return()
@spec to_string!(Cldr.Calendar.any_date_time(), options(), []) ::
String.t() | no_return()
def to_string!(date_time, backend \\ Cldr.Date.default_backend(), options \\ [])
def to_string!(date_time, backend, options) do
case to_string(date_time, backend, options) do
{:ok, string} -> string
{:error, {exception, message}} -> raise exception, message
end
end
@doc """
Returns a map of the standard date_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.DateTime.Format.date_time_formats(:en)
{:ok,
%Cldr.DateTime.Formats{
short: "{1}, {0}",
medium: "{1}, {0}",
long: "{1}, {0}",
full: "{1}, {0}"
}}
iex> Cldr.DateTime.Format.date_time_formats(:en, :buddhist, MyApp.Cldr)
{:ok,
%Cldr.DateTime.Formats{
short: "{1}, {0}",
medium: "{1}, {0}",
long: "{1}, {0}",
full: "{1}, {0}"
}}
"""
@spec formats(
Locale.locale_reference(),
Cldr.Calendar.calendar(),
Cldr.backend()
) ::
{:ok, map()} | {: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.date_time_formats(locale, calendar, backend)
end
@doc """
Returns a map of the available date_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.DateTime.available_formats(:en)
{:ok,
%{
MMMMW: %{
other: "'week' W 'of' MMMM",
one: "'week' W 'of' MMMM",
pluralize: :week_of_month
},
Ehm: %{unicode: "E h:mm a", ascii: "E h:mm a"},
y: "y",
MMMd: "MMM d",
yyMd: "M/d/yy",
GyM: "M/y G",
hv: %{unicode: "h a v", ascii: "h a v"},
Hm: "HH:mm",
Bhms: "h:mm:ss B",
ms: "mm:ss",
yMd: "M/d/y",
GyMMM: "MMM y G",
GyMMMEd: "E, MMM d, y G",
yMMMd: "MMM d, y",
EBh: "E h B",
Gy: "y G",
Hmsv: "HH:mm:ss v",
hmv: %{unicode: "h:mm a v", ascii: "h:mm a v"},
M: "L",
h: %{unicode: "h a", ascii: "h a"},
Hms: "HH:mm:ss",
yMMMMd: "MMMM d, y",
EHm: "E HH:mm",
ahmmsszzzz: %{unicode: "h:mm:ss a zzzz", ascii: "h:mm:ss a zzzz"},
MMM: "LLL",
ahmmssz: %{unicode: "h:mm:ss a z", ascii: "h:mm:ss a z"},
d: "d",
Hmv: "HH:mm v",
GyMd: "M/d/y G",
yQQQ: "QQQ y",
yMMMEd: "E, MMM d, y",
ahmmss: %{unicode: "h:mm:ss a", ascii: "h:mm:ss a"},
MMMMd: "MMMM d",
H: "HH",
MEd: "E, M/d",
Md: "M/d",
GyMEd: "E, M/d/y G",
yMMM: "MMM y",
EBhms: "E h:mm:ss B",
yw: %{
other: "'week' w 'of' Y",
one: "'week' w 'of' Y",
pluralize: :week_of_year
},
hmsv: %{unicode: "h:mm:ss a v", ascii: "h:mm:ss a v"},
MMMEd: "E, MMM d",
hms: %{unicode: "h:mm:ss a", ascii: "h:mm:ss a"},
EBhm: "E h:mm B",
EHms: "E HH:mm:ss",
Hv: "HH'h' v",
ahmm: %{unicode: "h:mm a", ascii: "h:mm a"},
hm: %{unicode: "h:mm a", ascii: "h:mm a"},
GyMMMd: "MMM d, y G",
yMEd: "E, M/d/y",
Eh: %{unicode: "E h a", ascii: "E h a"},
Bh: "h B",
Ehms: %{unicode: "E h:mm:ss a", ascii: "E h:mm:ss a"},
yMMMM: "MMMM y",
yQQQQ: "QQQQ y",
yMMMMEEEEd: "EEEE, MMMM d, y",
E: "ccc",
yM: "M/y",
Bhm: "h:mm B",
Ed: "d E"
}}
"""
@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
Format.date_time_available_formats(locale, calendar, backend)
end
defp normalize_options(date_time, backend, []) do
{locale, _backend} = Cldr.locale_and_backend_from(nil, backend)
number_system = Cldr.Number.System.number_system_from_locale(locale, backend)
calendar = Map.get(date_time, :calendar, Cldr.Calendar.Gregorian)
date_time = Map.put_new(date_time, :calendar, calendar)
{format, date_format, time_format} =
formats_from_options(date_time, nil, nil, nil, @default_standard_format)
options =
%{
locale: locale,
number_system: number_system,
format: format,
date_format: date_format,
time_format: time_format,
style: @default_style,
prefer: [@default_prefer],
separators: @default_separators
}
{:ok, date_time, options}
end
defp normalize_options(date_time, _backend, options) when is_map(options) do
{:ok, date_time, options}
end
defp normalize_options(date_time, backend, options) when is_list(options) do
{locale, backend} = Cldr.locale_and_backend_from(options[:locale], backend)
calendar = Map.get(date_time, :calendar, Cldr.Calendar.Gregorian)
date_time = Map.put_new(date_time, :calendar, calendar)
style = options[:style] || @default_style
prefer = Keyword.get(options, :prefer, @default_prefer) |> List.wrap()
format = options[:format]
date_format = options[:date_format]
time_format = options[:time_format]
locale_number_system = Cldr.Number.System.number_system_from_locale(locale, backend)
number_system = Keyword.get(options, :number_system, locale_number_system)
separators = Keyword.get(options, :separators, @default_separators)
{format, date_format, time_format} =
formats_from_options(date_time, format, date_format, time_format, @default_standard_format)
with :ok <- validate_formats_consistent(format, date_format, time_format) do
options =
options
|> Map.new()
|> Map.put(:locale, locale)
|> Map.put(:format, format)
|> Map.put(:date_format, date_format)
|> Map.put(:time_format, time_format)
|> Map.put(:style, style)
|> Map.put(:prefer, prefer)
|> Map.put(:number_system, number_system)
|> Map.put(:separators, separators)
{:ok, date_time, options}
end
end
@doc false
def format_for_skeleton(format, standard_format, skeleton, locale, calendar, backend) do
{:ok, available_formats} = available_formats(locale, calendar, backend)
case Map.fetch(available_formats, skeleton) do
{:ok, format} ->
{:ok, format}
:error ->
{:error,
{
Cldr.DateTime.UnresolvedFormat,
"Standard format #{inspect(format)} could not be resolved from " <>
"#{inspect(standard_format)}"
}}
end
end
# Only a single format, which is applied to date and time and to
# the compisition format.
defp validate_formats_consistent(format, nil = _date_format, nil = _time_format)
when is_atom(format) or is_binary(format) do
:ok
end
# No general format, just date and time format. We will derive the
# joining format later.
defp validate_formats_consistent(nil, date_format, time_format)
when not is_nil(date_format) and not is_nil(time_format) do
:ok
end
# All the formats are short, medium, long or full
defp validate_formats_consistent(format, date_format, time_format)
when format in @standard_formats and date_format in @standard_formats and
time_format in @standard_formats do
:ok
end
# Joining format is short, medium, long or full and date_foramt and
# time_format are an atom (including nil) or a string.
defp validate_formats_consistent(format, date_format, time_format)
when (format in @standard_formats or is_nil(format)) and
(is_atom(date_format) or is_binary(date_format)) and
(is_binary(format) or is_atom(time_format)) do
:ok
end
defp validate_formats_consistent(format, date_format, time_format)
when is_atom(format) or is_binary(format) do
{:error,
{Cldr.DateTime.InvalidFormat,
":date_format and :time_format cannot be specified if :format is also specified as " <>
"a format id or a format string. Found [format: #{inspect(format)}, time_format: #{inspect(time_format)}, " <>
"date_format: #{inspect(date_format)}]"}}
end
# Returns the CLDR calendar type for a calendar
@doc false
def type_from_calendar(Cldr.Calendar.Gregorian = calendar) do
{:ok, calendar.cldr_calendar_type()}
end
def type_from_calendar(calendar) do
with {:ok, calendar} <- Cldr.Calendar.validate_calendar(calendar) do
{:ok, calendar.cldr_calendar_type()}
end
end
# There are three formats required to format a date time:
# 1. A format for the date part, if any.
# 2. A format for the time part, if any.
# 3. A format for how to combine the two parts.
# All formats are optional - they can be derived. See
# https://www.unicode.org/reports/tr35/tr35-dates.html#Matching_Skeletons and
# https://www.unicode.org/reports/tr35/tr35-dates.html#Missing_Skeleton_Fields
# When we have a standard format then we use the same format name for
# the date and the time.
defp formats_from_options(date_time, nil, nil, nil, _default)
when is_any_date_time(date_time) do
{@default_standard_format, @default_standard_format, @default_standard_format}
end
defp formats_from_options(_date_time, format, nil, nil, _default)
when format in @standard_formats do
{format, format, format}
end
# When we have a string or atom format then it controls everything and there
# should be no separate date format or time format
defp formats_from_options(_date_time, format, nil, nil, _default)
when is_binary(format) do
{format, nil, nil}
end
defp formats_from_options(_date_time, format, nil, nil, _default)
when is_atom(format) do
{format, nil, nil}
end
# Replace nil date and time formats with the format iff format is
# one of the standard types.
defp formats_from_options(_date_time, format, date_format, nil, _default)
when format in @standard_formats do
{format, date_format, format}
end
defp formats_from_options(_date_time, format, nil, time_format, _default)
when format in @standard_formats do
{format, format, time_format}
end
defp formats_from_options(_date_time, format, nil, time_format, _default)
when time_format in @standard_formats do
{format, time_format, time_format}
end
defp formats_from_options(_date_time, format, date_format, nil, _default)
when date_format in @standard_formats do
{format, date_format, date_format}
end
# If standard date and time formats but no format, we'll derive the
# format later on.
defp formats_from_options(_date_time, nil = format, date_format, time_format, _default)
when date_format in @standard_formats and time_format in @standard_formats do
{format, date_format, time_format}
end
defp formats_from_options(_date_time, format, date_format, time_format, _default) do
{format, date_format, time_format}
end
# Resolve the actual format string for the date time format.
# Unless we need to derive the format, this only touches `format`,
# not `date_format` or `time_format`.
# If its a partial date_time and a standard format is requested, its an error
defp find_format(date_time, format, _locale, _calendar, _backend, _options)
when format in @standard_formats and not is_any_date_time(date_time) do
{:error,
{
Cldr.DateTime.UnresolvedFormat,
"Standard formats are not available for partial date times"
}}
end
# Standard format, at style
defp find_format(_date_time, format, locale, calendar, backend, %{style: :at} = options)
when format in @standard_formats do
with {:ok, formats} <- Format.date_time_at_formats(locale, calendar, backend),
{:ok, format} <- preferred_format(formats, format, options.prefer) do
{:ok, format, options}
end
end
# Standard format, standard style
defp find_format(_date_time, format, locale, calendar, backend, options)
when format in @standard_formats do
with {:ok, formats} <- Format.date_time_formats(locale, calendar, backend),
{:ok, format} <- preferred_format(formats, format, options.prefer) do
{:ok, format, options}
end
end
# The format is specified as a skeleton so we need to find
# the best match for it. The best match can be a single skeleton
# (that is guaranteed to be in the date_time_available_formats list)
# or two skeletons - one for the date part and one for the time part.
defp find_format(date_time, format, locale, calendar, backend, options)
when is_skeleton(format) do
case Match.best_match(format, locale, calendar, backend) do
{:ok, {date_format, time_format}} ->
options =
options
|> Map.put(:date_format, date_format)
|> Map.put(:time_format, time_format)
|> Map.put(:requested_skeleton, format)
# Since the return is a date and a time format, we need
# to derive the joining format.
find_format(date_time, nil, locale, calendar, backend, options)
{:ok, format} ->
with {:ok, skeleton_tokens} <- Compiler.tokenize_skeleton(options.format),
{:ok, formats} <- Format.date_time_available_formats(locale, calendar, backend),
{:ok, preferred_format} <- preferred_format(formats, format, options.prefer),
{:ok, format_string} <- Match.adjust_field_lengths(preferred_format, skeleton_tokens) do
{:ok, format_string, options}
end
error ->
error
end
end
# We need to derive the format, and maybe even date_format and time_format too.
defp find_format(date_time, nil, locale, calendar, backend, options) do
date_format = options.date_format
time_format = options.time_format
with {:ok, date_format} <-
date_format(date_time, date_format, locale, calendar, backend, options),
{:ok, time_format} <-
time_format(date_time, time_format, locale, calendar, backend, options),
{:ok, format} <- resolve_format(date_format, options.style, locale, calendar, backend) do
options =
options
|> Map.put(:date_format, date_format)
|> Map.put(:time_format, time_format)
{:ok, format, options}
end
end
# Straight up format string
defp find_format(_date_time, format, _locale, _calendar, _backend, options)
when is_binary(format) do
{:ok, format, options}
end
# Format with a number system
defp find_format(date_time, %{} = format, locale, calendar, backend, options) do
%{number_system: number_system, format: format} = format
{:ok, format_string, options} =
find_format(date_time, format, locale, calendar, backend, options)
{:ok, %{number_system: number_system, format: format_string}, options}
end
@doc false
def best_match(format, locale, calendar, backend, options) do
skeleton = options[:requested_skeleton] || format
with {:ok, available_formats} <-
Cldr.DateTime.Format.date_time_available_formats(locale, calendar, backend),
{:ok, skeleton_tokens} <- Compiler.tokenize_skeleton(skeleton),
{:ok, match} <- Match.best_match(format, locale, calendar, backend),
{:ok, format_string} <- Map.fetch(available_formats, match) do
Match.adjust_field_lengths(format_string, skeleton_tokens)
else
:error ->
{:error, Match.no_format_resolved_error(format)}
other ->
other
end
end
# From https://www.unicode.org/reports/tr35/tr35-dates.html#Missing_Skeleton_Fields
# Combine the patterns for the two dateFormatItems using the appropriate dateTimeFormat pattern, determined as follows from the requested date
# fields:
# If the requested date fields include wide month (MMMM, LLLL) and weekday name of any length (e.g. E, EEEE, c, cccc), use <dateTimeFormatLength
# type="full">
# Otherwise, if the requested date fields include wide month, use <dateTimeFormatLength type="long">
# Otherwise, if the requested date fields include abbreviated month (MMM, LLL), use <dateTimeFormatLength type="medium">
# Otherwise use <dateTimeFormatLength type="short">
defp resolve_format(%{format: format}, style, locale, calendar, backend) do
resolve_format(format, style, locale, calendar, backend)
end
defp resolve_format(date_format, style, locale, calendar, backend) do
{:ok, formats} = formats_for_style(style, locale, calendar, backend)
cond do
has_wide_month?(date_format) && has_weekday_name?(date_format) ->
{:ok, formats.full}
has_wide_month?(date_format) ->
{:ok, formats.long}
has_abbreviated_month?(date_format) ->
{:ok, formats.medium}
true ->
{:ok, formats.short}
end
end
defp formats_for_style(:at, locale, calendar, backend) do
Cldr.DateTime.Format.date_time_at_formats(locale, calendar, backend)
end
defp formats_for_style(_standard, locale, calendar, backend) do
Cldr.DateTime.Format.date_time_formats(locale, calendar, backend)
end
# We need to derive the date format now since that data
# is used to establish what date_time format we derive.
defp date_format(date_time, nil, locale, calendar, backend, options) do
format_id = Cldr.Date.derive_format_id(date_time)
date_format(date_time, format_id, locale, calendar, backend, options)
end
defp date_format(date_time, format_id, locale, calendar, backend, options) do
Cldr.Date.find_format(date_time, format_id, locale, calendar, backend, options)
end
defp time_format(date_time, nil, locale, calendar, backend, options) do
format_id = Cldr.Time.derive_format_id(date_time)
time_format(date_time, format_id, locale, calendar, backend, options)
end
defp time_format(date_time, format_id, locale, calendar, backend, options) do
Cldr.Time.find_format(date_time, format_id, locale, calendar, backend, options)
end
# FIXME These functions don't consider the impact
# of literals in the format. For now, the only known
# literal is a "," or "at" so we are safe for the moment.
defp has_wide_month?(format) do
String.contains?(format, "MMMM") || String.contains?(format, "LLLL")
end
defp has_abbreviated_month?(format) do
String.contains?(format, "MMM") || String.contains?(format, "LLL")
end
defp has_weekday_name?(format) do
String.contains?(format, "E") || String.contains?(format, "c")
end
# Given the fields in the (maybe partial) date, derive
# format id (atom map key into available formats)
@doc false
def derive_format_id(date_time, field_map, field_names) do
date_time
|> Map.take(field_names)
|> Map.keys()
|> Enum.map(&Map.fetch!(field_map, &1))
|> Enum.join()
|> String.to_atom()
end
defp preferred_format(formats, format, prefer) do
case Map.fetch(formats, format) do
{:ok, format} ->
apply_preference(format, prefer)
:error ->
{:error,
{Cldr.DateTime.InvalidFormat,
"Invalid date_time format #{inspect(format)}. " <>
"The valid formats are #{inspect(@standard_formats)}."}}
end
end
@doc false
def apply_preference(%{unicode: unicode, ascii: ascii}, preference) do
if :ascii in preference do
{:ok, ascii}
else
{:ok, unicode}
end
end
def apply_preference(%{default: default, variant: variant}, preference) do
if :variant in preference do
{:ok, variant}
else
{:ok, default}
end
end
def apply_preference(%{default: default}, _preference) do
{:ok, default}
end
def apply_preference(%{unicode: unicode}, _preference) do
{:ok, unicode}
end
def apply_preference(format, _) do
{:ok, format}
end
@doc false
def resolve_plural_format(%{other: _, pluralize: field} = format, date_time, backend, options) do
pluralizer = Module.concat(backend, Number.Cardinal)
case apply(Cldr.Calendar, field, [date_time]) do
{_year_or_month, month_or_week} ->
{:ok, pluralizer.pluralize(month_or_week, options.locale, format)}
other ->
{:ok, pluralizer.pluralize(other, options.locale, format)}
end
end
def resolve_plural_format(format, _date_time, _backend, _options) do
{:ok, format}
end
defp error_return(map, requirements) do
requirements =
requirements
|> Enum.map(&inspect/1)
|> Cldr.DateTime.Formatter.join_requirements()
{:error,
{ArgumentError,
"Invalid DateTime. DateTime is a map that contains at least #{requirements}. " <>
"Found: #{inspect(map)}"}}
end
end