lib/cldr/pattern.ex

defmodule Cldr.List.Pattern do
  @moduledoc """
  A list pattern drives list formatting and it defines how to combine
  list elements at the begging, in the middle and at the end of
  a list. It also has a special pattern used when the list contains
  only two elements.

  """

  defstruct [:start, :middle, :end, :two]

  @type pattern :: [non_neg_integer() | String.t(), ...]

  @type t :: %__MODULE__{
    start: pattern(),
    middle: pattern(),
    end: pattern(),
    two: pattern()
  }

  @valid_options [:start, :middle, :end, :two]

  @doc """
  Creates a new list format.

  A list pattern consists of four string
  templates into which list elements are interpolated
  when formatting.

  The four templates are:

  * `:start` used to format the first two
    list elements.

  * `:middle` is used to format elements in
    the middle of the list.

  * `:end` is used to format the last two
    elements in the list.

  * `:two` is used to format a list if
    it contains only two elements.

  Only the `:start` option is required. It
  will be used as the default for any other
  option that is not provided.

  ## Arguments

  * `options` is a keyword list of options.

  ## Options

  * `:start` is a pattern template as a string.
    This option is required.

  * `:middle` is a pattern template as a string.

  * `:end` is a pattern template as a string.

  * `:two` is a pattern template as a string.

  ## Returns

  `{:ok, pattern}` or

  `{:error, reason}`

  ## Pattern template

  A pattern template is a string that contains
  two placeholders denoted by `{0}` and `{1}`.

  ## Example

      iex> Cldr.List.Pattern.new(
      ...>   start: "{0}, {1}"),
      ...>   middle: "{0}, {1}"),
      ...>   end: "{0} and {1}"),
      ...>   two: "{0} and {1}")
      ...>  )
      {:ok, _pattern}

  """
  def new(options) when is_list(options) do
    with {:ok, options} <- validate_options(options) do
      start = Keyword.get(options, :start)

      struct(__MODULE__, options)
      |> put_new(:middle, start)
      |> put_new(:end, start)
      |> put_new(:two, start)
      |> wrap(:ok)
    end
  end

  defp validate_options(options) when is_list(options) do
    options =
      Enum.reduce_while options, [], fn
        {key, string}, acc when key in @valid_options and is_binary(string) ->
          case validate_pattern(string) do
            {:error, reason} ->
              {:halt, {:error, reason}}

            {:ok, string} ->
              pattern = Cldr.Substitution.parse(string)
              {:cont, [{key, pattern} | acc]}
          end

        {key, value}, _acc when key in @valid_options ->
          {:halt, {:error, "Invalid value #{inspect value} found. Option #{inspect key} should be a string"}}

        {key, _value}, _acc ->
          {:halt, {:error, "Invalid option #{inspect key} found. Valid options are #{inspect @valid_options}"}}
      end

      case options do
        {:error, reason} ->
          {:error, reason}

        options ->
          if Keyword.has_key?(options, :start),
            do: {:ok, options},
            else: {:error, "Option :start must be provided"}
      end
  end

  defp validate_pattern(string) when is_binary(string) do
    if String.contains?(string, "{0}") && String.contains?(string, "{1}") do
      {:ok, string}
    else
      {:error, "Invalid pattern #{inspect string}. A pattern must have two placeholders {0} and {1}"}
    end
  end

  defp put_new(options, key, default) do
    if Map.get(options, key), do: options, else: Map.put(options, key, default)
  end

  defp wrap(term, atom) do
    {atom, term}
  end
end