defmodule Cldr.DateTime.Formatter do
  @moduledoc """
  Functions that implement the formatting for each specific
  format symbol.

  Each format symbol is an ASCII character in the
  range `a-zA-z`.  Although not all characters are used as
  format symbols, all characters are reserved for that use
  requiring that literals be enclosed in single quote
  characters, for example `'a literal'`.

  Variations of each format are defined by repeating the
  format symbol one or more times.  CLDR typically defines
  an `:abbreviated`, `:wide` and `:narrow` format that is
  reprented by a sequence of 3, 4 or 5 format symbols but
  this can vary depending on the format symbol.

  The [CLDR standard](http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)
  defines a wide range of format symbols.  Most - but not
  all - of these symbols are supported in `Cldr`.  The supported
  symbols are described below.

  ### Format Symbol Table

  | Element                | Symbol     | Example         | Cldr Format                        |
  | :--------------------  | :--------  | :-------------- | :--------------------------------- |
  | Era                    | G, GG, GGG | "AD"            | Abbreviated                        |
  |                        | GGGG       | "Anno Domini"   | Wide                               |
  |                        | GGGGG      | "A"             | Narrow                             |
  | Year                   | y          | 7               | Minimum necessary digits           |
  |                        | yy         | "17"            | Least significant 2 digits         |
  |                        | yyy        | "017", "2017"   | Padded to at least 3 digits        |
  |                        | yyyy       | "2017"          | Padded to at least 4 digits        |
  |                        | yyyyy      | "02017"         | Padded to at least 5 digits        |
  | ISOWeek Year           | Y          | 7               | Minimum necessary digits           |
  |                        | YY         | "17"            | Least significant 2 digits         |
  |                        | YYY        | "017", "2017"   | Padded to at least 3 digits        |
  |                        | YYYY       | "2017"          | Padded to at least 4 digits        |
  |                        | YYYYY      | "02017"         | Padded to at least 5 digits        |
  | Related Gregorian Year | r, rr, rr+ | 2017            | Minimum necessary digits           |
  | Cyclic Year            | U, UU, UUU | "甲子"           | Abbreviated                        |
  |                        | UUUU       | "甲子" (for now) | Wide                               |
  |                        | UUUUU      | "甲子" (for now) | Narrow                             |
  | Extended Year          | u+         | 4601            | Minimum necessary digits           |
  | Quarter                | Q          | 2               | Single digit                       |
  |                        | QQ         | "02"            | Two digits                         |
  |                        | QQQ        | "Q2"            | Abbreviated                        |
  |                        | QQQQ       | "2nd quarter"   | Wide                               |
  |                        | QQQQQ      | "2"             | Narrow                             |
  | Standalone Quarter     | q          | 2               | Single digit                       |
  |                        | qq         | "02"            | Two digits                         |
  |                        | qqq        | "Q2"            | Abbreviated                        |
  |                        | qqqq       | "2nd quarter"   | Wide                               |
  |                        | qqqqq      | "2"             | Narrow                             |
  | Month                  | M          | 9               | Single digit                       |
  |                        | MM         | "09"            | Two digits                         |
  |                        | MMM        | "Sep"           | Abbreviated                        |
  |                        | MMMM       | "September"     | Wide                               |
  |                        | MMMMM      | "S"             | Narrow                             |
  | Standalone Month       | L          | 9               | Single digit                       |
  |                        | LL         | "09"            | Two digits                         |
  |                        | LLL        | "Sep"           | Abbreviated                        |
  |                        | LLLL       | "September"     | Wide                               |
  |                        | LLLLL      | "S"             | Narrow                             |
  | Week of Year           | w          | 2, 22           | Single digit                       |
  |                        | ww         | 02, 22          | Two digits, zero padded            |
  | Week of Month          | W          | 2               | Single digit                       |
  | Day of Year            | D          | 3, 33, 333      | Minimum necessary digits           |
  |                        | DD         | 03, 33, 333     | Minimum of 2 digits, zero padded   |
  |                        | DDD        | 003, 033, 333   | Minimum of 3 digits, zero padded   |
  | Day of Month           | d          | 2, 22           | Minimum necessary digits           |
  |                        | dd         | 02, 22          | Two digits, zero padded            |
  |                        | ddd        | 2nd, 25th       | Ordinal day of month               |
  | Day of Week            | E, EE, EEE | "Tue"           | Abbreviated                        |
  |                        | EEEE       | "Tuesday"       | Wide                               |
  |                        | EEEEE      | "T"             | Narrow                             |
  |                        | EEEEEE     | "Tu"            | Short                              |
  |                        | e          | 2               | Single digit                       |
  |                        | ee         | "02"            | Two digits                         |
  |                        | eee        | "Tue"           | Abbreviated                        |
  |                        | eeee       | "Tuesday"       | Wide                               |
  |                        | eeeee      | "T"             | Narrow                             |
  |                        | eeeeee     | "Tu"            | Short                              |
  | Standalone Day of Week | c, cc      | 2               | Single digit                       |
  |                        | ccc        | "Tue"           | Abbreviated                        |
  |                        | cccc       | "Tuesday"       | Wide                               |
  |                        | ccccc      | "T"             | Narrow                             |
  |                        | cccccc     | "Tu"            | Short                              |
  | AM or PM               | a, aa, aaa | "am."           | Abbreviated                        |
  |                        | aaaa       | "am."           | Wide                               |
  |                        | aaaaa      | "am"            | Narrow                             |
  | Noon, Mid, AM, PM      | b, bb, bbb | "mid."          | Abbreviated                        |
  |                        | bbbb       | "midnight"      | Wide                               |
  |                        | bbbbb      | "md"            | Narrow                             |
  | Flexible time period   | B, BB, BBB | "at night"      | Abbreviated                        |
  |                        | BBBB       | "at night"      | Wide                               |
  |                        | BBBBB      | "at night"      | Narrow                             |
  | Hour                   | h, K, H, k |                 | See the table below                |
  | Minute                 | m          | 3, 10           | Minimum digits of minutes          |
  |                        | mm         | "03", "12"      | Two digits, zero padded            |
  | Second                 | s          | 3, 48           | Minimum digits of seconds          |
  |                        | ss         | "03", "48"      | Two digits, zero padded            |
  | Fractional Seconds     | S          | 3, 48           | Minimum digits of fractional seconds |
  |                        | SS         | "03", "48"      | Two digits, zero padded            |
  | Milliseconds            | A+         | 4000, 63241     | Minimum digits of milliseconds since midnight |
  | Generic non-location TZ | v         | "Etc/UTC"       | `:time_zone` key, unlocalised      |
  |                         | vvvv      | "unk"           | Generic timezone name.  Currently returns only "unk" |
  | Specific non-location TZ | z..zzz   | "UTC"           | `:zone_abbr` key, unlocalised      |
  |                         | zzzz      | "GMT"           | Delegates to `zone_gmt/4`          |
  | Timezone ID             | V         | "unk"           | `:zone_abbr` key, unlocalised      |
  |                         | VV        | "Etc/UTC        | Delegates to `zone_gmt/4`          |
  |                         | VVV       | "Unknown City"  | Exemplar city.  Not supported.     |
  |                         | VVVV      | "GMT"           | Delegates to `zone_gmt/4`          |
  | ISO8601 Format          | Z..ZZZ    | "+0100"         | ISO8601 Basic Format with hours and minutes |
  |                         | ZZZZ      | "+01:00"        | Delegates to `zone_gmt/4`          |
  |                         | ZZZZZ     | "+01:00:10"     | ISO8601 Extended format with optional seconds |
  | ISO8601 plus Z          | X         | "+01"           | ISO8601 Basic Format with hours and optional minutes or "Z" |
  |                         | XX        | "+0100"         | ISO8601 Basic Format with hours and minutes or "Z"          |
  |                         | XXX       | "+0100"         | ISO8601 Basic Format with hours and minutes, optional seconds or "Z" |
  |                         | XXXX      | "+010059"       | ISO8601 Basic Format with hours and minutes, optional seconds or "Z" |
  |                         | XXXXX     | "+01:00:10"     | ISO8601 Extended Format with hours and minutes, optional seconds or "Z" |
  | ISO8601 minus Z         | x         | "+0100"         | ISO8601 Basic Format with hours and optional minutes |
  |                         | xx        | "-0800"         | ISO8601 Basic Format with hours and minutes          |
  |                         | xxx       | "+01:00"        | ISO8601 Extended Format with hours and minutes       |
  |                         | xxxx      | "+010059"       | ISO8601 Basic Format with hours and minutes, optional seconds     |
  |                         | xxxxx     | "+01:00:10"     | ISO8601 Extended Format with hours and minutes, optional seconds  |
  | GMT Format              | O         | "+0100"         | Short localised GMT format        |
  |                         | OOOO      | "+010059"       | Long localised GMT format         |

  ### Formatting symbols for hour of day

  The hour of day can be formatted differently depending whether
  a 12- or 24-hour day is being represented and depending on the
  way in which midnight and noon are represented.  The following
  table illustrates the differences:

  | Symbol  | Midn.	|	Morning	| Noon |	Afternoon	| Midn. |
  | :----:  | :---: | :-----: | :--: | :--------: | :---: |
  |   h	    |  12	  | 1...11	|  12	 |   1...11   |  12   |
  |   K	    |   0	  | 1...11	|   0	 |   1...11   |   0   |
  |   H	    |   0	  | 1...11	|  12	 |  13...23   |   0   |
  |   k	    |  24	  | 1...11	|  12	 |  13...23   |  24   |

  ### Fractional Seconds

  The fractional seconds formatting code (`S`) formats *only*
  the fraction of a second as represented by the `t:Time.t/0`
  type which is a tuple of the form `{microseconds, precision}`.

  If used standalone, `S` will be formatted as only the
  fractional digits. For example:

      iex> t = ~T[10:00:07.002]
      ~T[10:00:07.002]
      iex> Cldr.Time.to_string(t, format: "SSS")
      {:ok, "002"}

  If, however, fractional seconds (format code `S`) are formatted
  immediately after seconds (format code `s`) then a decimal
  separator is inserted between the `s` and `S`. The separator
  will be that appropriate for the designated locale, number system
  and selected separators.  For example:

      iex> t = ~T[10:00:07.002]
      ~T[10:00:07.002]
      iex> Cldr.Time.to_string t, format: "ssSSS"
      {:ok, "07.002"}

  Lastly, the number of fractional seconds is truncated
  to the count of the number of `S` format characters. And, if
  required, will be zero filled to that width.

      iex> t = ~T[10:00:07.002]
      ~T[10:00:07.002]
      iex> Cldr.Time.to_string t, format: "ssS"
      {:ok, "07.0"}
      iex> Cldr.Time.to_string t, format: "ssSS"
      {:ok, "07.00"}
      iex> Cldr.Time.to_string t, format: "ssSSS"
      {:ok, "07.002"}
      iex> Cldr.Time.to_string t, format: "ssSSSS"
      {:ok, "07.0020"}

  """

  alias Cldr.DateTime.Timezone
  alias Cldr.Calendar.Gregorian
  alias Cldr.Locale

  @duration_modules [Cldr.Calendar.Duration, Duration]
  @default_format 1
  @default_separators :standard

  @doc false
  defguard is_date(date)
           when is_map_key(date, :year) and is_map_key(date, :month) and is_map_key(date, :day)

  defguard is_time(time)
           when is_map_key(time, :hour) and is_map_key(time, :minute) and is_map_key(time, :second)

  defguard is_date_time(datetime)
           when is_date(datetime) and is_time(datetime)

  defguard has_year_and_month(date)
           when is_map_key(date, :year) and is_map_key(date, :month)

  @doc """
  Returns a formatted date.

  DateTime formats are defined in CLDR using substitution rules whereby
  the Date and/or Time are substituted into a format string.  Therefore
  this function crafts a date format string which is then inserted into
  the overall format being requested.

  """
  @spec date(Calendar.date(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()} | {:error, {module, String.t()}}

  def date(date, n \\ @default_format, options \\ [])

  def date(date, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    date(date, @default_format, locale, backend, options)
  end

  def date(date, n, options) do
    {locale, backend} = extract_locale!(options)
    date(date, n, locale, backend, options)
  end

  @spec date(
          Calendar.date(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          Keyword.t() | map()
        ) ::
          String.t() | {:error, {module(), String.t()}}

  def date(date, _n, _locale, backend, options) when is_list(options) do
    with {:ok, date_string} <- Cldr.Date.to_string(date, backend, options) do
      date_string
    end
  end

  def date(date, n, locale, backend, options) when is_map(options) do
    date(date, n, locale, backend, Map.to_list(options))
  end

  @doc """
  Returns a formatted time.

  DateTime formats are defined in CLDR using substitution rules whereby
  the Date and/or Time are substituted into a format string.  Therefore
  this function crafts a time format string which is then inserted into
  the overall format being requested.

  """
  @spec time(Cldr.Calendar.time(), integer, Keyword.t()) ::
          String.t() | {:error, {module(), String.t()}}

  def time(time, n \\ @default_format, options \\ [])

  def time(time, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    time(time, @default_format, locale, backend, options)
  end

  def time(time, n, options) do
    {locale, backend} = extract_locale!(options)
    time(time, n, locale, backend, options)
  end

  @spec time(
          Cldr.Calendar.time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          Keyword.t() | map()
        ) ::
          String.t() | {:error, String.t()} | {:error, {module, String.t()}}

  def time(time, _n, _locale, backend, options) when is_list(options) do
    with {:ok, time_string} <- Cldr.Time.to_string(time, backend, options) do
      time_string
    end
  end

  def time(time, n, locale, backend, options) when is_map(options) do
    time(time, n, locale, backend, Map.to_list(options))
  end

  @doc """
  Returns the `era` (format symbol `G`) of a date
  for given locale.

  ## Arguments

  * `date` is a `Date` struct or any map that contains at least the
    keys `:month` and `:calendar`

  * `n` in an integer between 1 and 5 that determines the format of
    the year

  * `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`

  * `options` is a `Keyword` list of options.  The only applicable
    option is `:era` with a value of either `nil` (the default) or
    `:variant` which will return the variant form of an era if one
    is available.

  ## Format Symbol

  The representation of the era is made in accordance
  with the following table:

  | Symbol     | Example         | Cldr Format                 |
  | :--------  | :-------------- | :---------------------------|
  | G, GG, GGG | "AD"            | Abbreviated                 |
  | GGGG       | "Anno Domini    | Wide                        |
  | GGGGG      | "A"             | Narrow                      |

  ## Examples

      iex> Cldr.DateTime.Formatter.era(~D[2017-12-01], 1)
      "AD"

      iex> Cldr.DateTime.Formatter.era(~D[2017-12-01], 4, "fr", MyApp.Cldr)
      "après Jésus-Christ"

      iex> Cldr.DateTime.Formatter.era(~D[2017-12-01], 4, "fr", MyApp.Cldr, era: :variant)
      "de l’ère commune"

  """
  @spec era(Cldr.Calendar.any_date_time(), pos_integer(), Keyword.t()) ::
          String.t() | {:error, String.t()}

  def era(date, n \\ @default_format, options \\ [])

  def era(date, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    era(date, @default_format, locale, backend, Map.new(options))
  end

  def era(date, n, options) do
    {locale, backend} = extract_locale!(options)
    era(date, n, locale, backend, Map.new(options))
  end

  @spec era(
          Cldr.Calendar.any_date_time(),
          pos_integer(),
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def era(date, n, locale, backend, options \\ %{})

  def era(%{calendar: Calendar.ISO} = date, n, locale, backend, options) do
    %{date | calendar: Cldr.Calendar.Gregorian}
    |> era(n, locale, backend, options)
  end

  def era(date, n, locale, backend, options) when is_date(date) and n in 1..3 do
    Cldr.Calendar.localize(date, :era, :format, :abbreviated, backend, locale, options)
    |> maybe_wrap(:era, options)
  end

  def era(date, 4, locale, backend, options) when is_date(date) do
    Cldr.Calendar.localize(date, :era, :format, :wide, backend, locale, options)
    |> maybe_wrap(:era, options)
  end

  def era(date, 5, locale, backend, options) when is_date(date) do
    Cldr.Calendar.localize(date, :era, :format, :narrow, backend, locale, options)
    |> maybe_wrap(:era, options)
  end

  def era(date, _n, _locale, _backend, _options) do
    error_return(date, "G", [:year, :month, :day, :calendar])
  end

  @doc """
  Returns the `year` (format symbol `y`) of a date
  as an integer. The `y` format returns the year
  as a simple integer in string format.

  The format `yy` is a special case which requests just
  the two low-order digits of the year, zero-padded
  as necessary. For most use cases, `y` or `yy` should
  be adequate.

  ## Arguments

  * `date` is a `Date` struct or any map that contains at least the
    keys `:month` and `:calendar`

  * `n` in an integer between 1 and 5 that determines the format of
    the year

  * `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`

  * `options` is a `Keyword` list of options.  There are no options
    used in `year/4`

  ## Format Symbol

  The representation of the quarter is made in accordance
  with the following table:

  | Symbol     | Example         | Cldr Format                 |
  | :--------  | :-------------- | :---------------------------|
  | y          | 7               | Minimum necessary digits    |
  | yy         | "17"            | Least significant 2 digits  |
  | yyy        | "017", "2017"   | Padded to at least 3 digits |
  | yyyy       | "2017"          | Padded to at least 4 digits |
  | yyyyy      | "02017"         | Padded to at least 5 digits |

  In most cases the length of the `y` field specifies
  the minimum number of   digits to display, zero-padded
  as necessary; more digits will be displayed if needed
  to show the full year.

  ## Examples

      iex> Cldr.DateTime.Formatter.year %{year: 2017, calendar: Calendar.ISO}, 1
      2017

      iex> Cldr.DateTime.Formatter.year %{year: 2017, calendar: Calendar.ISO}, 2
      "17"

      iex> Cldr.DateTime.Formatter.year %{year: 2017, calendar: Calendar.ISO}, 3
      "2017"

      iex> Cldr.DateTime.Formatter.year %{year: 2017, calendar: Calendar.ISO}, 4
      "2017"

      iex> Cldr.DateTime.Formatter.year %{year: 2017, calendar: Calendar.ISO}, 5
      "02017"

  """
  @spec year(Cldr.Calendar.any_date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def year(year, n \\ @default_format, options \\ [])

  def year(year, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    year(year, @default_format, locale, backend, Map.new(options))
  end

  def year(year, n, options) do
    {locale, backend} = extract_locale!(options)
    year(year, n, locale, backend, Map.new(options))
  end

  @spec year(
          Cldr.Calendar.any_date_time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def year(date, n, locale, backend, options \\ %{})

  def year(%{calendar: Calendar.ISO} = date, n, locale, backend, options) do
    %{date | calendar: Cldr.Calendar.Gregorian}
    |> year(n, locale, backend, options)
  end

  def year(%{year: _year} = date, 1, _locale, backend, options) do
    date
    |> Cldr.Calendar.calendar_year()
    |> transliterate("y", backend, options)
    |> maybe_wrap(:year, options)
  end

  def year(%{year: _year} = date, 2 = n, _locale, backend, options) do
    date
    |> Cldr.Calendar.calendar_year()
    |> rem(100)
    |> transliterate("y", backend, options)
    |> pad(n)
    |> maybe_wrap(:year, options)
  end

  def year(%{year: _year} = date, n, _locale, backend, options) do
    date
    |> Cldr.Calendar.calendar_year()
    |> transliterate("y", backend, options)
    |> pad(n)
    |> maybe_wrap(:year, options)
  end

  def year(date, _n, _locale, _backend, _options) do
    error_return(date, "y", [:year])
  end

  @doc """
  Returns the `year` (format symbol `Y`) in “Week of Year”
  based calendars in which the year transition occurs
  on a week boundary.

  ## Arguments

  * `date` is a `Date` struct or any map that contains at least the
    keys `:month` and `:calendar`

  * `n` in an integer between 1 and 5 that determines the format of
    the year

  * `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`

  * `options` is a `Keyword` list of options.  There are no options
    used in `weeK_aligned_year/4`

  ## Format Symbol

  The representation of the year is made in accordance
  with the following table:

  | Symbol     | Example         | Cldr Format                 |
  | :--------  | :-------------- | :---------------------------|
  | Y          | 7               | Minimum necessary digits    |
  | YY         | "17"            | Least significant 2 digits  |
  | YYY        | "017", "2017"   | Padded to at least 3 digits |
  | YYYY       | "2017"          | Padded to at least 4 digits |
  | YYYYY      | "02017"         | Padded to at least 5 digits |

  The result may differ from calendar year ‘y’ near
  a year transition. This numeric year designation
  is used in conjunction with pattern character ‘w’
  in the ISO year-week calendar as defined
  by ISO 8601, but can be used in non-Gregorian based
  calendar systems where week date processing is desired.

  The field length is interpreted in the same was as for
  `y`; that is, `yy` specifies use of the two low-order
  year digits, while any other field length specifies a
  minimum number of digits to display.

  ## Examples

      iex> Cldr.DateTime.Formatter.week_aligned_year ~D[2017-01-04], 1
      "2017"

      iex> Cldr.DateTime.Formatter.week_aligned_year ~D[2017-01-04], 2
      "17"

      iex> Cldr.DateTime.Formatter.week_aligned_year ~D[2017-01-04], 3
      "2017"

      iex> Cldr.DateTime.Formatter.week_aligned_year ~D[2017-01-04], 4
      "2017"

      iex> Cldr.DateTime.Formatter.week_aligned_year ~D[2017-01-04], 5
      "02017"

  """
  @spec week_aligned_year(Cldr.Calendar.any_date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def week_aligned_year(week_aligned_year, n \\ @default_format, options \\ [])

  def week_aligned_year(week_aligned_year, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    week_aligned_year(week_aligned_year, @default_format, locale, backend, Map.new(options))
  end

  def week_aligned_year(week_aligned_year, n, options) do
    {locale, backend} = extract_locale!(options)
    week_aligned_year(week_aligned_year, n, locale, backend, Map.new(options))
  end

  @spec week_aligned_year(
          Cldr.Calendar.date(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def week_aligned_year(date, n, locale, backend, options \\ %{})

  def week_aligned_year(%{calendar: Calendar.ISO} = date, n, locale, backend, options) do
    %{date | calendar: Cldr.Calendar.Gregorian}
    |> week_aligned_year(n, locale, backend, options)
  end

  def week_aligned_year(date, 1, _locale, _backend, options) when is_date(date) do
    {year, _week} = Cldr.Calendar.week_of_year(date)

    to_string(year)
    |> maybe_wrap(:week_aligned_year, options)
  end

  def week_aligned_year(date, 2 = n, _locale, _backend, options) when is_date(date) do
    {year, _week} = Cldr.Calendar.week_of_year(date)

    year
    |> rem(100)
    |> pad(n)
    |> maybe_wrap(:week_aligned_year, options)
  end

  def week_aligned_year(date, n, _locale, _backend, options) when is_date(date) and n in 3..5 do
    {year, _week} = Cldr.Calendar.week_of_year(date)

    pad(year, n)
    |> maybe_wrap(:week_aligned_year, options)
  end

  def week_aligned_year(date, _n, _locale, _backend, _options) do
    error_return(date, "Y", [:year, :month, :day, :calendar])
  end

  @doc """
  Returns the Extended year (format symbol `u`).

  ## Arguments

  * `date` is a `Date` struct or any map that contains at least the
    keys `:month` and `:calendar`

  * `n` in an integer between 1 and 5 that determines the format of
    the year

  * `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`

  * `options` is a `Keyword` list of options.  There are no options
    used in `weeK_aligned_year/4`

  **NOTE: This current implementation always returns
  the year provided in the supplied date.  This means
  `u` returns the same result as the format `y`.**

  ## Format Symbol

  | Symbol     | Example         | Cldr Format               |
  | :--------  | :-------------- | :------------------------ |
  | u+         | 4601            | Minimum necessary digits  |

  This is a single number designating the year of this
  calendar system, encompassing all supra-year fields.

  For example, for the Julian calendar system, year
  numbers are positive, with an era of BCE or CE. An
  extended year value for the Julian calendar system
  assigns positive values to CE years and negative
  values to BCE years, with 1 BCE being year 0.

  For `u`, all field lengths specify a minimum number of
  digits; there is no special interpretation for `uu`.

  """
  @spec extended_year(Cldr.Calendar.any_date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def extended_year(extended_year, n \\ @default_format, options \\ [])

  def extended_year(extended_year, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    extended_year(extended_year, @default_format, locale, backend, Map.new(options))
  end

  def extended_year(extended_year, n, options) do
    {locale, backend} = extract_locale!(options)
    extended_year(extended_year, n, locale, backend, Map.new(options))
  end

  @spec extended_year(Calendar.date(), integer, Locale.locale_reference(), Cldr.backend(), map()) ::
          String.t() | {:error, String.t()}

  def extended_year(date, n, locale, backend, options \\ %{})

  def extended_year(%{calendar: Calendar.ISO} = date, n, locale, backend, options) do
    %{date | calendar: Cldr.Calendar.Gregorian}
    |> extended_year(n, locale, backend, options)
  end

  def extended_year(%{year: year, calendar: Calendar.ISO}, n, _locale, _backend, options) do
    year
    |> pad(n)
    |> maybe_wrap(:extended_year, options)
  end

  def extended_year(%{year: year}, n, _locale, _backend, options) do
    year
    |> pad(n)
    |> maybe_wrap(:extended_year, options)
  end

  def extended_year(date, _n, _locale, _backend, _options) do
    error_return(date, "u", [:year, :calendar])
  end

  @doc """
  Returns the cyclic year (format symbol `U`) name for
  non-gregorian calendars.

  **NOTE: In the current implementation, the cyclic year is
  delegated to `Cldr.DateTime.Formatter.year/3`
  (format symbol `y`) and does not return a localized
  cyclic year.**

  ## Format Symbol

  | Symbol     | Example         | Cldr Format     |
  | :--------  | :-------------- | :-------------- |
  | U, UU, UUU | "甲子"           | Abbreviated     |
  | UUUU       | "甲子" (for now) | Wide            |
  | UUUUU      | "甲子" (for now) | Narrow          |

  Calendars such as the Chinese lunar
  calendar (and related calendars) and the Hindu calendars
  use 60-year cycles of year names. If the calendar does
  not provide cyclic year name data, or if the year value
  to be formatted is out of the range of years for which
  cyclic name data is provided, then numeric formatting
  is used (behaves like format symbol `y`).

  Currently the CLDR data only provides abbreviated names,
  which will be used for all requested name widths.

  """
  @spec cyclic_year(Cldr.Calendar.any_date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def cyclic_year(cyclic_year, n \\ @default_format, options \\ [])

  def cyclic_year(cyclic_year, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    cyclic_year(cyclic_year, @default_format, locale, backend, Map.new(options))
  end

  def cyclic_year(cyclic_year, n, options) do
    {locale, backend} = extract_locale!(options)
    cyclic_year(cyclic_year, n, locale, backend, Map.new(options))
  end

  @spec cyclic_year(
          Cldr.Calendar.any_date_time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def cyclic_year(date, n, locale, backend, options \\ %{})

  def cyclic_year(%{calendar: Calendar.ISO} = date, n, locale, backend, options) do
    %{date | calendar: Cldr.Calendar.Gregorian}
    |> cyclic_year(n, locale, backend, options)
  end

  def cyclic_year(%{year: year}, _n, _locale, _backend, options) do
    maybe_wrap(year, :cyclic_year, options)
  end

  def cyclic_year(date, _n, _locale, _backend, _options) do
    error_return(date, "U", [:year])
  end

  @doc """
  Returns the related gregorian year (format symbol `r`)
  of a date for given locale.

  ## Arguments

  * `date` is a `Date` struct or any map that contains at least the
    keys `:month` and `:calendar`

  * `n` in an integer between 1 and 5 that determines the format of
    the quarter

  * `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`

  * `options` is a `Keyword` list of options.  There are no options
    used in `related_year/4`

  ## Format Symbol

  The representation of the related year is made in accordance
  with the following table:

  | Symbol     | Example         | Cldr Format     |
  | :--------  | :-------------- | :-------------- |
  | r+         | 2017            |                 |

  This corresponds to the extended Gregorian year
  in which the calendar’s year begins. Related
  Gregorian years are often displayed, for example,
  when formatting dates in the Japanese calendar —
  e.g. “2012(平成24)年1月15日” — or in the Chinese
  calendar — e.g. “2012壬辰年腊月初四”. The related
  Gregorian year is usually displayed using the
  ":latn" numbering system, regardless of what
  numbering systems may be used for other parts
  of the formatted date.

  If the calendar’s year is linked to the solar
  year (perhaps using leap months), then for that
  calendar the ‘r’ year will always be at a fixed
  offset from the ‘u’ year.

  For the Gregorian calendar, the ‘r’ year
  is the same as the ‘u’ year. For ‘r’, all field
  lengths specify a minimum number of digits; there
  is no special interpretation for “rr”.

  """
  @spec related_year(Cldr.Calendar.any_date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def related_year(related_year, n \\ @default_format, options \\ [])

  def related_year(related_year, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    related_year(related_year, @default_format, locale, backend, Map.new(options))
  end

  def related_year(related_year, n, options) do
    {locale, backend} = extract_locale!(options)
    related_year(related_year, n, locale, backend, Map.new(options))
  end

  @spec related_year(
          Cldr.Calendar.date(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def related_year(date, n, locale, backend, options \\ %{})

  def related_year(%{calendar: Calendar.ISO} = date, n, locale, backend, options) do
    %{date | calendar: Cldr.Calendar.Gregorian}
    |> related_year(n, locale, backend, options)
  end

  def related_year(%{year: year, calendar: Gregorian}, n, _locale, _backend, options)
      when n in 1..5 do
    maybe_wrap(year, :related_year, options)
  end

  def related_year(date, n, _locale, _backend, options) when n in 1..5 do
    date
    |> Date.convert!(Gregorian)
    |> Map.get(:year)
    |> maybe_wrap(:related_year, options)
  end

  def related_year(date, _n, _locale, _backend, _options) do
    error_return(date, "r", [:year, :calendar])
  end

  @doc """
  Returns the `quarter` (format symbol `Q`) of a date
  for given locale.

  ## Arguments

  * `date` is a `Date` struct or any map that contains at least the
    keys `:month` and `:calendar`

  * `n` in an integer between 1 and 5 that determines the format of
    the quarter

  * `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`

  * `options` is a `Keyword` list of options.  There are no options
    used in `quarter/5`

  ## Format Symbol

  The representation of the quarter is made in accordance
  with the following table:

  | Symbol     | Example         | Cldr Format     |
  | :--------  | :-------------- | :-------------- |
  | Q          | 2               | Single digit    |
  | QQ         | "02"            | Two digits      |
  | QQQ        | "Q2"            | Abbreviated     |
  | QQQQ       | "2nd quarter"   | Wide            |
  | QQQQQ      | "2"             | Narrow          |

  ## Examples

      iex> Cldr.DateTime.Formatter.quarter ~D[2017-04-01], 1
      2

      iex> Cldr.DateTime.Formatter.quarter ~D[2017-04-01], 2
      "02"

      iex> Cldr.DateTime.Formatter.quarter ~D[2017-04-01], 3
      "Q2"

      iex> Cldr.DateTime.Formatter.quarter ~D[2017-04-01], 4
      "2nd quarter"

      iex> Cldr.DateTime.Formatter.quarter ~D[2017-04-01], 5
      "2"

  """
  @spec quarter(Cldr.Calendar.any_date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def quarter(quarter, n \\ @default_format, options \\ [])

  def quarter(quarter, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    quarter(quarter, @default_format, locale, backend, Map.new(options))
  end

  def quarter(quarter, n, options) do
    {locale, backend} = extract_locale!(options)
    quarter(quarter, n, locale, backend, Map.new(options))
  end

  @spec quarter(
          Cldr.Calendar.any_date_time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def quarter(date, n, locale, backend, options \\ %{})

  def quarter(%{calendar: Calendar.ISO} = date, n, locale, backend, options)
      when has_year_and_month(date) do
    %{date | calendar: Cldr.Calendar.Gregorian}
    |> quarter(n, locale, backend, options)
  end

  def quarter(date, 1, _locale, _backend, options) when has_year_and_month(date) do
    date
    |> Cldr.Calendar.quarter_of_year()
    |> maybe_wrap(:quarter, options)
  end

  def quarter(date, 2, _locale, _backend, options) when has_year_and_month(date) do
    date
    |> Cldr.Calendar.quarter_of_year()
    |> pad(2)
    |> maybe_wrap(:quarter, options)
  end

  def quarter(date, 3, locale, backend, options) when has_year_and_month(date) do
    date
    |> Cldr.Calendar.localize(:quarter, :format, :abbreviated, backend, locale)
    |> maybe_wrap(:quarter, options)
  end

  def quarter(date, 4, locale, backend, options) when has_year_and_month(date) do
    date
    |> Cldr.Calendar.localize(:quarter, :format, :wide, backend, locale)
    |> maybe_wrap(:quarter, options)
  end

  def quarter(date, 5, locale, backend, options) when has_year_and_month(date) do
    date
    |> Cldr.Calendar.localize(:quarter, :format, :narrow, backend, locale)
    |> maybe_wrap(:quarter, options)
  end

  def quarter(date, _n, _locale, _backend, _options) do
    error_return(date, "Q", [:month])
  end

  @doc """
  Returns the standalone `quarter` (format symbol `a`) of a date
  for given locale.

  ## Arguments

  * `date` is a `Date` struct or any map that contains at least the
    keys `:month` and `:calendar`

  * `n` in an integer between 1 and 5 that determines the format of
    the quarter

  * `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`

  * `options` is a `Keyword` list of options.  There are no options
    used in `standalone_quarter/5`

  ## Format Symbol

  The representation of the quarter is made in accordance
  with the following table:

  | Symbol     | Example         | Cldr Format     |
  | :--------  | :-------------- | :-------------- |
  | q          | 2               | Single digit    |
  | qq         | "02"            | Two digits      |
  | qqq        | "Q2"            | Abbreviated     |
  | qqqq       | "2nd quarter"   | Wide            |
  | qqqqq      | "2"             | Narrow          |

  ## Examples

      iex> Cldr.DateTime.Formatter.standalone_quarter ~D[2019-06-08], 1
      2

      iex> Cldr.DateTime.Formatter.standalone_quarter ~D[2019-06-08], 2
      "02"

      iex> Cldr.DateTime.Formatter.standalone_quarter ~D[2019-06-08], 3
      "Q2"

      iex> Cldr.DateTime.Formatter.standalone_quarter ~D[2019-06-08], 4
      "2nd quarter"

      iex> Cldr.DateTime.Formatter.standalone_quarter ~D[2019-06-08], 5
      "2"

  """
  @spec standalone_quarter(Cldr.Calendar.any_date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def standalone_quarter(standalone_quarter, n \\ @default_format, options \\ [])

  def standalone_quarter(standalone_quarter, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    standalone_quarter(standalone_quarter, @default_format, locale, backend, Map.new(options))
  end

  def standalone_quarter(standalone_quarter, n, options) do
    {locale, backend} = extract_locale!(options)
    standalone_quarter(standalone_quarter, n, locale, backend, Map.new(options))
  end

  @spec standalone_quarter(
          Cldr.Calendar.date(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def standalone_quarter(date, n, locale, backend, options \\ %{})

  def standalone_quarter(%{calendar: Calendar.ISO} = date, n, locale, backend, options)
      when has_year_and_month(date) do
    %{date | calendar: Cldr.Calendar.Gregorian}
    |> standalone_quarter(n, locale, backend, options)
  end

  def standalone_quarter(date, 1, _locale, _backend, options) when has_year_and_month(date) do
    date
    |> Cldr.Calendar.quarter_of_year()
    |> maybe_wrap(:standalone_quarter, options)
  end

  def standalone_quarter(date, 2, _locale, _backend, options) when has_year_and_month(date) do
    date
    |> Cldr.Calendar.quarter_of_year()
    |> pad(2)
    |> maybe_wrap(:standalone_quarter, options)
  end

  def standalone_quarter(date, 3, locale, backend, options) when has_year_and_month(date) do
    date
    |> Cldr.Calendar.localize(:quarter, :stand_alone, :abbreviated, backend, locale)
    |> maybe_wrap(:standalone_quarter, options)
  end

  def standalone_quarter(date, 4, locale, backend, options) when has_year_and_month(date) do
    date
    |> Cldr.Calendar.localize(:quarter, :stand_alone, :wide, backend, locale)
    |> maybe_wrap(:standalone_quarter, options)
  end

  def standalone_quarter(date, 5, locale, backend, options) when has_year_and_month(date) do
    date
    |> Cldr.Calendar.localize(:quarter, :stand_alone, :narrow, backend, locale)
    |> maybe_wrap(:standalone_quarter, options)
  end

  def standalone_quarter(date, _n, _locale, _backend, _options) do
    error_return(date, "q", [:month])
  end

  @doc """
  Returns the `month` (format symbol `M`) of a date
  for given locale.

  ## Arguments

  * `date` is a `Date` struct or any map that contains at least the
    keys `:month` and `:calendar`

  * `n` in an integer between 1 and 5 that determines the format of
    the month

  * `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`

  * `options` is a `Keyword` list of options.  There are no options
    used in `month/4`

  ## Format Symbol

  The representation of the month is made in accordance
  with the following table:

  | Symbol     | Example         | Cldr Format     |
  | :--------  | :-------------- | :-------------- |
  | M          | 9               | Single digit    |
  | MM         | "09"            | Two digits      |
  | MMM        | "Sep"           | Abbreviated     |
  | MMMM       | "September"     | Wide            |
  | MMMMM      | "S"             | Narrow          |

  ## Examples

      iex> Cldr.DateTime.Formatter.month ~D[2019-09-08]
      "9"

      iex> Cldr.DateTime.Formatter.month ~D[2019-09-08], 2
      "09"

      iex> Cldr.DateTime.Formatter.month ~D[2019-09-08], 3
      "Sep"

      iex> Cldr.DateTime.Formatter.month ~D[2019-09-08], 4
      "September"

      iex> Cldr.DateTime.Formatter.month ~D[2019-09-08], 5
      "S"

  """
  @spec month(Cldr.Calendar.any_date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def month(month, n \\ @default_format, options \\ [])

  def month(month, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    month(month, @default_format, locale, backend, Map.new(options))
  end

  def month(month, n, options) do
    {locale, backend} = extract_locale!(options)
    month(month, n, locale, backend, Map.new(options))
  end

  @spec month(
          Cldr.Calendar.any_date_time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def month(date, n, locale, backend, options \\ %{})

  def month(%{calendar: Calendar.ISO} = date, n, locale, backend, options) do
    %{date | calendar: Cldr.Calendar.Gregorian}
    |> month(n, locale, backend, options)
  end

  def month(%{month: _month} = date, n, locale, backend, options) when n in 1..2 do
    date
    |> Cldr.Calendar.localize(:month, :numeric, :any, backend, locale)
    |> pad(n)
    |> maybe_wrap(:month, options)
  end

  def month(%{month: _month} = date, 3, locale, backend, options) do
    date
    |> Cldr.Calendar.localize(:month, :format, :abbreviated, backend, locale)
    |> maybe_wrap(:month, options)
  end

  def month(%{month: _month} = date, 4, locale, backend, options) do
    date
    |> Cldr.Calendar.localize(:month, :format, :wide, backend, locale)
    |> maybe_wrap(:month, options)
  end

  def month(%{month: _month} = date, 5, locale, backend, options) do
    date
    |> Cldr.Calendar.localize(:month, :format, :narrow, backend, locale)
    |> maybe_wrap(:month, options)
  end

  def month(date, _n, _locale, _backend, _options) do
    error_return(date, "M", [:month])
  end

  @doc """
  Returns the `month` (symbol `L`) in standalone format which is
  intended to formatted without an accompanying day (`d`).

  ## Arguments

  * `date` is a `Date` struct or any map that contains at least the
    keys `:month` and `:calendar`

  * `n` in an integer between 1 and 5 that determines the format of
    the month

  * `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`

  * `options` is a `Keyword` list of options.  There are no options
    used in `standalone_month/4`

  ## Format Symbol

  The representation of the standalone month is made in accordance
  with the following table:

  | Symbol     | Example         | Cldr Format     |
  | :--------  | :-------------- | :-------------- |
  | L          | 9               | Single digit    |
  | LL         | "09"            | Two digits      |
  | LLL        | "Sep"           | Abbreviated     |
  | LLLL       | "September"     | Wide            |
  | LLLLL      | "S"             | Narrow          |

  ## Examples

      iex> Cldr.DateTime.Formatter.standalone_month ~D[2019-09-08]
      "9"

      iex> Cldr.DateTime.Formatter.standalone_month ~D[2019-09-08], 2
      "09"

      iex> Cldr.DateTime.Formatter.standalone_month ~D[2019-09-08], 3
      "Sep"

      iex> Cldr.DateTime.Formatter.standalone_month ~D[2019-09-08], 4
      "September"

      iex> Cldr.DateTime.Formatter.standalone_month ~D[2019-09-08], 5
      "S"

  """
  @spec standalone_month(Cldr.Calendar.any_date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def standalone_month(standalone_month, n \\ @default_format, options \\ [])

  def standalone_month(standalone_month, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    standalone_month(standalone_month, @default_format, locale, backend, Map.new(options))
  end

  def standalone_month(standalone_month, n, options) do
    {locale, backend} = extract_locale!(options)
    standalone_month(standalone_month, n, locale, backend, Map.new(options))
  end

  @spec standalone_month(
          Cldr.Calendar.date(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def standalone_month(date, n, locale, backend, options \\ %{})

  def standalone_month(%{calendar: Calendar.ISO} = date, n, locale, backend, options) do
    %{date | calendar: Cldr.Calendar.Gregorian}
    |> standalone_month(n, locale, backend, options)
  end

  def standalone_month(%{month: _month} = date, n, locale, backend, options) when n in 1..2 do
    date
    |> Cldr.Calendar.localize(:month, :numeric, :any, backend, locale)
    |> pad(n)
    |> maybe_wrap(:standalone_month, options)
  end

  def standalone_month(%{month: _month} = date, 3, locale, backend, options) do
    date
    |> Cldr.Calendar.localize(:month, :stand_alone, :abbreviated, backend, locale)
    |> maybe_wrap(:standalone_month, options)
  end

  def standalone_month(%{month: _month} = date, 4, locale, backend, options) do
    date
    |> Cldr.Calendar.localize(:month, :stand_alone, :wide, backend, locale)
    |> maybe_wrap(:standalone_month, options)
  end

  def standalone_month(%{month: _month} = date, 5, locale, backend, options) do
    date
    |> Cldr.Calendar.localize(:month, :stand_alone, :narrow, backend, locale)
    |> maybe_wrap(:standalone_month, options)
  end

  def standalone_month(date, _n, _locale, _backend, _options) do
    error_return(date, "L", [:year, :month, :day, :calendar])
  end

  @doc """
  Returns the week of the year (symbol `w`) as an integer.

  ## Arguments

  * `date` is a `Date` struct or any map that contains at least the
    keys `:year`, `:month`, `:day` and `:calendar`

  * `n` in an integer between 1 and 2 that determines the format of
    the week of the year

  * `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`

  * `options` is a `Keyword` list of options.  There are no options
    used in `week_of_year/4`

  ## Notes

  Determining the week of the year is influenced
  by two factors:

  1. The calendar in use.  For example the ISO calendar (which
  is the default calendar in Elixir) follows the ISO standard
  in which the first week of the year is the week containing
  the first thursday of the year.

  2. The territory in use.  For example, in the US the first
  week of the year is the week containing January 1st whereas
  many territories follow the ISO standard.

  ## Format Symbol

  The representation of the day of the year is made in accordance
  with the following table:

  | Symbol     | Example         | Cldr Format     |
  | :--------  | :-------------- | :-------------- |
  | w          | 2, 22           |                 |
  | ww         | 02, 22          |                 |

  ## Examples

      iex> import Cldr.Calendar.Sigils
      Cldr.Calendar.Sigils
      iex> Cldr.DateTime.Formatter.week_of_year ~D[2019-01-07], 1
      "2"
      iex> Cldr.DateTime.Formatter.week_of_year ~d[2019-W04-1], 2
      "04"

  """
  @spec week_of_year(Cldr.Calendar.any_date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def week_of_year(week_of_year, n \\ @default_format, options \\ [])

  def week_of_year(week_of_year, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    week_of_year(week_of_year, @default_format, locale, backend, Map.new(options))
  end

  def week_of_year(week_of_year, n, options) do
    {locale, backend} = extract_locale!(options)
    week_of_year(week_of_year, n, locale, backend, Map.new(options))
  end

  @spec week_of_year(
          Cldr.Calendar.date(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def week_of_year(date, n, locale, backend, options \\ %{})

  def week_of_year(%{calendar: Calendar.ISO} = date, n, locale, backend, options)
      when is_date(date) do
    %{date | calendar: Cldr.Calendar.Gregorian}
    |> week_of_year(n, locale, backend, options)
  end

  def week_of_year(date, n, _locale, _backend, options) when is_date(date) and n in 1..2 do
    {_year, week} = Cldr.Calendar.week_of_year(date)

    week
    |> pad(n)
    |> maybe_wrap(:week_of_year, options)
  end

  def week_of_year(date, _n, _locale, _backend, _options) do
    error_return(date, "w", [:year, :month, :day, :calendar])
  end

  @doc """
  Returns the week of the month (format symbol `W`) as an integer.

  ## Arguments

  * `date` is a `Date` struct or any map that contains at least the
    keys `:year`, `:month`, `:day` and `:calendar`

  * `n` in an integer between that should be between 1 and 4 that
    determines the format of the week of the month

  * `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`

  * `options` is a `Keyword` list of options.  There are no options
    used in `week_of_month/4`

  ## Format Symbol

  The representation of the week of the month is made in accordance
  with the following table:

  | Symbol     | Example         | Cldr Format     |
  | :--------  | :-------------- | :-------------- |
  | W          | 2               |                 |

  ## Examples

      iex> import Cldr.Calendar.Sigils
      Cldr.Calendar.Sigils
      iex> Cldr.DateTime.Formatter.week_of_month ~D[2019-01-07], 1
      "2"
      iex> Cldr.DateTime.Formatter.week_of_month ~d[2019-W04-1], 1
      "4"

  """
  @spec week_of_month(Cldr.Calendar.any_date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def week_of_month(week_of_month, n \\ @default_format, options \\ [])

  def week_of_month(week_of_month, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    week_of_month(week_of_month, @default_format, locale, backend, Map.new(options))
  end

  def week_of_month(week_of_month, n, options) do
    {locale, backend} = extract_locale!(options)
    week_of_month(week_of_month, n, locale, backend, Map.new(options))
  end

  @spec week_of_month(
          Cldr.Calendar.date(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def week_of_month(date, n, locale, backend, options \\ %{})

  def week_of_month(%{calendar: Calendar.ISO} = date, n, locale, backend, options) do
    %{date | calendar: Cldr.Calendar.Gregorian}
    |> week_of_month(n, locale, backend, options)
  end

  def week_of_month(date, n, _locale, _backend, options) when is_date(date) and n in 1..2 do
    {_month, week_of_month} = Cldr.Calendar.week_of_month(date)

    week_of_month
    |> pad(n)
    |> maybe_wrap(:week_of_month, options)
  end

  def week_of_month(date, _n, _locale, _backend, _options) do
    error_return(date, "W", [:year, :month, :day, :calendar])
  end

  @doc """
  Returns the day of the month (symbol `d`) as an integer.

  ## Arguments

  * `date` is a `Date` struct or any map that contains at least the
    keys `:year`, `:month`, `:day` and `:calendar`

  * `n` in an integer between 1 and 2 that determines the format of
    the day of month

  * `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`

  * `options` is a `Keyword` list of options.  There are no options
    used in `day_of_month/4`

  ## Format Symbol

  The representation of the day of the month is made in accordance
  with the following table. Note that `ddd` is not part of the CLDR
  standard.

  | Symbol     | Example         | Cldr Format     |
  | :--------  | :-------------- | :-------------- |
  | d          | 2, 22           |                 |
  | dd         | 02, 22          |                 |
  | ddd        | 2nd, 22nd       |                 |

  ## Examples

      iex> Cldr.DateTime.Formatter.day_of_month ~D[2017-01-04], 1
      4

      iex> Cldr.DateTime.Formatter.day_of_month ~D[2017-01-04], 2
      "04"

      iex> Cldr.DateTime.Formatter.day_of_month ~D[2017-01-04], 3
      "4th"

  """
  @spec day_of_month(Cldr.Calendar.any_date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def day_of_month(day_of_month, n \\ @default_format, options \\ [])

  def day_of_month(day_of_month, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    day_of_month(day_of_month, @default_format, locale, backend, Map.new(options))
  end

  def day_of_month(day_of_month, n, options) do
    {locale, backend} = extract_locale!(options)
    day_of_month(day_of_month, n, locale, backend, Map.new(options))
  end

  @spec day_of_month(
          Cldr.Calendar.date(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def day_of_month(date, n, locale, backend, options \\ %{})

  def day_of_month(%{calendar: Calendar.ISO} = date, n, locale, backend, options) do
    %{date | calendar: Cldr.Calendar.Gregorian}
    |> day_of_month(n, locale, backend, options)
  end

  def day_of_month(%{day: day}, 1, _locale, backend, options) do
    day
    |> transliterate("d", backend, options)
    |> maybe_wrap(:day, options)
  end

  def day_of_month(%{day: day}, 2, _locale, backend, options) do
    day
    |> transliterate("d", backend, options)
    |> pad(2)
    |> maybe_wrap(:day, options)
  end

  def day_of_month(%{day: day}, 3, locale, backend, options) do
    day
    |> transliterate("d", backend, options)
    |> Cldr.Number.to_string!(backend, format: :ordinal, locale: locale)
    |> maybe_wrap(:day, options)
  end

  def day_of_month(date, _n, _locale, _backend, _options) do
    error_return(date, "d", [:day])
  end

  @doc """
  Returns the day of the year (symbol `D`) as an integer in string
  format.

  ## Arguments

  * `date` is a `Date` struct or any map that contains at least the
    keys `:year`, `:month`, `:day` and `:calendar`

  * `n` in an integer between 1 and 3 that determines the format of
    the day of year

  * `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`

  * `options` is a `Keyword` list of options.  There are no options
    used in `day_of_year/4`

  ## Format Symbol

  The representation of the day of the year is made in accordance with
  the following table:

  | Symbol     | Example         | Cldr Format     |
  | :--------  | :-------------- | :-------------- |
  | D          | 3, 33, 333      |                 |
  | DD         | 03, 33, 333     |                 |
  | DDD        | 003, 033, 333   |                 |

  ## Examples

      iex> Cldr.DateTime.Formatter.day_of_year ~D[2017-01-15], 1
      "15"

      iex> Cldr.DateTime.Formatter.day_of_year ~D[2017-01-15], 2
      "15"

      iex> Cldr.DateTime.Formatter.day_of_year ~D[2017-01-15], 3
      "015"

  """
  @spec day_of_year(Cldr.Calendar.any_date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def day_of_year(day_of_year, n \\ @default_format, options \\ [])

  def day_of_year(day_of_year, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    day_of_year(day_of_year, @default_format, locale, backend, Map.new(options))
  end

  def day_of_year(day_of_year, n, options) do
    {locale, backend} = extract_locale!(options)
    day_of_year(day_of_year, n, locale, backend, Map.new(options))
  end

  @spec day_of_year(
          Cldr.Calendar.any_date_time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def day_of_year(date, n, locale, backend, options \\ %{})

  def day_of_year(%{calendar: Calendar.ISO} = date, n, locale, backend, options) do
    %{date | calendar: Cldr.Calendar.Gregorian}
    |> day_of_year(n, locale, backend, options)
  end

  def day_of_year(date, n, _locale, _backend, options) when is_date(date) and n in 1..3 do
    date
    |> Cldr.Calendar.day_of_year()
    |> pad(n)
    |> maybe_wrap(:day_of_year, options)
  end

  def day_of_year(date, _n, _locale, _backend, _options) do
    error_return(date, "D", [:year, :month, :day, :calendar])
  end

  @doc """
  Returns the weekday name (format  symbol `E`) as an string.

  ## Arguments

  * `date` is a `Date` struct or any map that contains at least the
    keys `:year`, `:month`, `:day` and `:calendar`

  * `n` in an integer between 1 and 6 that determines the format of
    the day of week

  * `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`

  * `options` is a `Keyword` list of options.  There are no options
    used in `day_name/4`

  ## Format Symbol

  The representation of the day name is made in accordance with
  the following table:

  | Symbol     | Example         | Cldr Format     |
  | :--------  | :-------------- | :-------------- |
  | E, EE, EEE | "Tue"           | Abbreviated     |
  | EEEE       | "Tuesday"       | Wide            |
  | EEEEE      | "T"             | Narrow          |
  | EEEEEE     | "Tu"            | Short           |

  ## Examples

      iex> Cldr.DateTime.Formatter.day_name ~D[2017-08-15], 6
      "Tu"

      iex> Cldr.DateTime.Formatter.day_name ~D[2017-08-15], 5
      "T"

      iex> Cldr.DateTime.Formatter.day_name ~D[2017-08-15], 4
      "Tuesday"

      iex> Cldr.DateTime.Formatter.day_name ~D[2017-08-15], 3
      "Tue"

      iex> Cldr.DateTime.Formatter.day_name ~D[2017-08-15], 2
      "Tue"

      iex> Cldr.DateTime.Formatter.day_name ~D[2017-08-15], 1
      "Tue"

  """
  @spec day_name(Cldr.Calendar.any_date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def day_name(day_name, n \\ @default_format, options \\ [])

  def day_name(day_name, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    day_name(day_name, @default_format, locale, backend, Map.new(options))
  end

  def day_name(day_name, n, options) do
    {locale, backend} = extract_locale!(options)
    day_name(day_name, n, locale, backend, Map.new(options))
  end

  @spec day_name(
          Cldr.Calendar.any_date_time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def day_name(date, n, locale, backend, options \\ %{})

  def day_name(%{calendar: Calendar.ISO} = date, n, locale, backend, options) do
    %{date | calendar: Cldr.Calendar.Gregorian}
    |> day_name(n, locale, backend, options)
  end

  def day_name(date, n, locale, backend, options) when is_date(date) and n in 1..3 do
    date
    |> Cldr.Calendar.localize(:day_of_week, :format, :abbreviated, backend, locale)
    |> maybe_wrap(:day_name, options)
  end

  def day_name(date, 4, locale, backend, options) when is_date(date) do
    date
    |> Cldr.Calendar.localize(:day_of_week, :format, :wide, backend, locale)
    |> maybe_wrap(:day_name, options)
  end

  def day_name(date, 5, locale, backend, options) when is_date(date) do
    date
    |> Cldr.Calendar.localize(:day_of_week, :format, :narrow, backend, locale)
    |> maybe_wrap(:day_name, options)
  end

  def day_name(date, 6, locale, backend, options) when is_date(date) do
    date
    |> Cldr.Calendar.localize(:day_of_week, :format, :short, backend, locale)
    |> maybe_wrap(:day_name, options)
  end

  def day_name(date, _n, _locale, _backend, _options) do
    error_return(date, "E", [:year, :month, :day, :calendar])
  end

  @doc """
  Returns the local day of week (format symbol `e`) as a
  number or name.

  ## Arguments

  * `date` is a `Date` struct or any map that contains at least the
    keys `:year`, `:month`, `:day` and `:calendar`

  * `n` in an integer between 1 and 6 that determines the format of
    the day of week

  * `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`

  * `options` is a `Keyword` list of options.  There are no options
    used in `day_of_week/4`

  ## Notes

  Returns the same as format symbol `E` except that it adds a
  numeric value that will depend on the local starting day
  of the week.

  ## Format Symbol

  The representation of the time period is made in accordance with
  the following table:

  | Symbol     | Example         | Cldr Format     |
  | :--------  | :-------------- | :-------------- |
  | e          | 2               | Single digit    |
  | ee         | "02"            | Two digits      |
  | eee        | "Tue"           | Abbreviated     |
  | eeee       | "Tuesday"       | Wide            |
  | eeeee      | "T"             | Narrow          |
  | eeeeee     | "Tu"            | Short           |

  ## Examples

      iex> Cldr.DateTime.Formatter.day_of_week ~D[2017-08-15], 3
      "Tue"

      iex> Cldr.DateTime.Formatter.day_of_week ~D[2017-08-15], 4
      "Tuesday"

      iex> Cldr.DateTime.Formatter.day_of_week ~D[2017-08-15], 5
      "T"

      iex> Cldr.DateTime.Formatter.day_of_week ~D[2017-08-15], 6
      "Tu"

      iex> Cldr.DateTime.Formatter.day_of_week ~D[2017-08-15], 1
      "2"

      iex> Cldr.DateTime.Formatter.day_of_week ~D[2017-08-15], 2
      "02"

  """
  @spec day_of_week(Cldr.Calendar.any_date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def day_of_week(day_of_week, n \\ @default_format, options \\ [])

  def day_of_week(day_of_week, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    day_of_week(day_of_week, @default_format, locale, backend, Map.new(options))
  end

  def day_of_week(day_of_week, n, options) do
    {locale, backend} = extract_locale!(options)
    day_of_week(day_of_week, n, locale, backend, Map.new(options))
  end

  @spec day_of_week(
          Cldr.Calendar.any_date_time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def day_of_week(date, n, locale, backend, options \\ %{})

  def day_of_week(%{calendar: Calendar.ISO} = date, n, locale, backend, options) do
    %{date | calendar: Cldr.Calendar.Gregorian}
    |> day_of_week(n, locale, backend, options)
  end

  def day_of_week(date, n, _locale, _backend, options) when is_date(date) and n in 1..2 do
    date
    |> Cldr.Calendar.day_of_week()
    |> pad(n)
    |> maybe_wrap(:day_of_week, options)
  end

  def day_of_week(date, n, locale, backend, options) when is_date(date) and n >= 3 do
    date
    |> day_name(n, locale, backend, options)
    |> maybe_wrap(:day_of_week, options)
  end

  def day_of_week(date, _n, _locale, _backend, _options) do
    error_return(date, "e", [:year, :month, :day, :calendar])
  end

  @doc """
  Returns the stand-alone local day (format symbol `c`)
  of week number/name.

  ## Arguments

  * `date` is a `Date` struct or any map that contains at least the
    keys `:year`, `:month`, `:day` and `:calendar`

  * `n` in an integer between 1 and 6 that determines the format of
    the day of week

  * `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`

  * `options` is a `Keyword` list of options.  There are no options
    used in `standalone_day_of_week/4`

  ## Notes

  This is the same as `weekday_number/4` except that it is intended
  for use without the associated `d` format symbol.

  ## Format Symbol

  The representation of the time period is made in accordance with
  the following table:

  | Symbol     | Example         | Cldr Format     |
  | :--------  | :-------------- | :-------------- |
  | c, cc      | 2               | Single digit    |
  | ccc        | "Tue"           | Abbreviated     |
  | cccc       | "Tuesday"       | Wide            |
  | ccccc      | "T"             | Narrow          |
  | cccccc     | "Tu"            | Short           |

  ## Examples

      iex> Cldr.DateTime.Formatter.standalone_day_of_week ~D[2017-08-15], 3
      "Tue"

      iex> Cldr.DateTime.Formatter.standalone_day_of_week ~D[2017-08-15], 4
      "Tuesday"

      iex> Cldr.DateTime.Formatter.standalone_day_of_week ~D[2017-08-15], 5
      "T"

      iex> Cldr.DateTime.Formatter.standalone_day_of_week ~D[2017-08-15], 6
      "Tu"

  """
  @spec standalone_day_of_week(Cldr.Calendar.any_date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def standalone_day_of_week(standalone_day_of_week, n \\ @default_format, options \\ [])

  def standalone_day_of_week(standalone_day_of_week, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)

    standalone_day_of_week(
      standalone_day_of_week,
      @default_format,
      locale,
      backend,
      Map.new(options)
    )
  end

  def standalone_day_of_week(standalone_day_of_week, n, options) do
    {locale, backend} = extract_locale!(options)
    standalone_day_of_week(standalone_day_of_week, n, locale, backend, Map.new(options))
  end

  @spec standalone_day_of_week(
          Cldr.Calendar.date(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def standalone_day_of_week(date, n, locale, backend, options \\ %{})

  def standalone_day_of_week(%{calendar: Calendar.ISO} = date, n, locale, backend, options) do
    %{date | calendar: Cldr.Calendar.Gregorian}
    |> standalone_day_of_week(n, locale, backend, options)
  end

  def standalone_day_of_week(date, n, _locale, _backend, options)
      when is_date(date) and n in 1..2 do
    date
    |> Cldr.Calendar.day_of_week()
    |> pad(n)
    |> maybe_wrap(:standalone_day_of_week, options)
  end

  def standalone_day_of_week(date, 3, locale, backend, options) when is_date(date) do
    date
    |> Cldr.Calendar.localize(:day_of_week, :stand_alone, :abbreviated, backend, locale)
    |> maybe_wrap(:standalone_day_of_week, options)
  end

  def standalone_day_of_week(date, 4, locale, backend, options) when is_date(date) do
    date
    |> Cldr.Calendar.localize(:day_of_week, :stand_alone, :wide, backend, locale)
    |> maybe_wrap(:standalone_day_of_week, options)
  end

  def standalone_day_of_week(date, 5, locale, backend, options) when is_date(date) do
    date
    |> Cldr.Calendar.localize(:day_of_week, :stand_alone, :narrow, backend, locale)
    |> maybe_wrap(:standalone_day_of_week, options)
  end

  def standalone_day_of_week(date, 6, locale, backend, options) when is_date(date) do
    date
    |> Cldr.Calendar.localize(:day_of_week, :stand_alone, :short, backend, locale)
    |> maybe_wrap(:standalone_day_of_week, options)
  end

  def standalone_day_of_week(date, _n, _locale, _backend, _options) do
    error_return(date, "c", [:year, :month, :day, :calendar])
  end

  #
  # Time formatters
  #

  @doc """
  Returns a localised version of `am` or `pm` (format symbol `a`).

  ## Arguments

  * `time` is a `Time` struct or any map that contains at least the
    key `:second`

  * `n` in an integer between 1 and 5 that determines the format of the
    time period

  * `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`

  * `options` is a `Keyword` list of options.  The available option is
    `period: :variant` which will use a veriant of localised "am" or
    "pm" if one is available

  ## Notes

  May be upper or lowercase depending on the locale and other options.
  The wide form may be the same as the short form if the “real”
  long form (eg ante meridiem) is not customarily used.

  ## Format Symbol

  The representation of the time period is made in accordance with the following
  table:

  | Symbol     | Example         | Cldr Format     |
  | :--------  | :-------------- | :-------------- |
  | a, aa, aaa | "am."           | Abbreviated     |
  | aaaa       | "am."           | Wide            |
  | aaaaa      | "am"            | Narrow          |

  ## Examples

      iex> Cldr.DateTime.Formatter.period_am_pm %{hour: 0, minute: 0}
      "AM"

      iex> Cldr.DateTime.Formatter.period_am_pm %{hour: 3, minute: 0}
      "AM"

      iex> Cldr.DateTime.Formatter.period_am_pm %{hour: 13, minute: 0}
      "PM"

      iex> Cldr.DateTime.Formatter.period_am_pm %{hour: 21, minute: 0}, 5
      "p"

      iex> Cldr.DateTime.Formatter.period_am_pm %{hour: 21, minute: 0}, 5
      "p"

  """
  @spec period_am_pm(Cldr.Calendar.time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def period_am_pm(period_am_pm, n \\ @default_format, options \\ [])

  def period_am_pm(period_am_pm, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    period_am_pm(period_am_pm, @default_format, locale, backend, Map.new(options))
  end

  def period_am_pm(period_am_pm, n, options) do
    {locale, backend} = extract_locale!(options)
    period_am_pm(period_am_pm, n, locale, backend, Map.new(options))
  end

  @spec period_am_pm(
          Cldr.Calendar.time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def period_am_pm(time, n, locale, backend, options \\ %{})

  def period_am_pm(%{calendar: Calendar.ISO} = time, n, locale, backend, options) do
    %{time | calendar: Cldr.Calendar.Gregorian}
    |> period_am_pm(n, locale, backend, options)
  end

  def period_am_pm(time, n, locale, backend, options)
      when is_map_key(time, :hour) and n in 1..3 do
    time
    |> Cldr.Calendar.localize(:am_pm, :format, :abbreviated, backend, locale, options)
    |> maybe_wrap(:period_am_pm, options)
  end

  def period_am_pm(time, 4, locale, backend, options) when is_map_key(time, :hour) do
    time
    |> Cldr.Calendar.localize(:am_pm, :format, :wide, backend, locale, options)
    |> maybe_wrap(:period_am_pm, options)
  end

  def period_am_pm(time, 5, locale, backend, options) when is_map_key(time, :hour) do
    time
    |> Cldr.Calendar.localize(:am_pm, :format, :narrow, backend, locale, options)
    |> maybe_wrap(:period_am_pm, options)
  end

  def period_am_pm(time, _n, _locale, _backend, _options) do
    error_return(time, "a", [:hour])
  end

  @doc """
  Returns the formatting of the time period as either
  `noon`, `midnight` or `am`/`pm` (format symbol 'b').

  ## Arguments

  * `time` is a `Time` struct or any map that contains at least the
    key `:second`

  * `n` in an integer between 1 and 5 that determines the format of the
    time period

  * `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`

  * `options` is a `Keyword` list of options.  The available option is
    `period: :variant` which will use a variant of localised "noon" and
    "midnight" if one is available

  ## Notes

  If the language doesn't support "noon" or "midnight" then
  `am`/`pm` is used for all time periods.

  May be upper or lowercase depending on the locale and other options.
  If the locale doesn't have the notion of a unique `noon == 12:00`,
  then the PM form may be substituted. Similarly for `midnight == 00:00`
  and the AM form.

  ## Format Symbol

  The representation of the time period is made in accordance with the following
  table:

  | Symbol     | Example         | Cldr Format     |
  | :--------  | :-------------- | :-------------- |
  | b, bb, bbb | "mid."          | Abbreviated     |
  | bbbb       | "midnight"      | Wide            |
  | bbbbb      | "md"            | Narrow          |

  ## Examples

      iex> Cldr.DateTime.Formatter.period_noon_midnight %{hour: 12, minute: 0}
      "noon"

      iex> Cldr.DateTime.Formatter.period_noon_midnight %{hour: 0, minute: 0}
      "midnight"

      iex> Cldr.DateTime.Formatter.period_noon_midnight %{hour: 11, minute: 0}
      "in the morning"

      iex> Cldr.DateTime.Formatter.period_noon_midnight %{hour: 16, minute: 0}
      "PM"

  """
  @spec period_noon_midnight(Cldr.Calendar.time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def period_noon_midnight(period_noon_midnight, n \\ @default_format, options \\ [])

  def period_noon_midnight(period_noon_midnight, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    period_noon_midnight(period_noon_midnight, @default_format, locale, backend, Map.new(options))
  end

  def period_noon_midnight(period_noon_midnight, n, options) do
    {locale, backend} = extract_locale!(options)
    period_noon_midnight(period_noon_midnight, n, locale, backend, Map.new(options))
  end

  @spec period_noon_midnight(
          Cldr.Calendar.time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def period_noon_midnight(time, n, locale, backend, options \\ %{})

  def period_noon_midnight(%{hour: hour, minute: minute} = time, n, locale, backend, options)
      when (rem(hour, 12) == 0 or rem(hour, 24) < 12) and minute == 0 do
    format_backend = Module.concat(backend, DateTime.Format)

    if format_backend.language_has_noon_and_midnight?(locale) do
      day_period = format_backend.day_period_for(time, locale.language)
      Cldr.Calendar.localize(day_period, :day_periods, :format, period_format(n), backend, locale)
    else
      period_am_pm(time, n, locale, backend, options)
    end
    |> maybe_wrap(:period_noon_midnight, options)
  end

  def period_noon_midnight(%{hour: _hour, minute: _minute} = time, n, locale, backend, options) do
    time
    |> period_am_pm(n, locale, backend, options)
    |> maybe_wrap(:period_noon_midnight, options)
  end

  def period_noon_midnight(time, _n, _locale, _backend, _options) do
    error_return(time, "b", [:hour, :minute])
  end

  @doc """
  Returns the formatting of the time period as a string, for
  example `at night` (format symbol `B`).

  ## Arguments

  * `time` is a `Time` struct or any map that contains at least the
    key `:second`

  * `n` in an integer between 1 and 5 that determines the format of the
    time period

  * `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`

  * `options` is a `Keyword` list of options.  The available option is
    `period: :variant` which will use a veriant of localised flexible time
    period names if one is available

  ## Notes

  The time period may be upper or lowercase depending on the locale and
  other options.  Often there is only one width that is customarily used.

  ## Format Symbol

  The representation of the time period is made in accordance with the following
  table:

  | Symbol     | Example         | Cldr Format     |
  | :--------  | :-------------- | :-------------- |
  | B, BB, BBB | "at night"      | Abbreviated     |
  | BBBB       | "at night"      | Wide            |
  | BBBBB      | "at night"      | Narrow          |

  ## Examples

      iex> Cldr.DateTime.Formatter.period_flex %{hour: 11, minute: 5, second: 23}
      "in the morning"

      iex> Cldr.DateTime.Formatter.period_flex %{hour: 16, minute: 5, second: 23}
      "in the afternoon"

      iex> Cldr.DateTime.Formatter.period_flex %{hour: 23, minute: 5, second: 23}
      "at night"

  """
  @spec period_flex(Cldr.Calendar.time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def period_flex(period_flex, n \\ @default_format, options \\ [])

  def period_flex(period_flex, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    period_flex(period_flex, @default_format, locale, backend, Map.new(options))
  end

  def period_flex(period_flex, n, options) do
    {locale, backend} = extract_locale!(options)
    period_flex(period_flex, n, locale, backend, Map.new(options))
  end

  @spec period_flex(Cldr.Calendar.time(), integer, Locale.locale_reference(), Cldr.backend(), map()) ::
          String.t() | {:error, String.t()}

  def period_flex(time, n, locale, backend, options \\ %{})

  def period_flex(%{hour: _hour, minute: _minute} = time, n, locale, backend, options) do
    format_backend = Module.concat(backend, DateTime.Format)
    day_period = format_backend.day_period_for(time, locale)

    day_period
    |> Cldr.Calendar.localize(:day_periods, :format, period_format(n), backend, locale)
    |> maybe_wrap(:period_flex, options)
  end

  def period_flex(time, _n, _locale, _backend, _options) do
    error_return(time, "B", [:hour, :minute])
  end

  defp period_format(n) when n in 1..3, do: :abbreviated
  defp period_format(4), do: :wide
  defp period_format(5), do: :narrow

  @doc """
  Returns the hour formatted in a locale-specific format

  ## Arguments

  * `time` is a `Time` struct or any map that contains at least the key `:hour`

  * `n` is the number of digits to which `:hour` is padded

  * `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`

  * `options` is a `Keyword` list of options.  There are no options used in
    `hour/4`

  ## Examples

      iex> Cldr.DateTime.Formatter.hour ~N[2020-04-25 15:00:00.0]
      "3"

      iex> Cldr.DateTime.Formatter.hour ~N[2020-04-25 15:00:00.0], locale: "fr"
      "15"

      iex> Cldr.DateTime.Formatter.hour ~N[2020-04-25 15:00:00.0], locale: "en-AU"
      "3"

      iex> Cldr.DateTime.Formatter.hour ~N[2020-04-25 15:00:00.0], locale: "en-AU-u-hc-h23"
      "15"

  """
  @spec hour(Cldr.Calendar.time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def hour(hour, n \\ @default_format, options \\ [])

  def hour(hour, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    hour(hour, @default_format, locale, backend, Map.new(options))
  end

  def hour(hour, n, options) do
    {locale, backend} = extract_locale!(options)
    hour(hour, n, locale, backend, Map.new(options))
  end

  @spec hour(Cldr.Calendar.time(), integer, Locale.locale_reference(), Cldr.backend(), map()) ::
          String.t() | {:error, String.t()}

  def hour(time, n, locale, backend, options \\ %{})

  def hour(hour, n, locale, backend, options) do
    hour_formatter = Cldr.Time.hour_format_from_locale(locale)

    __MODULE__
    |> apply(hour_formatter, [hour, n, locale, backend, options])
    |> maybe_wrap(:hour, options)
  end

  @doc """
  Returns the formatting of the `:hour` (format symbol `h`) as a number in the
  range 1..12 as a string.

  ## Arguments

  * `time` is a `Time` struct or any map that contains at least the key `:hour`

  * `n` is the number of digits to which `:hour` is padded

  * `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`

  * `options` is a `Keyword` list of options.  There are no options used in
    `h12/4`

  ## Format Symbol

  The representation of the `hour` is made in accordance with the following
  table:

  | Symbol  | Midn.	|	Morning	| Noon |	Afternoon	| Midn. |
  | :----:  | :---: | :-----: | :--: | :--------: | :---: |
  |   h     |  12   | 1...11  |  12  |  1...11    |  12   |

  ## Examples

      iex> Cldr.DateTime.Formatter.h12 %{hour: 0}
      "12"

      iex> Cldr.DateTime.Formatter.h12 %{hour: 12}
      "12"

      iex> Cldr.DateTime.Formatter.h12 %{hour: 24}
      "12"

      iex> Cldr.DateTime.Formatter.h12 %{hour: 11}
      "11"

      iex> Cldr.DateTime.Formatter.h12 %{hour: 23}
      "11"

  """
  @spec h12(Cldr.Calendar.time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def h12(h12, n \\ @default_format, options \\ [])

  def h12(h12, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    h12(h12, @default_format, locale, backend, Map.new(options))
  end

  def h12(h12, n, options) do
    {locale, backend} = extract_locale!(options)
    h12(h12, n, locale, backend, Map.new(options))
  end

  @spec h12(Cldr.Calendar.time(), integer, Locale.locale_reference(), Cldr.backend(), map()) ::
          String.t() | {:error, String.t()}

  def h12(time, n, locale, backend, options \\ %{})

  def h12(%{hour: hour}, n, _locale, _backend, options) when hour in [0, 12, 24] do
    12
    |> pad(n)
    |> maybe_wrap(:h12, options)
  end

  def h12(%{hour: hour}, n, _locale, _backend, options) when hour in 1..11 do
    hour
    |> pad(n)
    |> maybe_wrap(:h12, options)
  end

  def h12(%{hour: hour}, n, _locale, _backend, options) when hour in 13..23 do
    (hour - 12)
    |> pad(n)
    |> maybe_wrap(:h12, options)
  end

  def h12(%module{hour: hour}, n, _locale, _backend, options) when module in @duration_modules do
    hour
    |> pad(n)
    |> maybe_wrap(:h12, options)
  end

  def h12(%{hour: hour}, _n, _locale, _backend, _options) when is_integer(hour) do
    {:error,
     {Cldr.DateTime.FormatError, "Hour must be in the range of 0..24. Found #{inspect(hour)}"}}
  end

  def h12(time, _n, _locale, _backend, _options) do
    error_return(time, "h", [:hour])
  end

  @doc """
  Returns the formatting of the `:hour` (format symbol `K`) as a number in the
  range 0..11 as a string.

  ## Arguments

  * `time` is a `Time` struct or any map that contains at least the key `:hour`

  * `n` is the number of digits to which `:hour` is padded

  * `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`

  * `options` is a `Keyword` list of options.  There are no options used in
    `h11/4`

  ## Format Symbol

  The representation of the `hour` is made in accordance with the following
  table:

  | Symbol  | Midn.	|	Morning	| Noon |	Afternoon	| Midn. |
  | :----:  | :---: | :-----: | :--: | :--------: | :---: |
  |   K     |   0   | 1...11  |   0  |  1...11    |   0   |

  ## Examples

      iex> Cldr.DateTime.Formatter.h11 %{hour: 0}
      "0"

      iex> Cldr.DateTime.Formatter.h11 %{hour: 12}
      "0"

      iex> Cldr.DateTime.Formatter.h11 %{hour: 24}
      "0"

      iex> Cldr.DateTime.Formatter.h11 %{hour: 23}
      "11"

      iex> Cldr.DateTime.Formatter.h11 %{hour: 11}
      "11"

      iex> Cldr.DateTime.Formatter.h11 %{hour: 9}
      "9"

  """
  @spec h11(Cldr.Calendar.time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def h11(h11, n \\ @default_format, options \\ [])

  def h11(h11, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    h11(h11, @default_format, locale, backend, Map.new(options))
  end

  def h11(h11, n, options) do
    {locale, backend} = extract_locale!(options)
    h11(h11, n, locale, backend, Map.new(options))
  end

  @spec h11(Cldr.Calendar.time(), integer, Locale.locale_reference(), Cldr.backend(), map()) ::
          String.t() | {:error, String.t()}

  def h11(time, n, locale, backend, options \\ %{})

  def h11(%{hour: hour}, n, _locale, _backend, options) when hour in [0, 12, 24] do
    0
    |> pad(n)
    |> maybe_wrap(:h11, options)
  end

  def h11(%{hour: hour}, n, _locale, _backend, options) when hour in 1..11 do
    hour
    |> pad(n)
    |> maybe_wrap(:h11, options)
  end

  def h11(%{hour: hour}, n, _locale, _backend, options) when hour in 13..23 do
    (hour - 12)
    |> pad(n)
    |> maybe_wrap(:h11, options)
  end

  def h11(%module{hour: hour}, n, _locale, _backend, options) when module in @duration_modules do
    hour
    |> pad(n)
    |> maybe_wrap(:h11, options)
  end

  def h11(%{hour: hour}, _n, _locale, _backend, _options) when is_integer(hour) do
    {:error,
     {Cldr.DateTime.FormatError, "Hour must be in the range of 0..24. Found #{inspect(hour)}"}}
  end

  def h11(time, _n, _locale, _backend, _options) do
    error_return(time, "K", [:hour])
  end

  @doc """
  Returns the formatting of the `:hour` (format symbol `k`) as a number in the
  range 1..24 as a string.

  ## Arguments

  * `time` is a `Time` struct or any map that contains at least the key `:hour`

  * `n` is the number of digits to which `:hour` is padded

  * `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`

  * `options` is a `Keyword` list of options.  There are no options used in
    `h24/4`

  ## Format Symbol

  The representation of the `hour` is made in accordance with the following
  table:

  | Symbol  | Midn.	|	Morning	| Noon |	Afternoon	| Midn. |
  | :----:  | :---: | :-----: | :--: | :--------: | :---: |
  |   k     |  24   | 1...11  |  12  |  13...23   |  24   |

  ## Examples

      iex(4)> Cldr.DateTime.Formatter.h24 %{hour: 0}
      "24"

      iex(5)> Cldr.DateTime.Formatter.h24 %{hour: 12}
      "12"

      iex(6)> Cldr.DateTime.Formatter.h24 %{hour: 13}
      "13"

      iex(7)> Cldr.DateTime.Formatter.h24 %{hour: 9}
      "9"

      iex(8)> Cldr.DateTime.Formatter.h24 %{hour: 24}
      "24"

  """
  @spec h24(Cldr.Calendar.time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def h24(h24, n \\ @default_format, options \\ [])

  def h24(h24, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    h24(h24, @default_format, locale, backend, Map.new(options))
  end

  def h24(h24, n, options) do
    {locale, backend} = extract_locale!(options)
    h24(h24, n, locale, backend, Map.new(options))
  end

  @spec h24(Cldr.Calendar.time(), integer, Locale.locale_reference(), Cldr.backend(), map()) ::
          String.t() | {:error, String.t()}

  def h24(time, n, locale, backend, options \\ %{})

  def h24(%{hour: hour}, n, _locale, _backend, options) when hour in [0, 24] do
    24
    |> pad(n)
    |> maybe_wrap(:h24, options)
  end

  def h24(%{hour: hour}, n, _locale, _backend, options) when hour in 1..23 do
    hour
    |> pad(n)
    |> maybe_wrap(:h24, options)
  end

  def h24(%module{hour: hour}, n, _locale, _backend, options) when module in @duration_modules do
    hour
    |> pad(n)
    |> maybe_wrap(:h24, options)
  end

  def h24(%{hour: hour}, _n, _locale, _backend, _options) when is_integer(hour) do
    {:error,
     {Cldr.DateTime.FormatError, "Hour must be in the range of 0..24. Found #{inspect(hour)}"}}
  end

  def h24(time, _n, _locale, _backend, _options) do
    error_return(time, "k", [:hour])
  end

  @doc """
  Returns the formatting of the `:hour` (format symbol `H`) as a number
  in the range 0..23 as a string.

  ## Arguments

  * `time` is a `Time` struct or any map that contains at least the key `:hour`

  * `n` is the number of digits to which `:hour` is padded

  * `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`

  * `options` is a `Keyword` list of options.  There are no options used in
    `h23/4`

  ## Format Symbol

  The representation of the `hour` is made in accordance with the following
  table:

  | Symbol  | Midn.	|	Morning	| Noon |	Afternoon	| Midn. |
  | :----:  | :---: | :-----: | :--: | :--------: | :---: |
  |   H     |   0   | 1...11  |  12  |  13...23   |   0   |

  ## Examples:

      iex> Cldr.DateTime.Formatter.h23 %{hour: 10}
      "10"

      iex> Cldr.DateTime.Formatter.h23 %{hour: 13}
      "13"

      iex> Cldr.DateTime.Formatter.h23 %{hour: 21}
      "21"

      iex> Cldr.DateTime.Formatter.h23 %{hour: 24}
      "0"

      iex> Cldr.DateTime.Formatter.h23 %{hour: 0}
      "0"

  """
  @spec h23(Cldr.Calendar.time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def h23(h23, n \\ @default_format, options \\ [])

  def h23(h23, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    h23(h23, @default_format, locale, backend, Map.new(options))
  end

  def h23(h23, n, options) do
    {locale, backend} = extract_locale!(options)
    h23(h23, n, locale, backend, Map.new(options))
  end

  @spec h23(Cldr.Calendar.time(), integer, Locale.locale_reference(), Cldr.backend(), map()) ::
          String.t() | {:error, String.t()}

  def h23(time, n, locale, backend, options \\ %{})

  def h23(%{hour: hour}, n, _locale, _backend, options) when abs(hour) in [0, 24] do
    0
    |> pad(n)
    |> maybe_wrap(:h23, options)
  end

  def h23(%{hour: hour}, n, _locale, _backend, options) when abs(hour) in 1..23 do
    abs(hour)
    |> pad(n)
    |> maybe_wrap(:h23, options)
  end

  def h23(%module{hour: hour}, n, _locale, _backend, options) when module in @duration_modules do
    hour
    |> pad(n)
    |> maybe_wrap(:h23, options)
  end

  def h23(%{hour: hour}, _n, _locale, _backend, _options) when is_integer(hour) do
    {:error,
     {Cldr.DateTime.FormatError, "Hour must be in the range of 0..24. Found #{inspect(hour)}"}}
  end

  def h23(time, _n, _locale, _backend, _options) do
    error_return(time, "H", [:hour])
  end

  @doc """
  Returns the `:minute` of a `time` or `datetime` (format symbol `m`) as number
  in string format.  The number of `m`'s in the format determines the formatting.

  ## Arguments

  * `time` is a `Time` struct or any map that contains at least the key `:minute`

  * `n` is the number of digits to which `:minute` is padded

  * `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`

  * `options` is a `Keyword` list of options.  There are no options used in
    `minute/4`

  ## Format Symbol

  The representation of the `minute` is made in accordance with the following
  table:

  | Symbol | Results    | Description                                           |
  | :----  | :--------- | :---------------------------------------------------- |
  | m      | 3, 10      | Minimum digits of minutes                             |
  | mm     | "03", "12" | Number of minutes zero-padded to 2 digits             |

  ## Examples

      iex> Cldr.DateTime.Formatter.minute %{minute: 3}, 1
      3

      iex> Cldr.DateTime.Formatter.minute %{minute: 3}, 2
      "03"

  """
  @spec minute(Cldr.Calendar.time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def minute(minute, n \\ @default_format, options \\ [])

  def minute(minute, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    minute(minute, @default_format, locale, backend, Map.new(options))
  end

  def minute(minute, n, options) do
    {locale, backend} = extract_locale!(options)
    minute(minute, n, locale, backend, Map.new(options))
  end

  @spec minute(Cldr.Calendar.time(), integer, Locale.locale_reference(), Cldr.backend(), map()) ::
          String.t() | {:error, String.t()}

  def minute(time, n, locale, backend, options \\ %{})

  def minute(%{minute: minute}, 1, _locale, _backend, options) do
    maybe_wrap(minute, :minute, options)
  end

  def minute(%{minute: minute}, 2 = n, _locale, _backend, options) do
    minute
    |> pad(n)
    |> maybe_wrap(:minute, options)
  end

  def minute(time, _n, _locale, _backend, _options) do
    error_return(time, "m", [:minute])
  end

  @doc """
  Returns the `:second` of a `time` or `datetime` (format symbol `s`) as number
  in string format.  The number of `s`'s in the format determines the formatting.

  ## Arguments

  * `time` is a `Time` struct or any map that contains at least the key `:second`

  * `n` is the number of digits to which `:hour` is padded

  * `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`

  * `options` is a `Keyword` list of options.  There are no options used in
    `second/4`

  ## Format Symbol

  The representation of the `second` is made in accordance with the following
  table:

  | Symbol | Results    | Description                                           |
  | :----  | :--------- | :---------------------------------------------------- |
  | s      | 3, 48      | Minimum digits of seconds                             |
  | ss     | "03", "48" | Number of seconds zero-padded to 2 digits             |

  ## Examples

      iex> Cldr.DateTime.Formatter.second %{second: 23}, 1
      "23"

      iex> Cldr.DateTime.Formatter.second %{second: 4}, 2
      "04"
  """
  @spec second(Cldr.Calendar.time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def second(second, n \\ @default_format, options \\ [])

  def second(second, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    second(second, @default_format, locale, backend, Map.new(options))
  end

  def second(second, n, options) do
    {locale, backend} = extract_locale!(options)
    second(second, n, locale, backend, Map.new(options))
  end

  @spec second(Cldr.Calendar.time(), integer, Locale.locale_reference(), Cldr.backend(), map()) ::
          String.t() | {:error, String.t()}

  def second(time, n, locale, backend, options \\ %{})

  def second(%{second: second}, n, _locale, _backend, options) do
    second
    |> pad(n)
    |> maybe_wrap(:second, options)
  end

  def second(time, _n, _locale, _backend, _options) do
    error_return(time, "s", [:second])
  end

  @doc """
  Returns the `:second` of a `time` or `datetime` (format symbol `S`) as float
  in string format. The seconds are calculate to include microseconds if they
  are available.  The number of `S`'s in the format determines the formatting.

  ## Arguments

  * `time` is a `Time` struct or any map that contains at least the key `:second`
    with and optional `:microsecond` key of the format used by `Time`

  * `n` is the number of fractional digits to which the float number of seconds
    is rounded

  * `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`

  * `options` is a `Keyword` list of options.  There are no options used in
    `fractional_second/4`

  ## Format Symbol

  The representation of the `second` is made in accordance with the following
  table:

  | Symbol | Results    | Description                                           |
  | :----  | :--------- | :---------------------------------------------------- |
  | S      | "4.0"      | Minimum digits of fractional seconds                  |
  | SS     | "4.00"     | Number of seconds zero-padded to 2 fractional digits  |
  | SSS    | "4.002"    | Number of seconds zero-padded to 3 fractional digits  |

  ## Examples

      iex> Cldr.DateTime.Formatter.fractional_second %{second: 4, microsecond: {2000, 3}}, 1
      "0"

      iex> Cldr.DateTime.Formatter.fractional_second %{second: 4, microsecond: {2000, 3}}, 3
      "002"

      iex> Cldr.DateTime.Formatter.fractional_second %{second: 4}, 1
      "0"

  """
  @spec fractional_second(Cldr.Calendar.time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def fractional_second(fractional_second, n \\ @default_format, options \\ [])

  def fractional_second(fractional_second, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    fractional_second(fractional_second, @default_format, locale, backend, Map.new(options))
  end

  def fractional_second(fractional_second, n, options) do
    {locale, backend} = extract_locale!(options)
    fractional_second(fractional_second, n, locale, backend, Map.new(options))
  end

  @spec fractional_second(
          Cldr.Calendar.time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def fractional_second(time, n, locale, backend, options \\ %{})

  def fractional_second(%{microsecond: {microseconds, precision}}, n, _locale, _backend, options) do
    microseconds
    |> microseconds_to_string(precision)
    |> truncate(n)
    |> right_fill(n)
    |> maybe_wrap(:fractional_second, options)
  end

  def fractional_second(%{second: _second}, n, _locale, _backend, options) do
    "0"
    |> right_fill(n)
    |> maybe_wrap(:fractional_second, options)
  end

  def fractional_second(time, _n, _locale, _backend, _options) do
    error_return(time, "S", [:fractional_second])
  end

  defp truncate(string, n) do
    String.slice(string, 0, n)
  end

  @doc false
  def microseconds_to_string(_microsecond, 0), do: []
  def microseconds_to_string(microsecond, 6), do: pad(microsecond, 6)

  def microseconds_to_string(microsecond, precision) do
    num = div(microsecond, scale_factor(precision))
    pad(num, precision)
  end

  defp scale_factor(1), do: 100_000
  defp scale_factor(2), do: 10_000
  defp scale_factor(3), do: 1_000
  defp scale_factor(4), do: 100
  defp scale_factor(5), do: 10
  defp scale_factor(6), do: 1

  @doc """
  Returns the `time` (format symbol `A`) as milliseconds since
  midnight.

  ## Arguments

  * `time` is a `Time` struct or any map that contains at least the key `:second`
    with and optional `:microsecond` key of the format used by `Time`

  * `n` is the number of fractional digits to which the float number of seconds
    is rounded

  * `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`

  * `options` is a `Keyword` list of options.  There are no options used in
    `millisecond/4`

  ## Format Symbol

  The representation of the `milliseconds` is made in accordance with the following
  table:

  | Symbol | Results    | Description                                             |
  | :----  | :--------- | :------------------------------------------------------ |
  | A+     | "4000"     | Minimum necessary digits of milliseconds since midnight |

  ## Examples

      iex> Cldr.DateTime.Formatter.millisecond %{hour: 0, minute: 0,
      ...>   second: 4, microsecond: {2000, 3}}, 1
      "4002"

      iex> Cldr.DateTime.Formatter.millisecond %{hour: 0, minute: 0, second: 4}, 1
      "4000"

      iex> Cldr.DateTime.Formatter.millisecond %{hour: 10, minute: 10, second: 4}, 1
      "36604000"

      iex> Cldr.DateTime.Formatter.millisecond ~T[07:35:13.215217]
      "27313215"

  """
  @spec millisecond(Cldr.Calendar.time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def millisecond(millisecond, n \\ @default_format, options \\ [])

  def millisecond(millisecond, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    millisecond(millisecond, @default_format, locale, backend, Map.new(options))
  end

  def millisecond(millisecond, n, options) do
    {locale, backend} = extract_locale!(options)
    millisecond(millisecond, n, locale, backend, Map.new(options))
  end

  @milliseconds 1_000
  @spec millisecond(Cldr.Calendar.time(), integer, Locale.locale_reference(), Cldr.backend(), map()) ::
          String.t() | {:error, String.t()}

  def millisecond(time, n, locale, backend, options \\ %{})

  def millisecond(
        %{hour: hour, minute: minute, second: second, microsecond: {fraction, _resolution}},
        n,
        _locale,
        _backend,
        options
      ) do
    (rem(hour, 24) * @milliseconds * 60 * 60 + minute * @milliseconds * 60 +
       second * @milliseconds + div(fraction, @milliseconds))
    |> pad(n)
    |> maybe_wrap(:millisecond, options)
  end

  def millisecond(%{hour: hour, minute: minute, second: second}, n, _locale, _backend, options) do
    (rem(hour, 24) * @milliseconds * 60 * 60 + minute * @milliseconds * 60 +
       second * @milliseconds)
    |> pad(n)
    |> maybe_wrap(:millisecond, options)
  end

  def millisecond(time, _n, _locale, _backend, _options) do
    error_return(time, "A", [:hour, :minute, :second])
  end

  @doc """
  Returns the generic non-location format of a timezone (format symbol `v`)
  from a `DateTime` or `Time`.

  Since Elixir does not provide full time zone support, we return here only
  the `:time_zone` element of the provided `DateTime` or other struct without
  any localization.

  ## Arguments

  * `time` is a `t:DateTime.t/0` struct or any map that contains at least the key `:time_zone`
    key of the format used by `Time`

  * `n` is the generic non-location timezone format and is either `1` (the
    default) or `4`

  * `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`

  * `options` is a `Keyword` list of options.  There are no options used in
    `zone_generic/4`

  ## Format Symbol

  The representation of the `timezone` is made in accordance with the following
  table:

  | Symbol | Results    | Description                                             |
  | :----  | :--------- | :------------------------------------------------------ |
  | v      | "Etc/UTC"  | `:time_zone` key, unlocalised                           |
  | vvvv   | "unk"      | Generic timezone name.  Currently returns only "unk"    |

  ## Examples

      iex> Cldr.DateTime.Formatter.zone_generic %{time_zone: "Etc/UTC",
      ...>   utc_offset: 0, std_offset: 0}, 4
      "GMT"

      iex> Cldr.DateTime.Formatter.zone_generic %{time_zone: "Etc/UTC",
      ...>   utc_offset: 0, std_offset: 0}, 1
      "Etc/UTC"

  """
  @spec zone_generic(Cldr.Calendar.date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def zone_generic(zone_generic, n \\ @default_format, options \\ [])

  def zone_generic(zone_generic, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    zone_generic(zone_generic, @default_format, locale, backend, Map.new(options))
  end

  def zone_generic(zone_generic, n, options) do
    {locale, backend} = extract_locale!(options)
    zone_generic(zone_generic, n, locale, backend, Map.new(options))
  end

  @spec zone_generic(
          Cldr.Calendar.date_time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def zone_generic(time, n, locale, backend, options \\ %{})

  def zone_generic(%{time_zone: time_zone}, 1, _locale, _backend, options) do
    maybe_wrap(time_zone, :zone_generic, options)
  end

  def zone_generic(time, 4, locale, backend, options) do
    time
    |> zone_id(4, locale, backend, options)
    |> maybe_wrap(:zone_generic, options)
  end

  def zone_generic(time, _n, _locale, _backend, _options) do
    error_return(time, "v", [:time_zone])
  end

  @doc """
  Returns the specific non-location format of a timezone (format symbol `z`)
  from a `DateTime` or `Time`.

  Since Elixir does not provide full time zone support, we return here only
  the `:time_zone` element of the provided `DateTime` or other struct without
  any localization.

  ## Arguments

  * `time` is a `t:DateTime.t/0` struct or any map that contains at least the `:zone_abbr`,
  `:utc_offset` and `:std_offset` keys of the format used by `Time`

  * `n` is the specific non-location timezone format and is in the range `1..4`

  * `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`

  * `options` is a `Keyword` list of options.  There are no options used in
   `zone_short/4`

  ## Format Symbol

  The representation of the `timezone` is made in accordance with the following
  table:

  | Symbol | Results    | Description                                             |
  | :----  | :--------- | :------------------------------------------------------ |
  | z..zzz | "UTC"      | `:zone_abbr` key, unlocalised                           |
  | zzzz   | "GMT"      | Delegates to `zone_gmt/4`                               |

  ## Examples

      iex> Cldr.DateTime.Formatter.zone_short %{zone_abbr: "UTC",
      ...>   utc_offset: 0, std_offset: 0}, 1
      "UTC"

      iex> Cldr.DateTime.Formatter.zone_short %{zone_abbr: "UTC",
      ...>   utc_offset: 0, std_offset: 0}, 4
      "GMT"

  """
  @spec zone_short(Cldr.Calendar.date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def zone_short(zone_short, n \\ @default_format, options \\ [])

  def zone_short(zone_short, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    zone_short(zone_short, @default_format, locale, backend, Map.new(options))
  end

  def zone_short(zone_short, n, options) do
    {locale, backend} = extract_locale!(options)
    zone_short(zone_short, n, locale, backend, Map.new(options))
  end

  @spec zone_short(
          Cldr.Calendar.date_time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def zone_short(time, n, locale, backend, options \\ %{})

  def zone_short(%{zone_abbr: zone_abbr}, n, _locale, _backend, options) when n in 1..3 do
    maybe_wrap(zone_abbr, :zone_short, options)
  end

  def zone_short(%{zone_abbr: _zone_abbr} = time, 4 = n, locale, backend, options) do
    time
    |> zone_gmt(n, locale, backend, options)
    |> maybe_wrap(:zone_short, options)
  end

  def zone_short(time, _n, _locale, _backend, _options) do
    error_return(time, "z", [:zone_abbr])
  end

  @doc """
  Returns the time zone ID (format symbol `V`) part of a `DateTime` or `Time`

  For now the short timezone name, exemplar city and generic location
  formats are not supported and therefore return the fallbacks defined in CLDR.

  ## Arguments

  * `time` is a `t:DateTime.t/0` struct or any map that contains at least the `:utc_offset`
    and `:std_offset` keys of the format used by `Time`

  * `n` is the specific non-location timezone format and is in the range `1..4`

  * `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`

  * `options` is a `Keyword` list of options.  There are no options used in
   `zone_id/4`

  ## Format Symbol

  The representation of the `timezone ID` is made in accordance with the following
  table:

  | Symbol | Results        | Description                                             |
  | :----  | :------------- | :------------------------------------------------------ |
  | V      | "unk"          | `:zone_abbr` key, unlocalised                           |
  | VV     | "Etc/UTC       | Delegates to `zone_gmt/4`                               |
  | VVV    | "Unknown City" | Examplar city.  Not supported.                          |
  | VVVV   | "GMT"          | Delegates to `zone_gmt/4`                               |

  ## Examples

      iex> Cldr.DateTime.Formatter.zone_id %{zone_abbr: "Etc/UTC",
      ...>   utc_offset: 0, std_offset: 0}, 1
      "unk"

      iex> Cldr.DateTime.Formatter.zone_id %{zone_abbr: "Etc/UTC",
      ...>   utc_offset: 0, std_offset: 0}, 2
      "Etc/UTC"

      iex> Cldr.DateTime.Formatter.zone_id %{zone_abbr: "Etc/UTC",
      ...>   utc_offset: 0, std_offset: 0}, 3
      "Unknown City"

      iex> Cldr.DateTime.Formatter.zone_id %{zone_abbr: "Etc/UTC",
      ...>   utc_offset: 0, std_offset: 0}, 4
      "GMT"

  """
  @spec zone_id(Cldr.Calendar.date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def zone_id(zone_id, n \\ @default_format, options \\ [])

  def zone_id(zone_id, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    zone_id(zone_id, @default_format, locale, backend, Map.new(options))
  end

  def zone_id(zone_id, n, options) do
    {locale, backend} = extract_locale!(options)
    zone_id(zone_id, n, locale, backend, Map.new(options))
  end

  @spec zone_id(
          Cldr.Calendar.date_time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def zone_id(time, n, locale, backend, options \\ %{})

  def zone_id(%{zone_abbr: _time_zone}, 1, _locale, _backend, options) do
    maybe_wrap("unk", :zone_id, options)
  end

  def zone_id(%{zone_abbr: time_zone}, 2, _locale, _backend, options) do
    maybe_wrap(time_zone, :zone_id, options)
  end

  def zone_id(%{zone_abbr: _time_zone}, 3, _locale, _backend, options) do
    maybe_wrap("Unknown City", :zone_id, options)
  end

  def zone_id(time, 4, locale, backend, options) do
    time
    |> zone_gmt(4, locale, backend, options)
    |> maybe_wrap(:zone_id, options)
  end

  def zone_id(time, _n, _locale, _backend, _options) do
    error_return(time, "V", [:zone_abbr])
  end

  @doc """
  Returns the basic zone offset (format symbol `Z`) part of a `DateTime` or `Time`,

  The ISO8601 basic format with hours, minutes and optional seconds fields.
  The format is equivalent to RFC 822 zone format (when optional seconds field
  is absent). This is equivalent to the "xxxx" specifier.

  ## Arguments

  * `time` is a `t:DateTime.t/0` struct or any map that contains at least the `:utc_offset`
    and `:std_offset` keys of the format used by `Time`

  * `n` is the specific non-location timezone format and is in the range `1..4`

  * `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`

  * `options` is a `Keyword` list of options.  There are no options used in
    `zone_basic/4`

  ## Format Symbol

  The representation of the `timezone` is made in accordance with the following
  table:

  | Symbol | Results        | Description                                             |
  | :----  | :------------- | :------------------------------------------------------ |
  | Z..ZZZ | "+0100"        | ISO8601 Basic Format with hours and minutes             |
  | ZZZZ   | "+01:00"       | Delegates to `zone_gmt/4`                               |
  | ZZZZZ  | "+01:00:10"    | ISO8601 Extended format with optional seconds           |

  ## Examples

      iex> Cldr.DateTime.Formatter.zone_basic %{time_zone: "Etc/UTC",
      ...>   utc_offset: 3600, std_offset: 0}, 1
      "+0100"

      iex> Cldr.DateTime.Formatter.zone_basic %{time_zone: "Etc/UTC",
      ...>   utc_offset: 3610, std_offset: 0}, 4
      "GMT+01:00"

      iex> Cldr.DateTime.Formatter.zone_basic %{time_zone: "Etc/UTC",
      ...>   utc_offset: 0, std_offset: 0}, 5
      "Z"

      iex> Cldr.DateTime.Formatter.zone_basic %{time_zone: "Etc/UTC",
      ...>   utc_offset: 3610, std_offset: 0}, 5
      "+01:00:10"

  """
  @spec zone_basic(Cldr.Calendar.date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def zone_basic(zone_basic, n \\ @default_format, options \\ [])

  def zone_basic(zone_basic, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    zone_basic(zone_basic, @default_format, locale, backend, Map.new(options))
  end

  def zone_basic(zone_basic, n, options) do
    {locale, backend} = extract_locale!(options)
    zone_basic(zone_basic, n, locale, backend, Map.new(options))
  end

  @spec zone_basic(
          Cldr.Calendar.date_time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def zone_basic(time, n, locale, backend, options \\ %{})

  def zone_basic(time, n, _locale, _backend, options) when n in 1..3 do
    with {hours, minutes, seconds} <- Timezone.time_from_zone_offset(time) do
      iso8601_tz_format(%{hour: hours, minute: minutes, second: seconds}, format: :basic)
      |> maybe_wrap(:zone_basic, options)
    end
  end

  def zone_basic(time, 4 = n, locale, backend, options) do
    zone_gmt(time, n, locale, backend, options)
    |> maybe_wrap(:zone_basic, options)
  end

  def zone_basic(time, 5, _locale, _backend, options) do
    with {hours, minutes, seconds} <- Timezone.time_from_zone_offset(time) do
      iso8601_tz_format(%{hour: hours, minute: minutes, second: seconds}, format: :extended)
      |> maybe_wrap(:zone_basic, options)
    end
  end

  def zone_basic(time, _n, _locale, _backend, _options) do
    error_return(time, "Z", [:utc_offset])
  end

  @doc """
  Returns the ISO zone offset (format symbol `X`) part of a `DateTime` or `Time`,

  This is the ISO8601 format with hours, minutes and optional seconds fields with
  "Z" as the identifier if the timezone offset is 0.

  ## Arguments

  * `time` is a `t:DateTime.t/0` struct or any map that contains at least the `:utc_offset`
    and `:std_offset` keys of the format used by `Time`

  * `n` is the specific non-location timezone format and is in the range `1..4`

  * `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`

  * `options` is a `Keyword` list of options.  There are no options used in
    `zone_iso_z/4`

  ## Format Symbol

  The representation of the `timezone offset` is made in accordance with the following
  table:

  | Symbol | Results        | Description                                                              |
  | :----  | :------------- | :----------------------------------------------------------------------- |
  | X      | "+01"          | ISO8601 Basic Format with hours and optional minutes or "Z"              |
  | XX     | "+0100"        | ISO8601 Basic Format with hours and minutes or "Z"                       |
  | XXX    | "+0100"        | ISO8601 Basic Format with hours and minutes, optional seconds or "Z"     |
  | XXXX   | "+010059"      | ISO8601 Basic Format with hours and minutes, optional seconds or "Z"     |
  | XXXXX  | "+01:00:10"    | ISO8601 Extended Format with hours and minutes, optional seconds or "Z"  |

  ## Examples

      iex> Cldr.DateTime.Formatter.zone_iso_z %{time_zone: "Etc/UTC",
      ...>   utc_offset: 3610, std_offset: 0}, 1
      "+01"

      iex> Cldr.DateTime.Formatter.zone_iso_z %{time_zone: "Etc/UTC",
      ...>   utc_offset: 3610, std_offset: 0}, 2
      "+0100"

      iex> Cldr.DateTime.Formatter.zone_iso_z %{time_zone: "Etc/UTC",
      ...>   utc_offset: 3610, std_offset: 0}, 3
      "+01:00:10"

      iex> Cldr.DateTime.Formatter.zone_iso_z %{time_zone: "Etc/UTC",
      ...>   utc_offset: 3610, std_offset: 0}, 4
      "+010010"

      iex> Cldr.DateTime.Formatter.zone_iso_z %{time_zone: "Etc/UTC",
      ...>   utc_offset: 3610, std_offset: 0}, 5
      "+01:00:10"

      iex> Cldr.DateTime.Formatter.zone_iso_z %{time_zone: "Etc/UTC",
      ...>   utc_offset: 0, std_offset: 0}, 5
      "Z"

      iex> Cldr.DateTime.Formatter.zone_iso_z %{time_zone: "Etc/UTC",
      ...>   utc_offset: 0, std_offset: 0}, 4
      "Z"

      iex> Cldr.DateTime.Formatter.zone_iso_z %{time_zone: "Etc/UTC",
      ...>   utc_offset: 0, std_offset: 0}, 3
      "Z"

      iex> Cldr.DateTime.Formatter.zone_iso_z %{time_zone: "Etc/UTC",
      ...>   utc_offset: 0, std_offset: 0}, 2
      "Z"

  """
  @spec zone_iso_z(Cldr.Calendar.date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def zone_iso_z(zone_iso_z, n \\ @default_format, options \\ [])

  def zone_iso_z(zone_iso_z, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    zone_iso_z(zone_iso_z, @default_format, locale, backend, Map.new(options))
  end

  def zone_iso_z(zone_iso_z, n, options) do
    {locale, backend} = extract_locale!(options)
    zone_iso_z(zone_iso_z, n, locale, backend, Map.new(options))
  end

  @spec zone_iso_z(
          Cldr.Calendar.date_time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def zone_iso_z(time, n, locale, backend, options \\ %{})

  def zone_iso_z(time, 1, _locale, _backend, options) do
    with {hours, minutes, seconds} <- Timezone.time_from_zone_offset(time) do
      case {hours, minutes, seconds} do
        {0, 0, _} ->
          "Z"

        {hours, minutes, seconds} ->
          iso8601_tz_format(%{hour: hours, minute: minutes, second: seconds}, format: :basic)
          |> String.replace(~r/00\Z/, "")
      end
      |> maybe_wrap(:zone_iso_z, options)
    end
  end

  def zone_iso_z(time, 2, _locale, _backend, options) do
    with {hours, minutes, seconds} <- Timezone.time_from_zone_offset(time) do
      case {hours, minutes, seconds} do
        {0, 0, _} ->
          "Z"

        {hours, minutes, seconds} ->
          iso8601_tz_format(%{hour: hours, minute: minutes, second: seconds}, format: :basic)
      end
      |> maybe_wrap(:zone_iso_z, options)
    end
  end

  def zone_iso_z(time, 3, _locale, _backend, options) do
    with {hours, minutes, seconds} <- Timezone.time_from_zone_offset(time) do
      case {hours, minutes, seconds} do
        {0, 0, _} ->
          "Z"

        {hours, minutes, seconds} ->
          iso8601_tz_format(%{hour: hours, minute: minutes, second: seconds}, format: :extended)
      end
      |> maybe_wrap(:zone_iso_z, options)
    end
  end

  def zone_iso_z(time, 4, _locale, _backend, options) do
    with {hours, minutes, seconds} <- Timezone.time_from_zone_offset(time) do
      case {hours, minutes, seconds} do
        {0, 0, _} ->
          "Z"

        {hours, minutes, 0 = seconds} ->
          iso8601_tz_format(%{hour: hours, minute: minutes, second: seconds}, format: :basic)

        {hours, minutes, seconds} ->
          iso8601_tz_format(%{hour: hours, minute: minutes, second: seconds}, format: :basic) <>
            pad(seconds, 2)
      end
      |> maybe_wrap(:zone_iso_z, options)
    end
  end

  def zone_iso_z(time, 5, _locale, _backend, options) do
    with {hours, minutes, seconds} <- Timezone.time_from_zone_offset(time) do
      case {hours, minutes, seconds} do
        {0, 0, _} ->
          "Z"

        {hours, minutes, seconds} ->
          iso8601_tz_format(%{hour: hours, minute: minutes, second: seconds}, format: :extended)
      end
      |> maybe_wrap(:zone_iso_z, options)
    end
  end

  def zone_iso_z(time, _n, _locale, _backend, _options) do
    error_return(time, "X", [:utc_offset])
  end

  @doc """
  Returns the ISO zone offset (format symbol `x`) part of a `DateTime` or `Time`,

  This is the ISO8601 format with hours, minutes and optional seconds fields but
  with no "Z" as the identifier if the timezone offset is 0.

  ## Arguments

  * `time` is a `t:DateTime.t/0` struct or any map that contains at least the `:utc_offset`
    and `:std_offset` keys of the format used by `Time`

  * `n` is the specific non-location timezone format and is in the range `1..4`

  * `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`

  * `options` is a `Keyword` list of options.  There are no options used in
    `zone_iso/4`

  ## Format Symbol

  The representation of the `timezone offset` is made in accordance with the following
  table:

  | Symbol | Results        | Description                                                       |
  | :----  | :------------- | :---------------------------------------------------------------- |
  | x      | "+0100"        | ISO8601 Basic Format with hours and optional minutes              |
  | xx     | "-0800"        | ISO8601 Basic Format with hours and minutes                       |
  | xxx    | "+01:00"       | ISO8601 Extended Format with hours and minutes                    |
  | xxxx   | "+010059"      | ISO8601 Basic Format with hours and minutes, optional seconds     |
  | xxxxx  | "+01:00:10"    | ISO8601 Extended Format with hours and minutes, optional seconds  |

  ## Examples

      iex> Cldr.DateTime.Formatter.zone_iso %{time_zone: "Etc/UTC",
      ...>   utc_offset: 3610, std_offset: 0}, 1
      "+01"

      iex> Cldr.DateTime.Formatter.zone_iso %{time_zone: "Etc/UTC",
      ...>   utc_offset: 3610, std_offset: 0}, 2
      "+0100"

      iex> Cldr.DateTime.Formatter.zone_iso %{time_zone: "Etc/UTC",
      ...>   utc_offset: 3610, std_offset: 0}, 3
      "+01:00"

      iex> Cldr.DateTime.Formatter.zone_iso %{time_zone: "Etc/UTC",
      ...>   utc_offset: 3610, std_offset: 0}, 4
      "+010010"

      iex> Cldr.DateTime.Formatter.zone_iso %{time_zone: "Etc/UTC",
      ...>   utc_offset: 3610, std_offset: 0}, 5
      "+01:00:10"

      iex> Cldr.DateTime.Formatter.zone_iso %{time_zone: "Etc/UTC",
      ...>   utc_offset: 0, std_offset: 0}, 5
      "+00:00"

      iex> Cldr.DateTime.Formatter.zone_iso %{time_zone: "Etc/UTC",
      ...>   utc_offset: 0, std_offset: 0}, 4
      "+0000"

      iex> Cldr.DateTime.Formatter.zone_iso %{time_zone: "Etc/UTC",
      ...>   utc_offset: 0, std_offset: 0}, 3
      "+00:00"

      iex> Cldr.DateTime.Formatter.zone_iso %{time_zone: "Etc/UTC",
      ...>   utc_offset: 0, std_offset: 0}, 2
      "+0000"

      iex> Cldr.DateTime.Formatter.zone_iso %{time_zone: "Etc/UTC",
      ...>   utc_offset: 0, std_offset: 0}, 1
      "+00"

  """
  @spec zone_iso(Cldr.Calendar.date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def zone_iso(zone_iso, n \\ @default_format, options \\ [])

  def zone_iso(zone_iso, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    zone_iso(zone_iso, @default_format, locale, backend, Map.new(options))
  end

  def zone_iso(zone_iso, n, options) do
    {locale, backend} = extract_locale!(options)
    zone_iso(zone_iso, n, locale, backend, Map.new(options))
  end

  @iso_utc_offset_hours_minutes "+00:00"
  @spec zone_iso(
          Cldr.Calendar.date_time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def zone_iso(time, n, locale, backend, options \\ %{})

  def zone_iso(time, 1, _locale, _backend, options) do
    with {hours, minutes, seconds} <- Timezone.time_from_zone_offset(time) do
      iso8601_tz_format(%{hour: hours, minute: minutes, second: seconds}, format: :basic)
      |> String.replace(~r/00\Z/, "")
      |> maybe_wrap(:zone_iso, options)
    end
  end

  def zone_iso(time, 2, _locale, _backend, options) do
    with {hours, minutes, seconds} = Timezone.time_from_zone_offset(time) do
      iso8601_tz_format(%{hour: hours, minute: minutes, second: seconds}, format: :basic)
      |> maybe_wrap(:zone_iso, options)
    end
  end

  def zone_iso(time, 3, _locale, _backend, options) do
    with {hours, minutes, seconds} <- Timezone.time_from_zone_offset(time) do
      case {hours, minutes, seconds} do
        {0, 0, _} ->
          @iso_utc_offset_hours_minutes

        {hours, minutes, _seconds} ->
          iso8601_tz_format(%{hour: hours, minute: minutes, second: 0}, format: :extended)
      end
      |> maybe_wrap(:zone_iso, options)
    end
  end

  def zone_iso(time, 4, _locale, _backend, options) do
    with {hours, minutes, seconds} <- Timezone.time_from_zone_offset(time) do
      case {hours, minutes, seconds} do
        {hours, minutes, 0 = seconds} ->
          iso8601_tz_format(%{hour: hours, minute: minutes, second: seconds}, format: :basic)

        {hours, minutes, seconds} ->
          iso8601_tz_format(%{hour: hours, minute: minutes, second: seconds}, format: :basic) <>
            pad(seconds, 2)
      end
      |> maybe_wrap(:zone_iso, options)
    end
  end

  def zone_iso(time, 5, _locale, _backend, options) do
    with {hours, minutes, seconds} <- Timezone.time_from_zone_offset(time) do
      case {hours, minutes, seconds} do
        {0, 0, 0} ->
          @iso_utc_offset_hours_minutes

        {hours, minutes, seconds} ->
          iso8601_tz_format(%{hour: hours, minute: minutes, second: seconds}, format: :extended)
      end
      |> maybe_wrap(:zone_iso, options)
    end
  end

  def zone_iso(time, _n, _locale, _backend, _options) do
    error_return(time, "x", [:utc_offset])
  end

  @doc """
  Returns the short localised GMT offset (format symbol `O`) part of a
  `DateTime` or `Time`.

  ## Arguments

  * `time` is a `t:DateTime.t/0` struct or any map that contains at least the `:utc_offset`
    and `:std_offset` keys of the format used by `Time`

  * `n` is the specific non-location timezone format and is in the range `1..4`

  * `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`

  * `options` is a `Keyword` list of options.  There are no options used in
    `zone_gmt/4`

  ## Format Symbol

  The representation of the `GMT offset` is made in accordance with the following
  table:

  | Symbol | Results        | Description                                                     |
  | :----  | :------------- | :-------------------------------------------------------------- |
  | O      | "GMT+1"        | Short localised GMT format                                      |
  | OOOO   | "GMT+01:00"    | Long localised GMT format                                       |

  ## Examples

      iex> Cldr.DateTime.Formatter.zone_gmt %{time_zone: "Etc/UTC",
      ...>   utc_offset: 3610, std_offset: 0}, 1
      "GMT+1"

      iex> Cldr.DateTime.Formatter.zone_gmt %{time_zone: "Etc/UTC",
      ...>   utc_offset: 3610, std_offset: 0}, 4
      "GMT+01:00"

  """
  @spec zone_gmt(Cldr.Calendar.date_time(), integer, Keyword.t()) ::
          String.t() | {:error, String.t()}

  def zone_gmt(zone_gmt, n \\ @default_format, options \\ [])

  def zone_gmt(zone_gmt, options, []) when is_list(options) do
    {locale, backend} = extract_locale!(options)
    zone_gmt(zone_gmt, @default_format, locale, backend, Map.new(options))
  end

  def zone_gmt(zone_gmt, n, options) do
    {locale, backend} = extract_locale!(options)
    zone_gmt(zone_gmt, n, locale, backend, Map.new(options))
  end

  @spec zone_gmt(
          Cldr.Calendar.date_time(),
          integer,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def zone_gmt(time, n, locale, backend, options \\ %{})

  def zone_gmt(time, 1, locale, backend, options) do
    with {hours, minutes, seconds} <- Timezone.time_from_zone_offset(time) do
      backend = Module.concat(backend, DateTime.Formatter)

      backend.gmt_tz_format(locale, %{hour: hours, minute: minutes, second: seconds}, format: :short)
      |> maybe_wrap(:zone_gmt, options)
    end
  end

  def zone_gmt(time, 4, locale, backend, options) do
    with {hours, minutes, seconds} <- Timezone.time_from_zone_offset(time) do
      backend = Module.concat(backend, DateTime.Formatter)

      backend.gmt_tz_format(locale, %{hour: hours, minute: minutes, second: seconds}, format: :long)
      |> maybe_wrap(:zone_gmt, options)
    end
  end

  def zone_gmt(time, _n, _locale, _backend, _options) do
    error_return(time, "O", [:utc_offset])
  end

  @doc """
  Returns a literal.

  ## Example

      iex> Cldr.DateTime.Formatter.literal %{time_zone:
      ...>   "Etc/UTC", utc_offset: 0, std_offset: 0}, "A literal"
      "A literal"

  """
  @spec literal(any(), String.t(), Keyword.t()) :: String.t()

  def literal(date, literal, options \\ []) do
    {locale, backend} = extract_locale!(options)
    literal(date, literal, locale, backend, Map.new(options))
  end

  @spec literal(any(), String.t(), Locale.locale_reference(), Cldr.backend(), map()) :: String.t()

  def literal(_date, binary, locale, backend, options \\ %{})

  def literal(_date, binary, _locale, _backend, options) do
    binary
    |> maybe_wrap(:literal, options)
  end

  @doc """
  Inserts the decimal separator appropriate for the
  given locale, number system and separators.

  """
  @spec decimal_separator(
          Cldr.Calendar.any_date_time(),
          atom,
          Locale.locale_reference(),
          Cldr.backend(),
          map()
        ) ::
          String.t() | {:error, String.t()}

  def decimal_separator(_date, nil, locale, backend, options) do
    {:ok, symbols} = Cldr.Number.Symbol.number_symbols_for(locale, backend)
    number_system = options.number_system
    separators = options.separators

    decimal_separators =
      symbols
      |> Map.fetch!(number_system)
      |> Map.fetch!(:decimal)

    Map.get(decimal_separators, separators) || Map.fetch!(decimal_separators, @default_separators)
  end

  # Helpers

  # ISO 8601 time zone formats:
  # The ISO 8601 basic format does not use a separator character between hours
  # and minutes field, while the extended format uses colon (':') as the
  # separator. The ISO 8601 basic format with hours and minutes fields is
  # equivalent to RFC 822 zone format.
  #
  # "-0800" (basic)
  # "-08" (basic - short)
  # "-08:00" (extended)
  # "Z" (UTC)
  defp iso8601_tz_format(%{hour: _hour, minute: _minute} = time, options) do
    iso8601_tz_format_type(time, options[:format] || :basic)
  end

  defp iso8601_tz_format_type(%{hour: 0, minute: 0}, :extended) do
    "Z"
  end

  defp iso8601_tz_format_type(%{hour: hour, minute: _minute} = time, :basic) do
    sign(hour) <> h23(time, 2) <> minute(time, 2)
  end

  defp iso8601_tz_format_type(%{hour: hour, minute: _minute} = time, :short) do
    sign(hour) <> h23(time, 2)
  end

  defp iso8601_tz_format_type(%{hour: hour, minute: _minute} = time, :long) do
    sign(hour) <> h23(time, 2) <> ":" <> minute(time, 2)
  end

  defp iso8601_tz_format_type(%{hour: hour, minute: _minute, second: 0} = time, :extended) do
    sign(hour) <> h23(time, 2) <> ":" <> minute(time, 2)
  end

  defp iso8601_tz_format_type(%{hour: hour, minute: _minute, second: _second} = time, :extended) do
    sign(hour) <> h23(time, 2) <> ":" <> minute(time, 2) <> ":" <> second(time, 2)
  end

  defp iso8601_tz_format_type(%{hour: hour, minute: _minute} = time, :extended) do
    sign(hour) <> h23(time, 2) <> ":" <> minute(time, 2)
  end

  defp sign(number) when number >= 0, do: "+"
  defp sign(_number), do: "-"

  defp pad(integer, n) when is_integer(integer) and integer >= 0 do
    padding = n - number_of_digits(integer)

    if padding <= 0 do
      Integer.to_string(integer)
    else
      :erlang.iolist_to_binary([List.duplicate(?0, padding), Integer.to_string(integer)])
    end
  end

  defp pad(integer, n) when is_integer(integer) and integer < 0 do
    :erlang.iolist_to_binary([?-, pad(abs(integer), n)])
  end

  defp pad(integer, n) when is_binary(integer) do
    len = String.length(integer)

    if len >= n do
      integer
    else
      String.duplicate("0", n - len) <> integer
    end
  end

  defp right_fill(string, n) do
    len = String.length(string)

    if len >= n do
      string
    else
      fill = String.duplicate("0", n - len)
      string <> fill
    end
  end

  # This should be more performant than doing
  # Enum.count(Integer.digits(n)) for all cases
  # defp number_of_digits(n) when n < 0, do: number_of_digits(abs(n))
  defp number_of_digits(n) when n < 10, do: 1
  defp number_of_digits(n) when n < 100, do: 2
  defp number_of_digits(n) when n < 1_000, do: 3
  defp number_of_digits(n) when n < 10_000, do: 4
  defp number_of_digits(n) when n < 100_000, do: 5
  defp number_of_digits(n) when n < 1_000_000, do: 6
  defp number_of_digits(n) when n < 10_000_000, do: 7
  defp number_of_digits(n) when n < 100_000_000, do: 8
  defp number_of_digits(n) when n < 1_000_000_000, do: 9
  defp number_of_digits(n) when n < 10_000_000_000, do: 10
  defp number_of_digits(n), do: Enum.count(Integer.digits(n))

  @doc false
  @dialyzer {:nowarn_function, error_return: 3}
  def error_return(map, symbol, requirements) do
    requirements =
      requirements
      |> Enum.map(&inspect/1)
      |> join_requirements

    raise Cldr.DateTime.FormatError,
          "The format symbol '#{symbol}' requires at map with at least #{requirements}. Found: #{inspect(map)}"

    :error
  end

  @doc false
  def join_requirements([]) do
    ""
  end

  def join_requirements([head]) do
    head
  end

  def join_requirements([head, tail]) do
    "#{head} and #{tail}"
  end

  def join_requirements([head | tail]) do
    to_string(head) <> ", " <> join_requirements(tail)
  end

  defp extract_locale!(options) do
    locale = Keyword.get(options, :locale, Cldr.get_locale())
    backend = Keyword.get(options, :backend)

    with {:ok, locale} <- Cldr.validate_locale(locale, backend) do
      {locale, backend || locale.backend}
    else
      {:error, {exception, reason}} -> raise exception, reason
    end
  end

  # Transliterate a string for a specific format code

  defp transliterate(number, format_code, backend, %{_number_systems: number_systems}) do
    if number_system = Map.get(number_systems, format_code) do
      Cldr.Number.System.to_system!(number, number_system, backend)
    else
      number
    end
  end

  defp transliterate(number, _format_code, _backend, _options) do
    number
  end

  def maybe_wrap(value, type, %{wrapper: wrapper}) when is_function(wrapper, 2) do
    wrapper.(value, type)
  end

  def maybe_wrap(value, _type, _options) do
    value
  end
end
