defmodule Cldr.DateTime.Interval do @moduledoc """ Interval formats allow for software to format intervals like "Jan 10-12, 2008" as a shorter and more natural format than "Jan 10, 2008 - Jan 12, 2008". They are designed to take a start and end date, time or datetime plus a formatting pattern and use that information to produce a localized format. See `Cldr.Interval.to_string/3` and `Cldr.DateTime.Interval.to_string/3` """ import Cldr.Calendar, only: [ naivedatetime: 0 ] @default_format :medium @formats [:short, :medium, :long] @default_prefer :default if Cldr.Code.ensure_compiled?(CalendarInterval) do @doc false def to_string(%CalendarInterval{} = interval) do {locale, backend} = Cldr.locale_and_backend_from(nil, nil) to_string(interval, backend, locale: locale) end end if Cldr.Code.ensure_compiled?(CalendarInterval) do @doc false def to_string(%CalendarInterval{} = interval, backend) when is_atom(backend) do {locale, backend} = Cldr.locale_and_backend_from(nil, backend) to_string(interval, backend, locale: locale) end @doc false def to_string(%CalendarInterval{} = interval, options) when is_list(options) do {locale, backend} = Cldr.locale_and_backend_from(options) to_string(interval, backend, locale: locale) end @doc """ Returns a localised string representing the formatted `CalendarInterval`. ### Arguments * `range` is a `CalendarInterval.t` * `backend` is any module that includes `use Cldr` and is therefore a `Cldr` backend module * `options` is a keyword list of options. The default is `[]`. ### Options * `:format` is one of `:short`, `:medium` or `:long` or a specific format type or a string representation of an interval format. The default is `:medium`. * `: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. * `: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 digits should be transliterated. * `:prefer` expresses the preference for one of the possible alternative sub-formats. See the variant preference notes below. ### 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.Date.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`. ### Returns * `{:ok, string}` or * `{:error, {exception, reason}}` ### Notes * `CalendarInterval` support requires adding the dependency [calendar_interval](https://hex.pm/packages/calendar_interval) to the `deps` configuration in `mix.exs`. * For more information on interval format string see the `Cldr.Interval`. * The available predefined formats that can be applied are the keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` where `"en"` can be replaced by any configuration locale name and `:gregorian` is the underlying CLDR calendar type. * In the case where `from` and `to` are equal, a single datetime is formatted instead of an interval ### Examples iex> Cldr.DateTime.Interval.to_string ~I"2020-01-01 10:00/12:00", MyApp.Cldr {:ok, "Jan 1, 2020, 10:00:00 AM – 12:00:00 PM"} """ @spec to_string(CalendarInterval.t(), Cldr.backend(), Keyword.t()) :: {:ok, String.t()} | {:error, {module, String.t()}} def to_string(%CalendarInterval{first: from, last: to, precision: precision}, backend, options) when precision in [:year, :month, :day] do Cldr.Date.Interval.to_string(from, to, backend, options) end def to_string(%CalendarInterval{first: from, last: to, precision: precision}, backend, options) when precision in [:hour, :minute] do from = %{from | second: 0, microsecond: {0, 6}} to = %{to | second: 0, microsecond: {0, 6}} to_string(from, to, backend, options) end def to_string(%CalendarInterval{first: from, last: to, precision: precision}, backend, options) when precision in [:hour, :minute] do from = %{from | microsecond: {0, 6}} to = %{to | microsecond: {0, 6}} to_string(from, to, backend, options) end end @doc false def to_string(unquote(naivedatetime()) = from, unquote(naivedatetime()) = to) do {locale, backend} = Cldr.locale_and_backend_from(nil, nil) to_string(from, to, backend, locale: locale) end def to_string(nil = from, unquote(naivedatetime()) = to) do _ = calendar {locale, backend} = Cldr.locale_and_backend_from(nil, nil) to_string(from, to, backend, locale: locale) end def to_string(unquote(naivedatetime()) = from, nil = to) do _ = calendar {locale, backend} = Cldr.locale_and_backend_from(nil, nil) to_string(from, to, backend, locale: locale) end @doc false def to_string(unquote(naivedatetime()) = from, unquote(naivedatetime()) = to, backend) when is_atom(backend) do {locale, backend} = Cldr.locale_and_backend_from(nil, backend) to_string(from, to, backend, locale: locale) end def to_string(nil = from, unquote(naivedatetime()) = to, backend) when is_atom(backend) do _ = calendar {locale, backend} = Cldr.locale_and_backend_from(nil, backend) to_string(from, to, backend, locale: locale) end def to_string(unquote(naivedatetime()) = from, nil = to, backend) when is_atom(backend) do _ = calendar {locale, backend} = Cldr.locale_and_backend_from(nil, backend) to_string(from, to, backend, locale: locale) end @doc false def to_string(unquote(naivedatetime()) = from, unquote(naivedatetime()) = to, options) when is_list(options) do {locale, backend} = Cldr.locale_and_backend_from(options) options = Keyword.put_new(options, :locale, locale) to_string(from, to, backend, options) end def to_string(nil = from, unquote(naivedatetime()) = to, options) when is_list(options) do _ = calendar {locale, backend} = Cldr.locale_and_backend_from(options) options = Keyword.put_new(options, :locale, locale) to_string(from, to, backend, options) end def to_string(unquote(naivedatetime()) = from, nil = to, options) when is_list(options) do _ = calendar {locale, backend} = Cldr.locale_and_backend_from(options) options = Keyword.put_new(options, :locale, locale) to_string(from, to, backend, options) end @doc """ Returns a localised string representing the formatted interval formed by two dates. ### Arguments * `from` is any map that conforms to the `Calendar.datetime` type. * `to` is any map that conforms to the `Calendar.datetime` type. `to` must occur on or after `from`. * `backend` is any module that includes `use Cldr` and is therefore a `Cldr` backend module * `options` is a keyword list of options. The default is `[]`. Either of `from` or `to` may also be `nil` in which case the result is an "open" interval and the non-nil parameter is formatted using `Cldr.DateTime.to_string/3`. ### Options * `:format` is one of `:short`, `:medium` or `:long` or a specific format type or a string representation of an interval format. The default is `:medium`. * `: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 thenthe time format is defined by the `:format` option. * `: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 digits should be transliterated. * `:prefer` expresses the preference for one of the possible alternative sub-formats. See the variant preference notes below. ### 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.Date.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`. ### Returns * `{:ok, string}` or * `{:error, {exception, reason}}` ### Notes * For more information on interval format string see the `Cldr.Interval`. * The available predefined formats that can be applied are the keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` where `"en"` can be replaced by any configuration locale name and `:gregorian` is the underlying CLDR calendar type. * In the case where `from` and `to` are equal, a single date is formatted instead of an interval ### Examples iex> Cldr.DateTime.Interval.to_string ~U[2020-01-01 00:00:00.0Z], ...> ~U[2020-12-31 10:00:00.0Z], MyApp.Cldr {:ok, "Jan 1, 2020, 12:00:00 AM – Dec 31, 2020, 10:00:00 AM"} iex> Cldr.DateTime.Interval.to_string ~U[2020-01-01 00:00:00.0Z], nil, MyApp.Cldr {:ok, "Jan 1, 2020, 12:00:00 AM –"} """ @spec to_string(Calendar.datetime() | nil, Calendar.datetime() | nil, Cldr.backend(), Keyword.t()) :: {:ok, String.t()} | {:error, {module, String.t()}} def to_string(from, to, backend, options \\ []) def to_string(unquote(naivedatetime()) = from, unquote(naivedatetime()) = to, backend, options) when calendar == Calendar.ISO do from = %{from | calendar: Cldr.Calendar.Gregorian} to = %{to | calendar: Cldr.Calendar.Gregorian} to_string(from, to, backend, options) end def to_string(nil = from, unquote(naivedatetime()) = to, backend, options) when calendar == Calendar.ISO do to = %{to | calendar: Cldr.Calendar.Gregorian} to_string(from, to, backend, options) end def to_string(unquote(naivedatetime()) = from, nil = to, backend, options) when calendar == Calendar.ISO do from = %{from | calendar: Cldr.Calendar.Gregorian} to_string(from, to, backend, options) end def to_string(unquote(naivedatetime()) = from, unquote(naivedatetime()) = to, options, []) when is_list(options) do {locale, backend} = Cldr.locale_and_backend_from(options) to_string(from, to, backend, Keyword.put_new(options, :locale, locale)) end def to_string(unquote(naivedatetime()) = from, nil = to, options, []) when is_list(options) do _ = calendar {locale, backend} = Cldr.locale_and_backend_from(options) to_string(from, to, backend, Keyword.put_new(options, :locale, locale)) end def to_string(nil = from, unquote(naivedatetime()) = to, options, []) when is_list(options) do _ = calendar {locale, backend} = Cldr.locale_and_backend_from(options) to_string(from, to, backend, Keyword.put_new(options, :locale, locale)) end def to_string(unquote(naivedatetime()) = from, unquote(naivedatetime()) = to, backend, options) do options = normalize_options(from, backend, options) format = options.format locale = options.locale backend = options.backend number_system = options.number_system date_format = Map.get(options, :date_format) time_format = Map.get(options, :time_format) with {:ok, _} <- from_less_than_or_equal_to(from, to), {:ok, backend} <- Cldr.validate_backend(backend), {:ok, locale} <- Cldr.validate_locale(locale, backend), {:ok, calendar} <- Cldr.Calendar.validate_calendar(from.calendar), {:ok, _} <- Cldr.Number.validate_number_system(locale, number_system, backend), {:ok, format, date_format, time_format} <- validate_format(format, date_format, time_format, options), {:ok, greatest_difference} <- greatest_difference(from, to) do options = adjust_options(options, locale, format, date_format, time_format) format_date_time(from, to, locale, backend, calendar, greatest_difference, options) else {:error, :no_practical_difference} -> options = adjust_options(options, locale, format) Cldr.DateTime.to_string(from, backend, options) other -> other end end # Open ended intervals use the `date_time_interval_fallback/0` format def to_string(nil, unquote(naivedatetime()) = to, backend, options) do {locale, backend} = Cldr.locale_and_backend_from(options[:locale], backend) cldr_calendar = calendar.cldr_calendar_type() with {:ok, formatted} <- Cldr.DateTime.to_string(to, backend, options) do pattern = Module.concat(backend, DateTime.Format).date_time_interval_fallback(locale, cldr_calendar) result = ["", formatted] |> Cldr.Substitution.substitute(pattern) |> Enum.join() |> String.trim_leading() {:ok, result} end end def to_string(unquote(naivedatetime()) = from, nil, backend, options) do {locale, backend} = Cldr.locale_and_backend_from(options[:locale], backend) cldr_calendar = calendar.cldr_calendar_type() with {:ok, formatted} <- Cldr.DateTime.to_string(from, backend, options) do pattern = Module.concat(backend, DateTime.Format).date_time_interval_fallback(locale, cldr_calendar) result = [formatted, ""] |> Cldr.Substitution.substitute(pattern) |> Enum.join() |> String.trim_trailing() {:ok, result} end end if Cldr.Code.ensure_compiled?(CalendarInterval) do @doc false def to_string!(%CalendarInterval{} = range, backend) when is_atom(backend) do {locale, backend} = Cldr.locale_and_backend_from(nil, backend) to_string!(range, backend, locale: locale) end @doc false def to_string!(%CalendarInterval{} = range, options) when is_list(options) do {locale, backend} = Cldr.locale_and_backend_from(options[:locale], nil) options = Keyword.put_new(options, :locale, locale) to_string!(range, backend, options) end end if Cldr.Code.ensure_compiled?(CalendarInterval) do @doc """ Returns a localised string representing the formatted interval formed by two dates or raises an exception. ### Arguments * `from` is any map that conforms to the `Calendar.datetime` type. * `to` is any map that conforms to the `Calendar.datetime` type. `to` must occur on or after `from`. * `backend` is any module that includes `use Cldr` and is therefore a `Cldr` backend module. * `options` is a keyword list of options. The default is `[]`. ### Options * `:format` is one of `:short`, `:medium` or `:long` or a specific format type or a string representation of an interval format. The default is `:medium`. * `: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. * `: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 digits should be transliterated. * `:prefer` expresses the preference for one of the possible alternative sub-formats. See the variant preference notes below. ### 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.Date.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`. ### Returns * `string` or * raises an exception ### Notes * For more information on interval format string see the `Cldr.Interval`. * The available predefined formats that can be applied are the keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` where `"en"` can be replaced by any configuration locale name and `:gregorian` is the underlying CLDR calendar type. * In the case where `from` and `to` are equal, a single date is formatted instead of an interval. ### Examples iex> use CalendarInterval iex> Cldr.DateTime.Interval.to_string! ~I"2020-01-01 00:00/10:00", MyApp.Cldr "Jan 1, 2020, 12:00:00 AM – 10:00:59 AM" """ @spec to_string!(CalendarInterval.t(), Cldr.backend(), Keyword.t()) :: String.t() | no_return def to_string!(%CalendarInterval{first: from, last: to, precision: precision}, backend, options) when precision in [:year, :month, :day] do Cldr.Date.Interval.to_string!(from, to, backend, options) end def to_string!(%CalendarInterval{first: from, last: to, precision: precision}, backend, options) when precision in [:hour, :minute, :second] do to_string!(from, to, backend, options) end end @doc """ Returns a localised string representing the formatted interval formed by two dates or raises an exception. ### Arguments * `from` is any map that conforms to the `Calendar.datetime` type. * `to` is any map that conforms to the `Calendar.datetime` type. `to` must occur on or after `from`. * `backend` is any module that includes `use Cldr` and is therefore a `Cldr` backend module. * `options` is a keyword list of options. The default is `[]`. ### Options * `:format` is one of `:short`, `:medium` or `:long` or a specific format type or a string representation of an interval format. The default is `:medium`. * `: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. * `: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 digits should be transliterated. * `:prefer` expresses the preference for one of the possible alternative sub-formats. See the variant preference notes below. ### 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.Date.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`. ### Returns * `string` or * raises an exception ### Notes * For more information on interval format string see the `Cldr.Interval`. * The available predefined formats that can be applied are the keys of the map returned by `Cldr.DateTime.Format.interval_formats("en", :gregorian)` where `"en"` can be replaced by any configuration locale name and `:gregorian` is the underlying CLDR calendar type. * In the case where `from` and `to` are equal, a single date is formatted instead of an interval ### Examples iex> Cldr.DateTime.Interval.to_string! ~U[2020-01-01 00:00:00.0Z], ...> ~U[2020-12-31 10:00:00.0Z], MyApp.Cldr "Jan 1, 2020, 12:00:00 AM – Dec 31, 2020, 10:00:00 AM" """ def to_string!(from, to, backend, options \\ []) do case to_string(from, to, backend, options) do {:ok, string} -> string {:error, {exception, reason}} -> raise exception, reason end end @doc """ Returns the format code representing the date or time unit that is the greatest difference between two date/times. ### Arguments * `from` is any `t:DateTime.t/0` or `t:NaiveDateTine.t/0` * `to` is any `t:DateTime.t/0` or `t:NaiveDateTine.t/0` ### Returns * `{:ok, format_code}` where `format_code` is one of * `:y` meaning that the greatest difference is in the year * `:M` meaning that the greatest difference is in the month * `:d` meaning that the greatest difference is in the day * `:H` meaning that the greatest difference is in the hour * `:m` meaning that the greatest difference is in the minute * `{:error, :no_practical_difference}` ### Example iex> Cldr.DateTime.Interval.greatest_difference ~U[2022-04-22 02:00:00.0Z], ~U[2022-04-22 03:00:00.0Z] {:ok, :H} iex> Cldr.DateTime.Interval.greatest_difference ~U[2022-04-22 02:00:00.0Z], ~U[2022-04-22 02:00:01.0Z] {:error, :no_practical_difference} """ def greatest_difference(from, to) do Cldr.Date.Interval.greatest_difference(from, to) end defp from_less_than_or_equal_to(%{time_zone: zone} = from, %{time_zone: zone} = to) do case DateTime.compare(from, to) do comp when comp in [:eq, :lt] -> {:ok, comp} _other -> {:error, Cldr.Date.Interval.datetime_order_error(from, to)} end end defp from_less_than_or_equal_to(%{time_zone: _zone1} = from, %{time_zone: _zone2} = to) do {:error, Cldr.Date.Interval.datetime_incompatible_timezone_error(from, to)} end defp from_less_than_or_equal_to(from, to) do case NaiveDateTime.compare(from, to) do comp when comp in [:eq, :lt] -> {:ok, comp} _other -> {:error, Cldr.Date.Interval.datetime_order_error(from, to)} end end defp normalize_options(from, 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) |> List.wrap() format = Keyword.get(options, :format, @default_format) options |> Map.new() |> Map.put(:format, format) |> Map.put(:locale, locale) |> Map.put(:number_system, number_system) |> Map.put(:backend, backend) |> Map.put(:calendar, from.calendar) |> Map.put(:prefer, prefer) end @doc false def adjust_options(options, locale, format, date_format \\ nil, time_format \\ nil) do options |> Map.put(:locale, locale) |> Map.put(:format, format) |> Map.put(:date_format, date_format) |> Map.put(:time_format, time_format) |> Map.delete(:style) end defp format_date_time(from, to, locale, backend, calendar, difference, options) do backend_format = Module.concat(backend, DateTime.Format) {:ok, calendar} = Cldr.DateTime.type_from_calendar(calendar) fallback = backend_format.date_time_interval_fallback(locale, calendar) format = Map.fetch!(options, :format) [from_format, to_format] = extract_format(format, difference, options) from_options = Map.put(options, :format, from_format) to_options = Map.put(options, :format, to_format) final_format = if is_atom(format), do: format, else: [from_format, to_format] do_format_date_time( from, to, backend, final_format, difference, from_options, to_options, fallback ) end # The difference is only in the time part defp do_format_date_time(from, to, backend, format, difference, from_opts, to_opts, fallback) when difference in [:H, :m] do with {:ok, from_string} <- Cldr.DateTime.to_string(from, backend, from_opts), {:ok, to_string} <- Cldr.Time.to_string(to, backend, to_opts) do {:ok, combine_result(from_string, to_string, format, fallback)} end end # The difference is in the date part # Format each datetime separately and join with # the interval fallback format defp do_format_date_time(from, to, backend, format, difference, from_opts, to_opts, fallback) when difference in [:y, :M, :d] do with {:ok, from_string} <- Cldr.DateTime.to_string(from, backend, from_opts), {:ok, to_string} <- Cldr.DateTime.to_string(to, backend, to_opts) do {:ok, combine_result(from_string, to_string, format, fallback)} end end defp combine_result(left, right, format, _fallback) when is_list(format) do left <> right end defp combine_result(left, right, format, fallback) when is_atom(format) do [left, right] |> Cldr.Substitution.substitute(fallback) |> Enum.join() end defp extract_format(format, _distance, options) when is_atom(format) do case options do %{time_format: nil, date_format: nil} -> [format, format] %{time_format: time_format, date_format: nil} -> [format, time_format] %{time_format: nil, date_format: date_format} -> [date_format, format] %{time_format: time_format, date_format: date_format} -> [date_format, time_format] end end defp extract_format(format, distance, options) when is_map(format) do format = Map.fetch!(format, distance) case format do %{default: default, variant: variant} -> if :variant in options.prefer, do: variant, else: default other -> other end end defp extract_format([from_format, to_format], _greatest_distance, _prefer) do [from_format, to_format] end # Using standard format terms like :short, :medium, :long # When there is no specific date or time format requested defp validate_format(format, nil, nil, _options) when format in @formats do {:ok, format, nil, nil} end # Using standard format terms like :short, :medium, :long # with date and time formats also of the same style defp validate_format(format, date_format, time_format, _options) when format in @formats and date_format in @formats and time_format in @formats do {:ok, format, date_format, time_format} end # Using standard format terms like :short, :medium, :long # with date format specified but time format # uses the interval format defp validate_format(format, date_format, nil, _options) when format in @formats and date_format in @formats do {:ok, format, date_format, format} end # Using standard format terms like :short, :medium, :long # with time format specified but date format # uses the interval format defp validate_format(format, nil, time_format, _options) when format in @formats and time_format in @formats do {:ok, format, format, time_format} end # Direct specification of a format as a string defp validate_format(format, nil, nil, _options) when is_binary(format) do with {:ok, format} <- Cldr.DateTime.Format.split_interval(format) do {:ok, format, nil, nil} end end # Direct specification of a format as a string defp validate_format(format, nil, nil, options) when is_atom(format) do alias Cldr.DateTime.Format locale = options.locale backend = options.backend {:ok, cldr_calendar} = Cldr.DateTime.type_from_calendar(options.calendar) with {:ok, interval_formats} <- Format.interval_formats(locale, cldr_calendar, backend) do if format = Map.get(interval_formats, format) do {:ok, format, nil, nil} else {:error, format_error(format, nil, nil)} end end end # If the format is binary then neither date or time format can be # specified. defp validate_format(format, date_format, time_format, _options) when is_binary(format) do {:error, format_error(format, date_format, time_format)} end defp validate_format(format, date_format, time_format, _options) do {:error, format_error(format, date_format, time_format)} end @doc false def format_error(format, date_format \\ nil, time_format \\ nil) def format_error(format, nil, nil) do { Cldr.DateTime.InvalidFormat, "The interval format #{inspect(format)} is invalid. " <> "Valid formats are #{inspect(@formats)} or an interval format string.}" } end def format_error(format, date_format, time_format) when format in @formats do { Cldr.DateTime.InvalidFormat, ":date_format and :time_format must be one of " <> inspect(@formats) <> " if :format is also one of #{inspect(@formats)}. Found #{inspect(date_format)} and #{inspect(time_format)}." } end def format_error(format, date_format, time_format) when is_binary(format) when not is_nil(date_format) and not is_nil(time_format) do { Cldr.DateTime.InvalidFormat, ":date_format and :time_format " <> error_string(format) <> "." } end def format_error(format, date_format, nil) when is_binary(format) and not is_nil(date_format) do { Cldr.DateTime.InvalidFormat, ":date_format " <> error_string(format) <> "." } end def format_error(format, nil, time_format) when is_binary(format) and not is_nil(time_format) do { Cldr.DateTime.InvalidFormat, ":time_format " <> error_string(format) <> "." } end defp error_string(format) do "cannot be specified when the interval format is a binary or atom other than one of #{inspect(@formats)}. Found: #{inspect(format)}" end end