diff --git a/CHANGELOG.md b/CHANGELOG.md index bc9b35c..d985a0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## Cldr Numbers v2.31.4 + +This is the changelog for Cldr v2.31.4 released on _____, 2023. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_numbers/tags) + +### Bug Fixes + +* Fix formatting numbers with format `:standard` when the standard format is an RBNF rule. + +* Fix formatting numbers with an RBNF format that is defined on locale `:und` even when the formatting locale doesn't have an `:rbnf_locale_name` + +* Improve several error messages around number systems and RBNF rules. + ## Cldr Numbers v2.31.3 This is the changelog for Cldr v2.31.3 released on July 24th, 2023. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_numbers/tags) diff --git a/lib/cldr/number.ex b/lib/cldr/number.ex index 4faf508..3a511b0 100644 --- a/lib/cldr/number.ex +++ b/lib/cldr/number.ex @@ -385,7 +385,7 @@ defmodule Cldr.Number do ## Errors - An error tuple `{:error, reason}` will be returned if an error is detected. + An error tuple `{:error, {exception, reason}}` will be returned if an error is detected. The two most likely causes of an error return are: * A format cannot be compiled. In this case the error tuple will look like: @@ -402,9 +402,9 @@ defmodule Cldr.Number do return looks like: ``` - iex> Cldr.Number.to_string(1234, TestBackend.Cldr, locale: "he", number_system: "hebr") + iex> Cldr.Number.to_string(1234, TestBackend.Cldr, locale: "he", number_system: "hebr", format: :percent) {:error, {Cldr.UnknownFormatError, - "The locale :he with number system :hebr does not define a format :standard"}} + "The locale :he with number system :hebr does not define a format :percent"}} ``` """ @spec to_string( @@ -533,10 +533,6 @@ defmodule Cldr.Number do end # For executing arbitrary RBNF rules that might exist for a given locale - defp to_string(_number, format, _backend, %{locale: %{rbnf_locale_name: nil} = locale}) do - {:error, Cldr.Rbnf.rbnf_rule_error(locale, format)} - end - defp to_string(number, format, backend, options) when is_atom(format) do with {:ok, module, locale} <- find_rbnf_format_module(options.locale, format, backend) do apply(module, format, [number, locale]) diff --git a/lib/cldr/number/backend/number.ex b/lib/cldr/number/backend/number.ex index afc8bb7..e7f4b0c 100644 --- a/lib/cldr/number/backend/number.ex +++ b/lib/cldr/number/backend/number.ex @@ -378,9 +378,9 @@ defmodule Cldr.Number.Backend.Number do return looks like: ``` - iex> #{inspect(__MODULE__)}.to_string(1234, locale: "he", number_system: "hebr") + iex> #{inspect(__MODULE__)}.to_string(1234, locale: "he", number_system: "hebr", format: :percent) {:error, {Cldr.UnknownFormatError, - "The locale :he with number system :hebr does not define a format :standard"}} + "The locale :he with number system :hebr does not define a format :percent"}} ``` """ @spec to_string(number | Decimal.t(), Keyword.t() | map()) :: diff --git a/lib/cldr/number/format/options.ex b/lib/cldr/number/format/options.ex index 4b8cf85..5847264 100644 --- a/lib/cldr/number/format/options.ex +++ b/lib/cldr/number/format/options.ex @@ -37,7 +37,7 @@ defmodule Cldr.Number.Format.Options do @valid_options @options -- ([:currency_spacing, :pattern] ++ [:cash]) - @short_format_styles [ + @short_formats [ :currency_short, :currency_long_with_symbol, :currency_long, @@ -193,9 +193,6 @@ defmodule Cldr.Number.Format.Options do options end - # As of CLDR 42 there is a format for a currency that excludes the - # currency symbol. - @doc false def resolve_standard_format(%{format: :currency, currency: nil} = options, backend) do options = Map.put(options, :format, :currency_no_symbol) @@ -209,33 +206,68 @@ defmodule Cldr.Number.Format.Options do def resolve_standard_format(%{format: format} = options, backend) when format in @standard_formats do - locale = Map.fetch!(options, :locale) - number_system = Map.fetch!(options, :number_system) + %{locale: locale, number_system: number_system} = options with {:ok, formats} <- Format.formats_for(locale, number_system, backend), - {:ok, resolved_format} <- get_standard_format(formats, format, locale, number_system) do + {:ok, resolved_format} <- standard_format(formats, format, locale, number_system, backend) do Map.put(options, :format, resolved_format) end end - def resolve_standard_format(other, _backend) do - other + def resolve_standard_format(%{format: format} = options, backend) + when format in @short_formats do + %{locale: locale, number_system: number_system} = options + + # :currency_long_with_symbol is a derived format that depends on + # the availability of :currency_long + check_format = if format == :currency_long_with_symbol, do: :currency_long, else: format + + with {:ok, formats} <- Format.formats_for(locale, number_system, backend) do + if Map.get(formats, check_format) do + options + else + {:error, unknown_format_error(format, locale, number_system)} + end + end end - def get_standard_format(formats, format, locale, number_system) do + def resolve_standard_format(options, _backend) do + options + end + + # The standard format is either defined as a format string + # for digits number systems and as an RBNF rule name for + # algorithmic number systems like hebrew and tamil. + + def standard_format(formats, format, locale, number_system, backend) do case Map.fetch(formats, format) do {:ok, nil} -> - {:error, - {Cldr.UnknownFormatError, - "The locale #{inspect(Map.fetch!(locale, :cldr_locale_name))} " <> - "with number system #{inspect(number_system)} " <> - "does not define a format #{inspect(format)}"}} + rbnf_rule(number_system, format, backend) || + {:error, unknown_format_error(format, locale, number_system)} {:ok, format} -> {:ok, format} end end + defp rbnf_rule(number_system, :standard, backend) do + case System.default_rbnf_rule(number_system, backend) do + {:ok, {_module, rule_function, _locale}} -> {:ok, rule_function} + {:error, _} -> nil + end + end + + defp rbnf_rule(_number_system, _format, _backend) do + nil + end + + defp unknown_format_error(format, locale, number_system) do + {Cldr.UnknownFormatError, + "The locale #{inspect(Map.fetch!(locale, :cldr_locale_name))} " <> + "with number system #{inspect(number_system)} " <> + "does not define a format #{inspect(format)}"} + end + @currency_placeholder Compiler.placeholder(:currency) # @iso_placeholder Compiler.placeholder(:currency) <> Compiler.placeholder(:currency) @@ -455,6 +487,7 @@ defmodule Cldr.Number.Format.Options do end end + defp validate_option(:format, _options, _backend, format) do {:ok, format} end @@ -645,7 +678,7 @@ defmodule Cldr.Number.Format.Options do @doc false @spec short_format_styles() :: list(atom()) def short_format_styles do - @short_format_styles + @short_formats end # # Sometimes we want the standard format for a currency but we want the diff --git a/lib/cldr/number/formatter/currency_formatter.ex b/lib/cldr/number/formatter/currency_formatter.ex index 4d72e59..e1b018c 100644 --- a/lib/cldr/number/formatter/currency_formatter.ex +++ b/lib/cldr/number/formatter/currency_formatter.ex @@ -53,8 +53,9 @@ defmodule Cldr.Number.Formatter.Currency do }} end - # The format :currency_medium is a composition of :currency_long - # and the default :currency format. + # The format :currency_long_with_symbol is a composition of :currency_long + # and the default :currency format. It is a derived format, not one + # defined by CLDR. def to_string(number, :currency_long_with_symbol, backend, options) do decimal_options = decimal_options(options, backend, number) diff --git a/lib/cldr/number/parse.ex b/lib/cldr/number/parse.ex index 27f740a..266303f 100644 --- a/lib/cldr/number/parse.ex +++ b/lib/cldr/number/parse.ex @@ -177,9 +177,7 @@ defmodule Cldr.Number.Parser do "The string \\"+1.000,34\\" could not be parsed as a number"}} iex> Cldr.Number.Parser.parse "一万二千三百四十五", locale: "ja-u-nu-jpan" - {:error, - {Cldr.UnknownNumberSystemError, - "The number system :jpan is not known or does not have digits"}} + {:error, {Cldr.UnknownNumberSystemError, "The number system :jpan does not have digits"}} """ @spec parse(String.t(), Keyword.t()) :: diff --git a/lib/cldr/number/rbnf.ex b/lib/cldr/number/rbnf.ex index d26e08d..9d60a37 100644 --- a/lib/cldr/number/rbnf.ex +++ b/lib/cldr/number/rbnf.ex @@ -72,7 +72,7 @@ defmodule Cldr.Rbnf do end def rule_names_for_locale(locale_name, backend \\ Cldr.default_backend!()) - when is_binary(locale_name) do + when is_binary(locale_name) or is_atom(locale_name) do with {:ok, locale} <- Cldr.Locale.canonical_language_tag(locale_name, backend) do rule_names_for_locale(locale) end @@ -225,25 +225,14 @@ defmodule Cldr.Rbnf do |> Cldr.Map.merge_map_list() end - def rbnf_locale_error(locale_name) when is_binary(locale_name) do - {Cldr.Rbnf.NotAvailable, "xRBNF is not available for the locale #{inspect(locale_name)}"} + def rbnf_locale_error(%LanguageTag{} = locale) do + {Cldr.Rbnf.NotAvailable, "RBNF is not available for locale #{inspect(locale)}"} end - def rbnf_locale_error(%LanguageTag{cldr_locale_name: locale_name}) do - rbnf_locale_error(locale_name) - end - - def rbnf_rule_error( - %LanguageTag{rbnf_locale_name: nil, cldr_locale_name: cldr_locale_name}, - _format - ) do - {Cldr.Rbnf.NotAvailable, "x2 RBNF is not available for the locale #{inspect(cldr_locale_name)}"} - end - - def rbnf_rule_error(%LanguageTag{rbnf_locale_name: rbnf_locale_name}, format) do + def rbnf_rule_error(%LanguageTag{} = locale, format) do { Cldr.Rbnf.NoRule, - "Locale #{inspect(rbnf_locale_name)} does not define an rbnf ruleset #{inspect(format)}" + "RBNF rule #{inspect(format)} is unknown to locale #{inspect(locale)}" } end diff --git a/lib/cldr/number/system.ex b/lib/cldr/number/system.ex index ebe906a..94026e4 100644 --- a/lib/cldr/number/system.ex +++ b/lib/cldr/number/system.ex @@ -66,11 +66,74 @@ defmodule Cldr.Number.System do @doc """ Number systems that have their own digit characters defined. + """ def systems_with_digits do @systems_with_digits end + @algorithmic_systems Enum.filter(@number_systems, fn {_name, system} -> + system.type == :algorithmic + end) + |> Map.new() + + @doc """ + Returns number systems that are algorithmic. + + Algorithmic number systems don't have decimal + digits. Numbers are formed by algorithm using + rules based number formats. + + """ + def algorithmic_systems do + @algorithmic_systems + end + + @doc """ + Returns the default RBNF rule name for an + algorithmic number system. + + ### Arguments + + * `system_name` is any number system name returned by + `Cldr.known_number_systems/0` or a number system type + returned by `Cldr.known_number_system_types/0`. + + * `backend` is any `Cldr` backend. That is, any module that + contains `use Cldr`. + + ### Returns + + * `{:ok, {module, rule_function, locale}}` or + + * `{:error, {module(), reason}}` + + ### Example + + iex> Cldr.Number.System.default_rbnf_rule :taml, MyApp.Cldr + {:ok, {MyApp.Cldr.Rbnf.NumberSystem, :tamil, :und}} + + iex> Cldr.Number.System.default_rbnf_rule :hebr, MyApp.Cldr + {:ok, {MyApp.Cldr.Rbnf.NumberSystem, :hebrew, :und}} + + iex> Cldr.Number.System.default_rbnf_rule :jpanfin, MyApp.Cldr + {:ok, {MyApp.Cldr.Rbnf.Spellout, :spellout_cardinal_financial, :ja}} + + iex> Cldr.Number.System.default_rbnf_rule :latn, MyApp.Cldr + {:error, + {Cldr.UnknownNumberSystemError, "The number system :latn is not algorithmic"}} + + """ + def default_rbnf_rule(system_name, backend) do + case Map.fetch(algorithmic_systems(), system_name) do + {:ok, definition} -> + {:ok, Cldr.Config.rbnf_rule_function(definition.rules, backend)} + + :error -> + {:error, algorithmic_system_error(system_name)} + end + end + @doc """ Returns the default number system from a language tag or locale name. @@ -85,7 +148,7 @@ defmodule Cldr.Number.System do ## Returns - * A number system name as an atom + * A number system name as an atom. ## Examples @@ -171,10 +234,10 @@ defmodule Cldr.Number.System do ## Arguments * `locale` is any valid locale name returned by `Cldr.known_locale_names/0` - or a `Cldr.LanguageTag` struct returned by ``Cldr.Locale.new!/2`` + or a `Cldr.LanguageTag` struct returned by ``Cldr.Locale.new!/2``. * `backend` is any `Cldr` backend. That is, any module that - contains `use Cldr` + contains `use Cldr`. ## Examples @@ -578,7 +641,7 @@ defmodule Cldr.Number.System do {:ok, "0123456789"} iex> Cldr.Number.System.number_system_digits(:nope) - {:error, {Cldr.UnknownNumberSystemError, "The number system :nope is not known or does not have digits"}} + {:error, {Cldr.UnknownNumberSystemError, "The number system :nope is not known"}} """ @spec number_system_digits(system_name()) :: @@ -835,9 +898,36 @@ defmodule Cldr.Number.System do @doc false def number_system_digits_error(system_name) do + case number_systems()[system_name] do + nil -> + unknown_number_system_error(system_name) + + _system -> + { + Cldr.UnknownNumberSystemError, + "The number system #{inspect(system_name)} does not have digits" + } + end + end + + @doc false + def algorithmic_system_error(system_name) do + case number_systems()[system_name] do + nil -> + unknown_number_system_error(system_name) + + _system -> + { + Cldr.UnknownNumberSystemError, + "The number system #{inspect(system_name)} is not algorithmic" + } + end + end + + defp unknown_number_system_error(system_name) do { Cldr.UnknownNumberSystemError, - "The number system #{inspect(system_name)} is not known or does not have digits" + "The number system #{inspect(system_name)} is not known" } end end diff --git a/mix.exs b/mix.exs index fb18102..9f85f58 100644 --- a/mix.exs +++ b/mix.exs @@ -3,7 +3,7 @@ defmodule Cldr.Numbers.Mixfile do use Mix.Project - @version "2.31.3" + @version "2.31.4" def project do [ diff --git a/mix/test_backend.ex b/mix/test_backend.ex index 4709aec..d37a4ef 100644 --- a/mix/test_backend.ex +++ b/mix/test_backend.ex @@ -10,7 +10,6 @@ defmodule MyApp.Cldr do "zh", "zh-Hant", "it", - "fr", "de", "th", "id", @@ -22,7 +21,11 @@ defmodule MyApp.Cldr do "nb", "no", "en-IN", - "ur" + "ur", + "fr-CH", + "fr-BE", + "ta", + "he" ], precompile_transliterations: [{:latn, :arab}, {:arab, :thai}, {:arab, :latn}], providers: [Cldr.Number], diff --git a/test/number/number_format_test.exs b/test/number/number_format_test.exs index 77dc207..f76a34a 100644 --- a/test/number/number_format_test.exs +++ b/test/number/number_format_test.exs @@ -62,32 +62,19 @@ defmodule Number.Format.Test do test "that an rbnf format request fails if the locale doesn't define the ruleset" do assert TestBackend.Cldr.Number.to_string(123, format: :spellout_ordinal_verbose, locale: "zh") == - {:error, - {Cldr.Rbnf.NoRule, - "Locale :zh does not define an rbnf ruleset :spellout_ordinal_verbose"}} + { + :error, + { + Cldr.Rbnf.NoRule, + "RBNF rule :spellout_ordinal_verbose is unknown to locale #Cldr.LanguageTag" + } + } end test "that we get default formats_for" do assert TestBackend.Cldr.Number.Format.formats_for!().__struct__ == Cldr.Number.Format end - test "that when there is no format defined for a number system we get an error return" do - assert TestBackend.Cldr.Number.to_string(1234, locale: "he", number_system: :hebr) == - { - :error, - { - Cldr.UnknownFormatError, - "The locale :he with number system :hebr does not define a format :standard" - } - } - end - - test "that when there is no format defined for a number system raises" do - assert_raise Cldr.UnknownFormatError, ~r/The locale .* does not define/, fn -> - TestBackend.Cldr.Number.to_string!(1234, locale: "he", number_system: :hebr) - end - end - test "setting currency_format: :iso" do assert TestBackend.Cldr.Number.to_string(123, currency: :USD, currency_symbol: :iso) == {:ok, "USD 123.00"} @@ -236,4 +223,14 @@ defmodule Number.Format.Test do assert {:ok, _formatted_number} = Cldr.Number.to_string(1234, locale: language_tag) end end + + test "that each number system for each locale can format a number with standard format" do + for locale <- TestBackend.Cldr.known_locale_names do + {:ok, systems} = TestBackend.Cldr.Number.System.number_systems_for(locale) + number_systems = Enum.uniq(Map.keys(systems) ++ Map.values(systems)) + for number_system <- number_systems do + assert {:ok, _} = TestBackend.Cldr.Number.to_string(123, locale: locale, number_system: number_system) + end + end + end end