lib/cldr/number/format/meta.ex

defmodule Cldr.Number.Format.Meta do
  @moduledoc """
  Describes the metadata that drives
  number formatting and provides functions to
  update the struct.

  ## Format definition

  The `:format` is a keyword list that with two
  elements:

  * `:positive` which is a keyword list for
    formatting a number >= zero

  * `:negative` which is a keyword list for
    formatting negative number

  There are two formats because we can format in
  an accounting style (that is, numbers surrounded
  by `()`) or any other arbitrary style. Typically
  the format for a negative number is the same as
  that for a positive number with a prepended
  minus sign.

  ## Localisation of number formatting

  Number formatting is always localised to either
  the currency processes locale or a locale
  provided as an option to `Cldr.Number.to_string/3`.

  The metadata is independent of the localisation
  process. Signs (`+`/`-`), grouping (`,`), decimal markers
  (`.`) and exponent signs are all localised when
  the number is formatted.

  ## Formatting directives

  The formats - positive and negative - are defined
  in the metadata struct, as a keyword list of keywords
  and values.

  The simplest formatting list might be:
  ```
  [format: _]`
  ```
  The `:format` keyword indicates
  that this is where the formatting number will be
  substituted into the format pattern.

  Another example would be for formatting a negative
  number:
  ```
  [minus: _, format: _]
  ```
  which will format with a localised minus sign
  followed by the formatted number. Note that the
  keyword value for `:minus` and `:format` are
  ignored.

  ## List of formatting keywords

  The following is the full list of formatting
  keywords which can be used to format a
  number. A `_` in the keyword format is
  used to denote `:dont_care`.

  * `[format: _]` inserts the formatted number
    exclusive of any sign

  * `[minus: _]` inserts a localised minus
    sign

  * `[plus: _]` inserts a localised plus sign

  * `[percent: _]` inserts a localised percent sign

  * `[permille: _]` inserts a localised permille sign

  * `[literal: "string"]` inserts `string` into the
    format without any processing

  * `[currency: 1..4]` inserts a localised currency
    symbol of the given `type`.  A `:currency` must be
    provided as an option to `Cldr.Number.Formatter.Decimal.to_string/4`.

  * `[pad: "char"]` inserts the correct number of `char`s
    to pad the number format to the width specified by
    `:padding_length` in the `%Meta{}` struct. The `:pad`
    can be anywhere in the format list but it is most
    typically inserted before or after the `:format`
    keyword.  The assumption is that the `char` is a single
    binary character but this is not checked.

  ## Currency symbol formatting

  Currency are localised and have four ways of being
  presented.  The different types are defined in the
  `[currency: type]` keyword where `type` is an integer
  in the range `1..4`  These types will insert
  into the final format:

    1. The standard currency symbol like `$`,`¥` or `€`
    2. The ISO currency code (like `USD` and `JPY`)
    3. The localised and pluralised currency display name
       like "Australian dollar" or "Australian dollars"
    4. The narrow currency symbol if defined for a locale

  """
  defstruct integer_digits: %{max: 0, min: 1},
            fractional_digits: %{max: 0, min: 0},
            significant_digits: %{max: 0, min: 0},
            exponent_digits: 0,
            exponent_sign: false,
            scientific_rounding: 0,
            grouping: %{
              fraction: %{first: 0, rest: 0},
              integer: %{first: 0, rest: 0}
            },
            round_nearest: 0,
            padding_length: 0,
            padding_char: " ",
            multiplier: 1,
            currency: nil,
            format: [
              positive: [format: "#"],
              negative: [minus: ~c"-", format: :same_as_positive]
            ],
            number: 0

  @typedoc "Metadata type that drives how to format a number"
  @type t :: %__MODULE__{
          :currency => nil | Cldr.Currency.t(),
          :exponent_digits => non_neg_integer(),
          :exponent_sign => boolean(),
          :format => [{:negative, [any(), ...]} | {:positive, [any(), ...]}, ...],
          :fractional_digits => %{:max => non_neg_integer(), :min => non_neg_integer()},
          :grouping => %{
            :fraction => %{:first => non_neg_integer(), :rest => non_neg_integer()},
            :integer => %{:first => non_neg_integer(), :rest => non_neg_integer()}
          },
          :integer_digits => %{:max => non_neg_integer(), :min => non_neg_integer()},
          :multiplier => non_neg_integer(),
          :number => non_neg_integer(),
          :padding_char => String.t(),
          :padding_length => non_neg_integer(),
          :round_nearest => non_neg_integer(),
          :scientific_rounding => non_neg_integer(),
          :significant_digits => %{:max => non_neg_integer(), :min => non_neg_integer()}
        }

  @doc """
  Returns a new number formatting metadata
  struct.

  """
  @spec new :: t()
  def new do
    %__MODULE__{}
  end

  @doc """
  Set the minimum, and optionally maximum, integer digits to
  format.

  """
  @spec put_integer_digits(t(), non_neg_integer, non_neg_integer) :: t()
  def put_integer_digits(%__MODULE__{} = meta, min, max \\ 0)
      when is_integer(min) and is_integer(max) do
    meta
    |> Map.put(:integer_digits, %{min: min, max: max})
  end

  @doc """
  Set the minimum, and optionally maximum, fractional digits to
  format.

  """
  @spec put_fraction_digits(t(), non_neg_integer, non_neg_integer) :: t()
  def put_fraction_digits(%__MODULE__{} = meta, min, max \\ 0)
      when is_integer(min) and is_integer(max) do
    meta
    |> Map.put(:fractional_digits, %{min: min, max: max})
  end

  @doc """
  Set the minimum, and optionally maximum, significant digits to
  format.

  """
  @spec put_significant_digits(t(), non_neg_integer, non_neg_integer) :: t()
  def put_significant_digits(%__MODULE__{} = meta, min, max \\ 0)
      when is_integer(min) and is_integer(max) do
    meta
    |> Map.put(:significant_digits, %{min: min, max: max})
  end

  @doc """
  Set the number of exponent digits to
  format.

  """
  @spec put_exponent_digits(t(), non_neg_integer) :: t()
  def put_exponent_digits(%__MODULE__{} = meta, digits) when is_integer(digits) do
    meta
    |> Map.put(:exponent_digits, digits)
  end

  @doc """
  Set whether to add the sign of the exponent to
  the format.

  """
  @spec put_exponent_sign(t(), boolean) :: t()
  def put_exponent_sign(%__MODULE__{} = meta, flag) when is_boolean(flag) do
    meta
    |> Map.put(:exponent_sign, flag)
  end

  @doc """
  Set the increment to which the number should
  be rounded.

  """
  @spec put_round_nearest_digits(t(), non_neg_integer) :: t()
  def put_round_nearest_digits(%__MODULE__{} = meta, digits) when is_integer(digits) do
    meta
    |> Map.put(:round_nearest, digits)
  end

  @doc """
  Set the number of scientific digits to which the number should
  be rounded.

  """
  @spec put_scientific_rounding_digits(t(), non_neg_integer) :: t()
  def put_scientific_rounding_digits(%__MODULE__{} = meta, digits) when is_integer(digits) do
    meta
    |> Map.put(:scientific_rounding, digits)
  end

  @spec put_padding_length(t(), non_neg_integer) :: t()
  def put_padding_length(%__MODULE__{} = meta, digits) when is_integer(digits) do
    meta
    |> Map.put(:padding_length, digits)
  end

  @doc """
  Set the padding character to be used when
  padding the formatted number.

  """
  @spec put_padding_char(t(), String.t()) :: t()
  def put_padding_char(%__MODULE__{} = meta, char) when is_binary(char) do
    meta
    |> Map.put(:padding_char, char)
  end

  @doc """
  Sets the multiplier for the number.

  Before formatting, the number is multiplied
  by this amount.  This is useful when
  formatting as a percent or permille.

  """
  @spec put_multiplier(t(), non_neg_integer) :: t()
  def put_multiplier(%__MODULE__{} = meta, multiplier) when is_integer(multiplier) do
    meta
    |> Map.put(:multiplier, multiplier)
  end

  @doc """
  Sets the number of digits in a group or
  optionally the first group and subsequent
  groups for the integer part of a number.

  The grouping character is defined by the locale
  defined for the current process or supplied
  as the `:locale` option to `to_string/3`.

  """
  @spec put_integer_grouping(t(), non_neg_integer, non_neg_integer) :: t()
  def put_integer_grouping(%__MODULE__{} = meta, first, rest)
      when is_integer(first) and is_integer(rest) do
    grouping =
      meta
      |> Map.get(:grouping)
      |> Map.put(:integer, %{first: first, rest: rest})

    Map.put(meta, :grouping, grouping)
  end

  @spec put_integer_grouping(t(), non_neg_integer) :: t()
  def put_integer_grouping(%__MODULE__{} = meta, all) when is_integer(all) do
    grouping =
      meta
      |> Map.get(:grouping)
      |> Map.put(:integer, %{first: all, rest: all})

    Map.put(meta, :grouping, grouping)
  end

  @doc """
  Sets the number of digits in a group or
  optionally the first group and subsequent
  groups for the fractional part of a number.

  The grouping character is defined by the locale
  defined for the current process or supplied
  as the `:locale` option to `to_string/3`.

  """
  @spec put_fraction_grouping(t(), non_neg_integer, non_neg_integer) :: t()
  def put_fraction_grouping(%__MODULE__{} = meta, first, rest)
      when is_integer(first) and is_integer(rest) do
    grouping =
      meta
      |> Map.get(:grouping)
      |> Map.put(:fraction, %{first: first, rest: rest})

    Map.put(meta, :grouping, grouping)
  end

  @spec put_fraction_grouping(t(), non_neg_integer) :: t()
  def put_fraction_grouping(%__MODULE__{} = meta, all) when is_integer(all) do
    grouping =
      meta
      |> Map.get(:grouping)
      |> Map.put(:fraction, %{first: all, rest: all})

    Map.put(meta, :grouping, grouping)
  end

  @doc """
  Set the metadata format for the positive
  and negative number format.

  Note that this is the parsed format as a simple keyword
  list, not a binary representation.

  Its up to each formatting engine to transform its input
  into this form.  See `Cldr.Number.Format.Meta` module
  documentation for the available keywords.

  """
  @spec put_format(t(), Keyword.t(), Keyword.t()) :: t()
  def put_format(%__MODULE__{} = meta, positive_format, negative_format) do
    meta
    |> Map.put(:format, positive: positive_format, negative: negative_format)
  end

  @spec put_format(t(), Keyword.t()) :: t()
  def put_format(%__MODULE__{} = meta, positive_format) do
    put_format(meta, positive_format, minus: ~c"-", format: :same_as_positive)
  end
end