lib/cldr/backend/compiler.ex

defmodule Cldr.DateTime.Compiler do
  @moduledoc """
  Tokenizes and parses `Date`, `Time` and `DateTime` format strings.

  During compilation, each of the date, time and datetime format
  strings defined in CLDR are compiled into a list of
  function bodies that are then grafted onto the function head
  `format/3` in a backend module.  As a result these compiled
  formats execute with good performance.

  For formats not defined in CLDR (ie a user defined format),
  the tokenizing and parsing is performed, then list of function
  bodies is created and then `format/3`
  recurses over the list, invoking each function and
  collecting the results.  This process is significantly slower
  than that of the precompiled formats.

  User defined formats can also be precompiled by configuring
  them under the key `:precompile_datetime_formats`.  For example:

      config :ex_cldr,
        precompile_datetime_formats: ["yy/dd", "hhh:mmm:sss"]

  """

  @doc """
  Scan a number format definition and return
  the tokens of a date/time/datetime format
  string.

  This function is designed to produce output
  that is fed into `Cldr.DateTime.Compiler.compile/3`.

  ## Arguments

  * `definition` is a date, datetime or time format
    string

  ## Returns

  A list of 3-tuples which represent the tokens
  of the format definition

  ## Example

      iex> Cldr.DateTime.Compiler.tokenize "yyyy/MM/dd"
      {:ok,
       [{:year, 1, 4}, {:literal, 1, "/"}, {:month, 1, 2}, {:literal, 1, "/"},
        {:day_of_month, 1, 2}], 1}

  """
  def tokenize(definition) when is_binary(definition) do
    definition
    |> String.to_charlist()
    |> :datetime_format_lexer.string()
  end

  def tokenize(%{number_system: _numbers, format: value}) do
    tokenize(value)
  end

  @doc """
  Parse a number format definition

  ## Arguments

  * `format_string` is a string defining how a date/time/datetime
  is to be formatted.  See `Cldr.DateTime.Formatter` for the list
  of supported format symbols.

  ## Returns

  Returns a list of function bodies which are grafted onto
  a function head in `Cldr.DateTime.Formatter` at compile time
  to produce a series of functions that process a given format
  string efficiently.

  """
  @spec compile(String.t(), module(), module()) ::
          {:ok, Cldr.Calendar.calendar()} | {:error, String.t()}
  def compile(format_string, backend, context)

  def compile("", _, _) do
    {:error, "empty format string cannot be compiled"}
  end

  def compile(nil, _, _) do
    {:error, "no format string or token list provided"}
  end

  def compile(definition, backend, context) when is_binary(definition) do
    with {:ok, tokens, _end_line} <- tokenize(definition) do
      transforms =
        Enum.map(tokens, fn {fun, _line, count} ->
          quote do
            Cldr.DateTime.Formatter.unquote(fun)(
              var!(date, unquote(context)),
              unquote(count),
              var!(locale, unquote(context)),
              unquote(backend),
              var!(options, unquote(context))
            )
          end
        end)

      {:ok, transforms}
    else
      error ->
        raise ArgumentError, "Could not parse #{inspect(definition)}: #{inspect(error)}"
    end
  end

  def compile(%{number_system: _number_system, format: value}, backend, context) do
    compile(value, backend, context)
  end

  def compile(arg, _, _) do
    raise ArgumentError, message: "No idea how to compile format: #{inspect(arg)}"
  end
end