Current section

Files

Jump to
ex_cldr_dates_times lib cldr date_time.ex
Raw

lib/cldr/date_time.ex

defmodule Cldr.DateTime do
@moduledoc """
Provides localization and formatting of a datetime.
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)
CLDR provides standard format strings for `t:DateTime.t/0` which
are represented by the names `:short`, `:medium`, `:long`
and `:full`. This allows for locale-independent
formatting since each locale and calendar will define the underlying
format string as appropriate.
"""
alias Cldr.DateTime.Format
alias Cldr.LanguageTag
alias Cldr.Locale
@typep options :: Keyword.t() | map()
@format_types [:short, :medium, :long, :full]
@default_format_type :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(datetime)
when is_map_key(datetime, :year) and
is_map_key(datetime, :month) and
is_map_key(datetime, :day) and
is_map_key(datetime, :hour) and
is_map_key(datetime, :minute) and
is_map_key(datetime, :second)
@doc """
Indicates if a given map fulfills the requirements
for a date time.
"""
defguard is_date_time(datetime)
when is_naive_date_time(datetime) and
is_map_key(datetime, :time_zone) and
is_map_key(datetime, :zone_abbr)
@doc """
Indicates if a given map fulfills the requirements
for a naive date time or date time.
"""
defguard is_any_date_time(datetime)
when is_date_time(datetime) or is_naive_date_time(datetime)
@doc """
Guards whether the given datetime has components of
a date.
"""
defguard has_date(datetime)
when is_map_key(datetime, :year) or is_map_key(datetime, :month) or
is_map_key(datetime, :day)
@doc """
Guards whether the given datetime has components of
a time.
"""
defguard has_time(datetime)
when is_map_key(datetime, :hour) or is_map_key(datetime, :minute) or
is_map_key(datetime, :second)
@doc """
Guard whether the given datetime has components of
both a date and a time.
"""
defguard has_date_and_time(datetime)
when has_date(datetime) and has_time(datetime)
defmodule Formats do
@moduledoc false
defstruct Module.get_attribute(Cldr.DateTime, :format_types)
end
@doc """
Formats a DateTime according to a format string
as defined in CLDR and described in [TR35](http://unicode.org/reports/tr35/tr35-dates.html).
### Arguments
* `datetime` 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 one of `:short`, `:medium`, `:long`, `:full`, or a format ID
or a format string. The default is `:medium` for full datetimes (that is,
dates having `:year`, `:month`, `:day`, `:hour`, `:minutes`, `:second` and
`:calendar` fields). The default for partial datetimes is to derive a candidate
format ID from the date and find the best match from the formats returned by
`Cldr.DateTime.available_formats/3`. See [here](README.md#date-time-and-datetime-localization-formats)
for more information about specifying formats.
* `:date_format` is any one of `:short`, `:medium`, `:long`, `:full`. If defined,
this option is used to format the date part of the date time. This option is
only acceptable if the `:format` option is not specified, or is specified as either
`:short`, `:medium`, `:long`, `:full`. If `:date_format` is not specified
then the date format is defined by the `:format` option.
* `:time_format` is any one of `:short`, `:medium`, `:long`, `:full`. If defined,
this option is used to format the time part of the date time. This option is
only acceptable if the `:format` option is not specified, or is specified as either
`:short`, `:medium`, `:long`, `:full`. If `:time_format` is not specified
then the time format is defined by the `:format` option.
* `:style` is either `:at` or `:default`. When set to `:at` the datetime 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.Format.date_time_available_formats/3`
to see which formats have these variants.
* `: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 datetime 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: :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 datetime 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 `datetime` contains only date fields, the call is delegated to
`Cldr.Date.to_string/2`.
* If the provided `datetime` contains only time fields, the call is delegated to
`Cldr.Time.to_string/2`.
### Returns
* `{:ok, formatted_datetime}` 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 GMT"}
iex> Cldr.DateTime.to_string(date_time, MyApp.Cldr, format: :full, locale: :fr)
{:ok, "samedi 1 janvier 2000, 23:59:59 UTC"}
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 GMT"}
iex> Cldr.DateTime.to_string(date_time, MyApp.Cldr, format: :full, style: :at, locale: :fr)
{:ok, "samedi 1 janvier 2000 à 23:59:59 UTC"}
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"}
"""
@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(datetime, backend \\ Cldr.Date.default_backend(), options \\ [])
def to_string(%{calendar: Calendar.ISO} = datetime, backend, options) do
%{datetime | calendar: Cldr.Calendar.Gregorian}
|> to_string(backend, options)
end
def to_string(datetime, options, []) when is_list(options) do
{locale, backend} = Cldr.locale_and_backend_from(options)
options = Keyword.put_new(options, :locale, locale)
to_string(datetime, backend, options)
end
def to_string(%{} = datetime, backend, options)
when is_atom(backend) and has_date_and_time(datetime) do
format_backend = Module.concat(backend, DateTime.Formatter)
with {:ok, datetime, options} <- normalize_options(datetime, backend, options),
{:ok, locale} <- Cldr.validate_locale(options.locale, backend),
{:ok, cldr_calendar} <- Cldr.DateTime.type_from_calendar(datetime.calendar),
{:ok, _} <- Cldr.Number.validate_number_system(locale, options.number_system, backend),
{:ok, format, options} <-
find_format(datetime, options.format, locale, cldr_calendar, backend, options),
{:ok, format} <- apply_preference(format, options.prefer),
{:ok, format_string} <- resolve_plural_format(format, datetime, backend, options) do
format_backend.format(datetime, format_string, locale, options)
end
rescue
e in [Cldr.DateTime.FormatError] ->
{:error, {e.__struct__, e.message}}
end
def to_string(%{} = datetime, backend, options)
when is_atom(backend) and has_date(datetime) do
Cldr.Date.to_string(datetime, backend, options)
end
def to_string(%{} = datetime, backend, options)
when is_atom(backend) and has_time(datetime) do
Cldr.Time.to_string(datetime, backend, options)
end
def to_string(datetime, value, []) when is_map(datetime) do
{:error,
{ArgumentError, "Unexpected option value #{inspect(value)}. Options must be a keyword list"}}
end
def to_string(datetime, _backend, _options) do
error_return(datetime, [:year, :month, :day, :hour, :minute, :second, :calendar])
end
@doc """
Formats a DateTime according to a format string
as defined in CLDR and described in [TR35](http://unicode.org/reports/tr35/tr35-dates.html)
returning a formatted string or raising on error.
### Arguments
* `datetime` 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 one of `:short`, `:medium`, `:long`, `:full`, or a format ID
or a format string. The default is `:medium` for full datetimes (that is,
dates having `:year`, `:month`, `:day`, `:hour`, `:minutes`, `:second` and
`:calendar` fields). The default for partial datetimes is to derive a candidate
format ID from the date and find the best match from the formats returned by
`Cldr.DateTime.available_formats/3`. See [here](README.md#date-time-and-datetime-localization-formats)
for more information about specifying formats.
* `:style` is either `:at` or `:default`. When set to `:at` the datetime 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 datetime
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.Format.date_time_available_formats/2`
to see which formats have these variants.
* `: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 datetime digits should
be transliterated.
* `: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: :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 datetime 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 `datetime` contains only date fields, the call is delegated to
`Cldr.Date.to_string/2`.
* If the provided `datetime` contains only time fields, the call is delegated to
`Cldr.Time.to_string/2`.
### Returns
* `formatted_datetime` 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 GMT"
iex> Cldr.DateTime.to_string!(date_time, MyApp.Cldr, format: :full, locale: :fr)
"samedi 1 janvier 2000, 23:59:59 UTC"
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"
"""
@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!(datetime, backend \\ Cldr.Date.default_backend(), options \\ [])
def to_string!(datetime, backend, options) do
case to_string(datetime, backend, options) do
{:ok, string} -> string
{:error, {exception, message}} -> raise exception, message
end
end
@doc """
Returns a map of the standard datetime 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{
full: "{1}, {0}",
long: "{1}, {0}",
medium: "{1}, {0}",
short: "{1}, {0}"
}}
iex> Cldr.DateTime.Format.date_time_formats(:en, :buddhist, MyApp.Cldr)
{:ok, %Cldr.DateTime.Formats{
full: "{1}, {0}",
long: "{1}, {0}",
medium: "{1}, {0}",
short: "{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 datetime 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,
%{
yw: %{
other: "'week' w 'of' Y",
one: "'week' w 'of' Y",
pluralize: :week_of_year
},
GyMMMEd: "E, MMM d, y G",
Hms: "HH:mm:ss",
MMMMW: %{
other: "'week' W 'of' MMMM",
one: "'week' W 'of' MMMM",
pluralize: :week_of_month
},
E: "ccc",
MMMd: "MMM d",
yMEd: "E, M/d/y",
yQQQ: "QQQ y",
Ehm: %{unicode: "E h:mm a", ascii: "E h:mm a"},
M: "L",
hm: %{unicode: "h:mm a", ascii: "h:mm a"},
yM: "M/y",
GyMMMd: "MMM d, y G",
GyMd: "M/d/y G",
Gy: "y G",
Hm: "HH:mm",
EBhms: "E h:mm:ss B",
d: "d",
hms: %{unicode: "h:mm:ss a", ascii: "h:mm:ss a"},
Ed: "d E",
Ehms: %{unicode: "E h:mm:ss a", ascii: "E h:mm:ss a"},
EHms: "E HH:mm:ss",
Bh: "h B",
h: %{unicode: "h a", ascii: "h a"},
Bhms: "h:mm:ss B",
Hmv: "HH:mm v",
hmv: %{unicode: "h:mm a v", ascii: "h:mm a v"},
yMd: "M/d/y",
ms: "mm:ss",
MMM: "LLL",
y: "y",
Bhm: "h:mm B",
yMMM: "MMM y",
yQQQQ: "QQQQ y",
yMMMEd: "E, MMM d, y",
yMMMM: "MMMM y",
EBhm: "E h:mm B",
Hmsv: "HH:mm:ss v",
yMMMd: "MMM d, y",
MEd: "E, M/d",
EHm: "E HH:mm",
GyMMM: "MMM y G",
hmsv: %{unicode: "h:mm:ss a v", ascii: "h:mm:ss a v"},
H: "HH",
Md: "M/d",
MMMEd: "E, MMM d",
MMMMd: "MMMM d"
}}
"""
@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(datetime, 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(datetime, :calendar, Cldr.Calendar.Gregorian)
datetime = Map.put_new(datetime, :calendar, calendar)
{format, date_format, time_format} =
formats_from_options(datetime, nil, nil, nil, @default_format_type)
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, datetime, options}
end
defp normalize_options(datetime, _backend, options) when is_map(options) do
{:ok, datetime, options}
end
defp normalize_options(datetime, backend, options) when is_list(options) do
{locale, backend} = Cldr.locale_and_backend_from(options[:locale], backend)
calendar = Map.get(datetime, :calendar, Cldr.Calendar.Gregorian)
datetime = Map.put_new(datetime, :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(datetime, format, date_format, time_format, @default_format_type)
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, datetime, options}
end
end
defp validate_formats_consistent(format, nil = _date_format, nil = _time_format)
when is_atom(format) or is_binary(format) do
:ok
end
defp validate_formats_consistent(nil, date_format, time_format)
when not is_nil(date_format) and not is_nil(time_format) do
:ok
end
defp validate_formats_consistent(format, date_format, time_format)
when format in @format_types and date_format in @format_types and
time_format in @format_types 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 [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(datetime, nil, nil, nil, _default)
when is_any_date_time(datetime) do
{@default_format_type, @default_format_type, @default_format_type}
end
defp formats_from_options(_datetime, format, nil, nil, _default)
when format in @format_types 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(_datetime, format, nil, nil, _default)
when is_binary(format) do
{format, nil, nil}
end
defp formats_from_options(_datetime, 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(_datetime, format, date_format, nil, _default)
when format in @format_types do
{format, date_format, format}
end
defp formats_from_options(_datetime, format, nil, time_format, _default)
when format in @format_types do
{format, format, time_format}
end
defp formats_from_options(_datetime, format, nil, time_format, _default)
when time_format in @format_types do
{format, time_format, time_format}
end
defp formats_from_options(_datetime, format, date_format, nil, _default)
when date_format in @format_types 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(_datetime, nil = format, date_format, time_format, _default)
when date_format in @format_types and time_format in @format_types do
{format, date_format, time_format}
end
defp formats_from_options(_datetime, 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`.
# Standard format, at style
defp find_format(_datetime, format, locale, calendar, backend, %{style: :at} = options)
when format in @format_types do
%LanguageTag{cldr_locale_name: locale_name} = locale
with {:ok, formats} <- Format.date_time_at_formats(locale_name, calendar, backend),
{:ok, format} <- preferred_format(formats, format, options.prefer) do
{:ok, format, options}
end
end
# Standard format, standard style
defp find_format(_datetime, format, locale, calendar, backend, options)
when format in @format_types do
%LanguageTag{cldr_locale_name: locale_name} = locale
with {:ok, formats} <- Format.date_time_formats(locale_name, calendar, backend),
{:ok, format} <- preferred_format(formats, format, options.prefer) do
{:ok, format, options}
end
end
# Look up for the format in :available_formats
defp find_format(_datetime, format, locale, calendar, backend, options)
when is_atom(format) and not is_nil(format) do
%LanguageTag{cldr_locale_name: locale_name} = locale
with {:ok, formats} <- Format.date_time_available_formats(locale_name, calendar, backend),
{:ok, format} <- preferred_format(formats, format, options.prefer) do
{:ok, format, options}
end
end
# Straight up format string
defp find_format(_datetime, format, _locale, _calendar, _backend, options)
when is_binary(format) do
{:ok, format, options}
end
# Format with a number system
defp find_format(datetime, %{} = format, locale, calendar, backend, options) do
%{number_system: number_system, format: format} = format
{:ok, format_string, options} =
find_format(datetime, format, locale, calendar, backend, options)
{:ok, %{number_system: number_system, format: format_string}, options}
end
# If its a partial datetime and a standard format is requested, its an error
defp find_format(datetime, format, _locale, _calendar, _backend, _options)
when format in @format_types and not is_any_date_time(datetime) do
{:error,
{
Cldr.DateTime.UnresolvedFormat,
"Standard formats are not available for partial date times"
}}
end
# We need to derive the format, or maybe even date_format and time_format
defp find_format(datetime, nil, locale, calendar, backend, options) do
date_format = options.date_format
time_format = options.time_format
with {:ok, date_format} <- date_format(datetime, date_format, locale, calendar, backend),
{:ok, time_format} <- time_format(datetime, time_format, locale, calendar, backend),
{:ok, format} <- resolve_format(date_format, locale, calendar, backend) do
options =
options
|> Map.put(:date_format, date_format)
|> Map.put(:time_format, time_format)
{:ok, format, options}
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(date_format, locale, calendar, backend) do
{:ok, formats} = Cldr.DateTime.Format.date_time_formats(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
# We need to derive the date format now since that data
# is used to establish what datetime format we derive.
defp date_format(datetime, nil, locale, calendar, backend) do
format = Cldr.Date.derive_format_id(datetime)
Cldr.Date.find_format(datetime, format, locale, calendar, backend)
end
defp date_format(datetime, format, locale, calendar, backend) do
Cldr.Date.find_format(datetime, format, locale, calendar, backend)
end
defp time_format(datetime, nil, locale, calendar, backend) do
format = Cldr.Time.derive_format_id(datetime)
Cldr.Time.find_format(datetime, format, locale, calendar, backend)
end
defp time_format(datetime, format, locale, calendar, backend) do
Cldr.Time.find_format(datetime, format, locale, calendar, backend)
end
# FIXME These functions don't consider the impace
# 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(datetime, field_map, field_names) do
datetime
|> 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 datetime format #{inspect(format)}. " <>
"The valid formats are #{inspect(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