Skip to content

Commit

Permalink
Use RBNF default format for algorithmic systems.
Browse files Browse the repository at this point in the history
* Improve RBNF error messages.
* Support NumberSystem RBNF rules even for locales that
  don't have their own RBNF rulesets.
* Validate that every locale can format a number with
  the standard format (digits or algorithmic) for each of
  its known number systems.

Closes #42
  • Loading branch information
kipcole9 committed Aug 15, 2023
1 parent e31a933 commit 56de714
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 74 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
10 changes: 3 additions & 7 deletions lib/cldr/number.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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(
Expand Down Expand Up @@ -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])
Expand Down
4 changes: 2 additions & 2 deletions lib/cldr/number/backend/number.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()) ::
Expand Down
65 changes: 49 additions & 16 deletions lib/cldr/number/format/options.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -455,6 +487,7 @@ defmodule Cldr.Number.Format.Options do
end
end


defp validate_option(:format, _options, _backend, format) do
{:ok, format}
end
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions lib/cldr/number/formatter/currency_formatter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 1 addition & 3 deletions lib/cldr/number/parse.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()) ::
Expand Down
21 changes: 5 additions & 16 deletions lib/cldr/number/rbnf.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
100 changes: 95 additions & 5 deletions lib/cldr/number/system.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()) ::
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule Cldr.Numbers.Mixfile do

use Mix.Project

@version "2.31.3"
@version "2.31.4"

def project do
[
Expand Down
Loading

0 comments on commit 56de714

Please sign in to comment.