lib/localize/inputs/date/parser.ex

defmodule Localize.Inputs.Date.Parser do
  @moduledoc """
  Front-door parser for locale-aware date form input.

  Delegates to `Calendrical.Date.parse/2`. The input layer does
  no parsing of its own — this module is a thin policy layer
  over the underlying CLDR-driven parser.

  """

  @doc """
  Parses a locale-formatted date string.

  Tries, in order: bare ISO-8601 (`YYYY-MM-DD`), then every
  CLDR `availableFormats` skeleton for the (locale, calendar)
  tuple. Skeletons encode the locale's preferred field order
  (and any era / week-numbering / quarter conventions), so the
  same input may parse to different dates under different
  locales — by design.

  Returns a `Date` in the calendar identified by the `:calendar`
  option — `Calendar.ISO` for `:gregorian` (the default),
  `Calendrical.Hebrew` for `:hebrew`, `Calendrical.Japanese` for
  `:japanese`, and so on. Pass `return_calendar: :iso` to force
  the result into Gregorian regardless.

  ### Arguments

  * `string` is the raw user input.

  * `options` is a keyword list of options.

  ### Options

  * `:locale` — the locale to interpret the string under. Defaults
    to `Localize.get_locale/0`.

  * `:calendar` — the CLDR calendar key whose patterns to try
    AND the calendar of the returned `Date`. Defaults to
    `:gregorian`.

  * `:reference_date` — the "today" anchor used for two-digit-year
    pivoting. Defaults to `Date.utc_today/0`.

  * `:return_calendar` — `:native` (default) returns the parsed
    date in whatever `:calendar` named. `:iso` forces
    `Calendar.ISO`.

  ### Returns

  * `{:ok, Date.t()}` on success.

  * `{:ok, nil}` for blank input.

  * `{:error, Exception.t()}` on parse failure.

  ### Examples

      iex> Localize.Inputs.Date.Parser.parse_date("2026-05-16", locale: :en)
      {:ok, ~D[2026-05-16]}

      iex> Localize.Inputs.Date.Parser.parse_date("5/16/26", locale: :en)
      {:ok, ~D[2026-05-16]}

      iex> Localize.Inputs.Date.Parser.parse_date("16/05/2026", locale: :"en-GB")
      {:ok, ~D[2026-05-16]}

      iex> Localize.Inputs.Date.Parser.parse_date("16.05.2026", locale: :de)
      {:ok, ~D[2026-05-16]}

      iex> Localize.Inputs.Date.Parser.parse_date("Q2 2026", locale: :en)
      {:ok, ~D[2026-04-01]}

      iex> Localize.Inputs.Date.Parser.parse_date("week 20 of 2026", locale: :en)
      {:ok, ~D[2026-05-11]}

      iex> Localize.Inputs.Date.Parser.parse_date("Saturday, May 16, 2026", locale: :en)
      {:ok, ~D[2026-05-16]}

      iex> Localize.Inputs.Date.Parser.parse_date("2026-05-16", locale: :en, calendar: :hebrew)
      {:ok, ~D[5786-09-29 Calendrical.Hebrew]}

      iex> Localize.Inputs.Date.Parser.parse_date("", locale: :en)
      {:ok, nil}

  """
  @spec parse_date(String.t() | nil, Keyword.t()) ::
          {:ok, Date.t() | nil} | {:error, Exception.t()}
  def parse_date(string, options \\ [])
  def parse_date(nil, _options), do: {:ok, nil}
  def parse_date("", _options), do: {:ok, nil}

  def parse_date(string, options) when is_binary(string) do
    Calendrical.Date.parse(String.trim(string), options)
  end
end