lib/cldr/number/formatter/short_formatter.ex

defmodule Cldr.Number.Formatter.Short do
  @moduledoc """
  Formats a number according to the locale-specific `:short` formats

  This is best explained by some
  examples:

      iex> Cldr.Number.to_string 123, TestBackend.Cldr, format: :short
      {:ok, "123"}

      iex> Cldr.Number.to_string 1234, TestBackend.Cldr, format: :short
      {:ok, "1K"}

      iex> Cldr.Number.to_string 523456789, TestBackend.Cldr, format: :short
      {:ok, "523M"}

      iex> Cldr.Number.to_string 7234567890, TestBackend.Cldr, format: :short
      {:ok, "7B"}

      iex> Cldr.Number.to_string 7234567890, TestBackend.Cldr, format: :long
      {:ok, "7 billion"}

  These formats are compact representations however they do lose
  precision in the presentation in favour of human readability.

  Note that for a `:currency` short format the number of decimal places
  is retrieved from the currency definition itself.  You can see the difference
  in the following examples:

      iex> Cldr.Number.to_string 1234, TestBackend.Cldr, format: :short, currency: "EUR"
      {:ok, "€1K"}

      iex> Cldr.Number.to_string 1234, TestBackend.Cldr, format: :short, currency: "EUR", fractional_digits: 2
      {:ok, "€1.23K"}

      iex> Cldr.Number.to_string 1234, TestBackend.Cldr, format: :short, currency: "JPY"
      {:ok, "¥1K"}

  **This module is not part of the public API and is subject
  to change at any time.**

  """

  alias Cldr.Math
  alias Cldr.Number.{System, Format, Formatter}
  alias Cldr.Locale
  alias Cldr.LanguageTag
  alias Cldr.Number.Format.Options

  # Notes from Unicode TR35 on formatting short formats:
  #
  # To format a number N, the greatest type less than or equal to N is
  # used, with the appropriate plural category. N is divided by the type, after
  # removing the number of zeros in the pattern, less 1. APIs supporting this
  # format should provide control over the number of significant or fraction
  # digits.
  #
  # If the value is precisely 0, or if the type is less than 1000, then the
  # normal number format pattern for that sort of object is supplied. For
  # example, formatting 1200 would result in “$1.2K”, while 990 would result in
  # simply “$990”.
  #
  # Thus N=12345 matches <pattern type="10000" count="other">00 K</pattern> . N
  # is divided by 1000 (obtained from 10000 after removing "00" and restoring one
  # "0". The result is formatted according to the normal decimal pattern. With no
  # fractional digits, that yields "12 K".

  @spec to_string(Math.number_or_decimal(), atom(), Cldr.backend(), Options.t()) ::
          {:ok, String.t()} | {:error, {module(), String.t()}}

  def to_string(number, _style, _backend, _options) when is_binary(number) do
    {:error,
      {
        ArgumentError,
        "Not a number: #{inspect number}. Long and short formats only support number or Decimal arguments"
      }
    }
  end
          
  def to_string(number, style, backend, options) do
    locale = options.locale || backend.default_locale()

    with {:ok, locale} <- Cldr.validate_locale(locale, backend),
         {:ok, number_system} <- System.system_name_from(options.number_system, locale, backend) do
      short_format_string(number, style, locale, number_system, backend, options)
    end
  end

  @spec short_format_string(
          Math.number_or_decimal(),
          atom,
          Locale.locale_name() | LanguageTag.t(),
          System.system_name(),
          Cldr.backend(),
          Options.t()
        ) :: {:ok, String.t()} | {:error, {module(), String.t()}}

  defp short_format_string(number, style, locale, number_system, backend, options) do
    format_rules =
      locale
      |> Format.formats_for!(number_system, backend)
      |> Map.fetch!(style)

    {normalized_number, format} = choose_short_format(number, format_rules, options, backend)
    options = digits(options, options.fractional_digits)
    format = Options.maybe_adjust_currency_symbol(format, options.currency_symbol)

    Formatter.Decimal.to_string(normalized_number, format, backend, options)
  end

  @doc """
  Returns the exponent that will be applied
  when formatting the given number as a short
  format.

  This function is primarily intended to support
  pluralization for compact numbers (numbers
  formatted with the `format: :short` option) since
  some languages pluralize compact numbers differently
  to a fully expressed number.

  Such rules are defined for the locale "fr" from
  CLDR version 38 with the intention that additional
  rules will be added in later versions.

  ## Examples

      iex> Cldr.Number.Formatter.Short.short_format_exponent 1234
      {1000, 1}

      iex> Cldr.Number.Formatter.Short.short_format_exponent 12345
      {10000, 2}

      iex> Cldr.Number.Formatter.Short.short_format_exponent 123456789
      {100000000, 3}

      iex> Cldr.Number.Formatter.Short.short_format_exponent 123456789, locale: "th"
      {100000000, 3}

  """
  def short_format_exponent(number, options \\ []) when is_list(options) do
    with {locale, backend} = Cldr.locale_and_backend_from(options),
         number_system = Keyword.get(options, :number_system, :default),
         {:ok, number_system} <- System.system_name_from(number_system, locale, backend),
         {:ok, all_formats} <- Format.formats_for(locale, number_system, backend) do
      formats = Map.fetch!(all_formats, :decimal_short)
      pluralizer = Module.concat(backend, Number.Cardinal)

      options =
        options
        |> Map.new
        |> Map.put_new(:locale, locale)
        |> Map.put_new(:number_system, number_system)
        |> Map.put_new(:currency, nil)

      case get_short_format_rule(number, formats, options, backend) do
        [range, plural_selectors] ->
          normalized_number = normalise_number(number, range, plural_selectors.other)
          plural_key = pluralization_key(normalized_number, options)
          [_format, number_of_zeros] = pluralizer.pluralize(plural_key, options.locale, plural_selectors)
          {range, number_of_zeros}
        {number, _format} ->
          {number, 0}
      end
    end
  end

  # For short formats the fractional digits should be 0 unless otherwise specified,
  # even for currencies
  defp digits(options, nil) do
    Map.put(options, :fractional_digits, 0)
  end

  defp digits(options, _digits) do
    options
  end

  defp choose_short_format(number, format_rules, options, backend) do
    pluralizer = Module.concat(backend, Number.Cardinal)

    case get_short_format_rule(number, format_rules, options, backend) do
      # Its a short format
      [range, plural_selectors] ->
        normalized_number = normalise_number(number, range, plural_selectors.other)
        plural_key = pluralization_key(normalized_number, options)
        [format, _number_of_zeros] = pluralizer.pluralize(plural_key, options.locale, plural_selectors)
        {normalized_number, format}

      # Its a standard format
      {number, format} ->
        {number, format}
    end
  end

  defp get_short_format_rule(number, _format_rules, options, backend) when is_number(number) and number < 1000 do
    format =
      options.locale
      |> Format.formats_for!(options.number_system, backend)
      |> Map.get(standard_or_currency(options))

    {number, format}
  end

  defp get_short_format_rule(number, format_rules, options, backend) when is_number(number) do
    format_rules
    |> Enum.filter(fn [range, _rules] -> range <= number end)
    |> Enum.reverse()
    |> hd
    |> maybe_get_default_format(number, options, backend)
  end

  defp get_short_format_rule(%Decimal{} = number, format_rules, options, backend) do
    number
    |> Decimal.round(0, :floor)
    |> Decimal.to_integer()
    |> get_short_format_rule(format_rules, options, backend)
  end

  defp maybe_get_default_format([_range, %{other: ["0", _]}], number, options, backend) do
    {_, format} = get_short_format_rule(0, [], options, backend)
    {number, format}
  end

  defp maybe_get_default_format(rule, _number, _options, _backend) do
    rule
  end

  defp standard_or_currency(options) do
    if options.currency do
      :currency
    else
      :standard
    end
  end

  @one_thousand Decimal.new(1000)
  defp normalise_number(%Decimal{} = number, range, number_of_zeros) do
    if Cldr.Decimal.compare(number, @one_thousand) == :lt do
      number
    else
      Decimal.div(number, Decimal.new(adjustment(range, number_of_zeros)))
    end
  end

  defp normalise_number(number, _range, _number_of_zeros) when number < 1000 do
    number
  end

  defp normalise_number(number, _range, ["0", _number_of_zeros]) do
    number
  end

  defp normalise_number(number, range, [_format, number_of_zeros]) do
    number / adjustment(range, number_of_zeros)
  end

  # TODO: We can precompute these at compile time which would
  # save this lookup
  defp adjustment(range, number_of_zeros) when is_integer(number_of_zeros) do
    (range / Math.power_of_10(number_of_zeros - 1))
    |> trunc
  end

  defp adjustment(range, [_, number_of_zeros]) when is_integer(number_of_zeros) do
   adjustment(range, number_of_zeros)
  end

  # The pluralization key has to consider when there is an
  # exact match and when the number would be rounded up. When
  # rounded up it also has to not be an exact match.
  defp pluralization_key(number, options) do
    rounding_mode = Map.get_lazy(options, :rounding_mode, &Cldr.Math.default_rounding_mode/0)

    if (rounded = Cldr.Math.round(number, 0, rounding_mode)) <= number do
      # Rounded number <= number means that the
      # pluralization key is the same integer part
      # so no issue
      number
    else
      # The rounded number is greater than the normalized
      # number so the plural key is different but not exactly
      # equal so we add an offset so pluralization works
      # correctly (we don't want to trigger an exact match;
      # although this relies on exact matches always being integers
      # which as of CLDR39 they are).
      rounded + 0.1
    end
  end
end