lib/cldr/list/backend.ex

defmodule Cldr.List.Backend do
  def define_list_module(config) do
    module = inspect(__MODULE__)
    backend = config.backend
    config = Macro.escape(config)

    quote location: :keep, bind_quoted: [module: module, backend: backend, config: config] do
      defmodule List do
        @moduledoc false
        if Cldr.Config.include_module_docs?(config.generate_docs) do
          @moduledoc """
          Cldr backend module that formats lists.

          If we have a list of days like `["Monday", "Tuesday", "Wednesday"]`
          then we can format that list for a given locale by:

              iex> #{inspect __MODULE__}.to_string(["Monday", "Tuesday", "Wednesday"], locale: "en")
              {:ok, "Monday, Tuesday, and Wednesday"}

          """
        end

        @default_format :standard

        alias Cldr.Substitution
        alias Cldr.Locale

        @doc """
        Formats a list into a string according to the list pattern rules for a locale.

        ## Arguments

        * `list` is any list of of terms that can be passed through `Kernel.to_string/1`

        * `options` is a keyword list

        ## Options

        * `:locale` is any configured locale. See . The default
          is `#{inspect backend}.known_locale_names/0`.

        * `:format` is any of those returned by
          `Cldr.List.known_list_formats/0` or by `Cldr.List.Pattern.new/1`.
          The default is `format: :standard`.

        ## Examples

            iex> #{inspect __MODULE__}.to_string(["a", "b", "c"], locale: "en")
            {:ok, "a, b, and c"}

            iex> #{inspect __MODULE__}.to_string(["a", "b", "c"], locale: "en", format: :unit_narrow)
            {:ok, "a b c"}

            iex> #{inspect __MODULE__}.to_string(["a", "b", "c"], locale: "fr")
            {:ok, "a, b et c"}

            iex> #{inspect __MODULE__}.to_string([1,2,3,4,5,6])
            {:ok, "1, 2, 3, 4, 5, and 6"}

            iex> #{inspect __MODULE__}.to_string(["a"])
            {:ok, "a"}

            iex> #{inspect __MODULE__}.to_string([1,2])
            {:ok, "1 and 2"}

        """
        @spec to_string(list(term()), Keyword.t()) ::
                {:ok, String.t()} | {:error, {module(), String.t()}}

        def to_string(list, options \\ []) do
          with {:ok, list} <- intersperse(list, options) do
            string =
              list
              |> Enum.map(&to_string/1)
              |> :erlang.iolist_to_binary

            {:ok, string}
          end
        end

        @doc """
        Formats a list using `to_string/2` but raises if there is
        an error.

        ## Examples

            iex> #{inspect __MODULE__}.to_string!(["a", "b", "c"], locale: "en")
            "a, b, and c"

            iex> #{inspect __MODULE__}.to_string!(["a", "b", "c"], locale: "en", format: :unit_narrow)
            "a b c"

        """
        @spec to_string!(list(term()), Keyword.t()) :: String.t() | no_return()
        def to_string!(list, options \\ []) do
          case to_string(list, options) do
            {:error, {exception, message}} ->
              raise exception, message

            {:ok, string} ->
              string
          end
        end

        @doc """
        Intersperces a list elements into a list format according to the list
        pattern rules for a locale.

        This function can be helpful when creating a list from `Phoenix`
        safe strings which are of the format `{:safe, "some string"}`

        ## Arguments

        * `list` is any list of of terms that can be passed through `Kernel.to_string/1`

        * `options` is a keyword list

        ## Options

        * `:locale` is any configured locale. See . The default
          is `#{inspect backend}.known_locale_names/0`.

        * `:format` is any of those returned by
          `Cldr.List.known_list_formats/0` or by `Cldr.List.Pattern.new/1`.
          The default is `format: :standard`.

        ## Examples

            iex> #{inspect __MODULE__}.intersperse(["a", "b", "c"], locale: "en")
            {:ok, ["a", ", ", "b", ", and ", "c"]}

            iex> #{inspect __MODULE__}.intersperse(["a", "b", "c"], locale: "en", format: :unit_narrow)
            {:ok, ["a", " ", "b", " ", "c"]}

            iex> #{inspect __MODULE__}.intersperse(["a", "b", "c"], locale: "fr")
            {:ok, ["a", ", ", "b", " et ", "c"]}

            iex> #{inspect __MODULE__}.intersperse([1,2,3,4,5,6])
            {:ok, [1, ", ", 2, ", ", 3, ", ", 4, ", ", 5, ", and ", 6]}

            iex> #{inspect __MODULE__}.intersperse(["a"])
            {:ok, ["a"]}

            iex> #{inspect __MODULE__}.intersperse([1,2])
            {:ok, [1, " and ", 2]}

        """
        @spec intersperse(list(term()), Keyword.t()) ::
                {:ok, list()} | {:error, {module(), String.t()}}

        def intersperse(list, options \\ [])

        def intersperse([], _options) do
          {:ok, []}
        end

        def intersperse(list, options) do
          with {:ok, locale, pattern} <- normalize_options(options) do
            list =
              list
              |> intersperse(locale, pattern)
              |> Elixir.List.flatten

            {:ok, list}
          end
        end

        # For when the list is empty
        def intersperse([], _locale, _pattern) do
          []
        end

        # For when there is one element only
        def intersperse([first], _locale, _pattern) do
          [first]
        end

        # For when there are two elements only
        def intersperse([first, last], locale, pattern) do
          Substitution.substitute([first, last], pattern.two)
        end

        # For when there are three elements only
        def intersperse([first, middle, last], locale, pattern) do
          last = Substitution.substitute([middle, last], pattern.end)
          Substitution.substitute([first, last], pattern.start)
        end

        # For when there are more than 3 elements
        def intersperse([first | rest], locale, pattern) do
          Substitution.substitute([first, intersperse(rest, locale, pattern)], pattern.start)
        end

        @doc """
        Formats a list using `intersperse/2` but raises if there is
        an error.

        ## Examples

            iex> #{inspect __MODULE__}.intersperse!(["a", "b", "c"], locale: "en")
            ["a", ", ", "b", ", and ", "c"]

            iex> #{inspect __MODULE__}.intersperse!(["a", "b", "c"], locale: "en", format: :unit_narrow)
            ["a", " ", "b", " ", "c"]

        """
        @spec intersperse!(list(term()), Keyword.t()) :: list(String.t()) | no_return()
        def intersperse!(list, options \\ []) do
          case intersperse(list, options) do
            {:error, {exception, message}} ->
              raise exception, message

            {:ok, list} ->
              list
          end
        end

        @spec normalize_options(Keyword.t()) ::
          {:ok, LanguageTag.t(), atom()} | {:error, {module(), String.t()}}

        defp normalize_options(options) do
          locale = options[:locale] || unquote(backend).get_locale()
          format = options[:format] || options[:style] || @default_format

          with {:ok, locale} <- unquote(backend).validate_locale(locale),
               {:ok, pattern} <- verify_format(locale.cldr_locale_name, format) do
            {:ok, locale, pattern}
          end
        end

        @spec verify_format(Locale.locale_name(), atom() | Cldr.List.Pattern.t()) ::
          {:ok, atom()} | {:error, {module(), String.t()}}

        defp verify_format(_locale_name, %Cldr.List.Pattern{} = pattern) do
          {:ok, pattern}
        end

        defp verify_format(locale_name, format) do
          if pattern = Map.get(list_patterns_for(locale_name), format) do
            {:ok, pattern}
          else
            {:error,
             {Cldr.UnknownFormatError, "The list format #{inspect(format)} is not known."}}
          end
        end

        @spec list_patterns_for(Locale.locale_name()) :: map()
        @spec list_formats_for(Locale.locale_name()) :: [atom]

        for locale_name <- Cldr.Locale.Loader.known_locale_names(config) do
          patterns =
            locale_name
            |> Cldr.Locale.Loader.get_locale(config)
            |> Map.get(:list_formats)
            |> Enum.map(fn {k, v} ->
              patterns =
                Cldr.List.Pattern
                |> struct(v)
                |> Map.put(:two, Map.fetch!(v, 2))

              {k, patterns}
            end)
            |> Map.new()

          pattern_names =
            Map.keys(patterns)
            |> Enum.sort()

          @doc """
          Returns the list patterns for a locale.

          List patterns provide rules for combining multiple
          items into a language format appropriate for a locale.

          ## Example

              iex> #{inspect __MODULE__}.list_patterns_for(:en)
              %{
                or: %Cldr.List.Pattern{
                  two: [0, " or ", 1],
                  end: [0, ", or ", 1],
                  middle: [0, ", ", 1],
                  start: [0, ", ", 1]
                },
                or_narrow: %Cldr.List.Pattern{
                  two: [0, " or ", 1],
                  end: [0, ", or ", 1],
                  middle: [0, ", ", 1],
                  start: [0, ", ", 1]
                },
                or_short: %Cldr.List.Pattern{
                  two: [0, " or ", 1],
                  end: [0, ", or ", 1],
                  middle: [0, ", ", 1],
                  start: [0, ", ", 1]
                },
                standard: %Cldr.List.Pattern{
                  two: [0, " and ", 1],
                  end: [0, ", and ", 1],
                  middle: [0, ", ", 1],
                  start: [0, ", ", 1]
                },
                standard_narrow: %Cldr.List.Pattern{
                  two: [0, ", ", 1],
                  end: [0, ", ", 1],
                  middle: [0, ", ", 1],
                  start: [0, ", ", 1]
                },
                standard_short: %Cldr.List.Pattern{
                  two: [0, " & ", 1],
                  end: [0, ", & ", 1],
                  middle: [0, ", ", 1],
                  start: [0, ", ", 1]
                },
                unit: %Cldr.List.Pattern{
                  two: [0, ", ", 1],
                  end: [0, ", ", 1],
                  middle: [0, ", ", 1],
                  start: [0, ", ", 1]
                },
                unit_narrow: %Cldr.List.Pattern{
                  two: [0, " ", 1],
                  end: [0, " ", 1],
                  middle: [0, " ", 1],
                  start: [0, " ", 1]
                },
                unit_short: %Cldr.List.Pattern{
                  two: [0, ", ", 1],
                  end: [0, ", ", 1],
                  middle: [0, ", ", 1],
                  start: [0, ", ", 1]
                }
              }

          """
          def list_patterns_for(unquote(locale_name)) do
            unquote(Macro.escape(patterns))
          end

          @doc """
          Returns the styles of list patterns available for a locale.

          Returns a list of `atom`s of of the list formats that are
          available in CLDR for a locale.

          ## Example

              iex> #{inspect __MODULE__}.list_formats_for(:en)
              [:or, :or_narrow, :or_short, :standard, :standard_narrow, :standard_short,
               :unit, :unit_narrow, :unit_short]

          """
          def list_formats_for(unquote(locale_name)) do
            unquote(pattern_names)
          end
        end

        def list_patterns_for(locale_name) when is_binary(locale_name) do
          with {:ok, locale} <- unquote(backend).validate_locale(locale_name) do
            list_patterns_for(locale.cldr_locale_name)
          end
        end

        def list_formats_for(locale_name) when is_binary(locale_name) do
          with {:ok, locale} <- unquote(backend).validate_locale(locale_name) do
            list_formats_for(locale.cldr_locale_name)
          end
        end

        # TODO remove as of version 3.0
        # @deprecated "Use #{__MODULE__}.list_formats_for/1"
        @doc false
        defdelegate list_styles_for(locale), to: __MODULE__, as: :list_formats_for

        # TODO remove as of version 3.0
        # @deprecated "Use #{__MODULE__}.list_formats_for/1"
        @doc false
        defdelegate list_pattern_styles_for(locale), to: __MODULE__, as: :list_formats_for

      end
    end
  end
end