Skip to content

Keep every CLDR number symbol and use the approximately sign - #67

Open
rubas wants to merge 3 commits into
elixir-cldr:mainfrom
rubas:fix/strict-symbols
Open

rubas wants to merge 3 commits into
elixir-cldr:mainfrom
rubas:fix/strict-symbols

Conversation

@rubas

@rubas rubas commented Sep 26, 2026

Copy link
Copy Markdown

Before

iex> Cldr.Number.to_approx_string(1234, locale: "fr-BE")
{:ok, "≈1 234"}
iex> Cldr.Number.to_approx_string(1234, locale: "nb")
{:ok, "ca. 1 234"}
iex> Cldr.Number.to_string(-70_000, locale: "en", currency: :CHF, format: :currency_long_with_symbol)
{:ok, "CHF--70 thousand"}

Problem

  • The backend builds Cldr.Number.Symbol with struct/2, which drops every CLDR symbol the struct has no field for. Nobody sees what it drops.
  • One dropped symbol is approximately_sign. CLDR says to format an approximate number with this sign where the minus sign goes. The "approximately" pattern that we use instead is "not currently in use" (UTS ** (KeyError) key :symbol_count not found in: nil. #35), and in 60 locales it has other text (fr-BE sign "≃", pattern "≈{0}").
  • :currency_long_with_symbol formats the number twice, so a negative number gets two minus signs.

After

def new!(symbols) do
  pending = @pending_symbols -- Map.keys(%__MODULE__{})
  struct!(__MODULE__, Map.drop(symbols, @excluded_symbols ++ pending))
end
iex> Cldr.Number.to_approx_string(1234, locale: "fr-BE")
{:ok, "≃1 234"}
iex> Cldr.Number.to_approx_string(-1234, locale: "nb")
{:ok, "ca.−1 234"}
iex> Cldr.Number.to_approx_string(12, locale: "nb", format: :spellout)
{:ok, "ca. tolv"}
iex> Cldr.Number.to_string(-70_000, locale: "en", currency: :CHF, format: :currency_long_with_symbol)
{:ok, "CHF-70 thousand"}

nb now gives "ca.1 234" with no space, the same as ICU.

Details
  • Strict symbols: Cldr.Number.Symbol.new!/1 leaves out time_separator_alt_variant by name (the "." of nb, nn, no and sv; no formatter reads it). It also leaves out currency_decimal and currency_group, but only while the struct has no field for them. Use CLDR currency separators when formatting a currency #66 adds those fields, so the two PRs can merge in either order. Any other unknown symbol raises when the backend compiles. All 840 symbol maps in the 766 locales build.
  • Relation to Raise on a CLDR number symbol that Cldr.Number.Symbol does not know cldr#274: that PR adds Cldr.Config.number_symbol!/1 to ex_cldr with the same rules. Once ex_cldr releases it, the backend calls Cldr.Config.number_symbols_for!/2 and new!/1 goes (marked TODO(revert)).
  • Approximate numbers follow UTS ** (KeyError) key :symbol_count not found in: nil. #35 "Approximate Number Formatting" and ICU (PatternStringUtils::patternInfoToStringBuilder). A positive number uses the negative pattern with the sign in place of the minus sign. A negative number gets the sign before the minus sign. When the negative pattern has no minus sign, ICU and this change put the sign before the positive pattern for a positive number and add no sign for a negative number: "~$1,234.00" and "($1,234.00)" in accounting. Spellout, roman and other RBNF formats have no minus position, so they keep the "approximately" pattern.
  • Where this library puts the minus sign after the currency symbol ("$-70,000.00", ICU "-$70,000.00"), the approximately sign goes there too. That placement is older than this PR.
  • :currency_long_with_symbol now formats the inner long number unsigned and without the approximately flag. The outer currency pattern adds the sign once. Take the sign from each number when formatting with validated options #65 changes how the sign pattern is picked. This fix does not depend on it.
  • Decimal -0 stays unsigned, as in to_string/3: "0". ICU gives "-0".
  • The new field changes the symbols of ja, yo and yo-BJ compared to en, so those locales leave the transliteration fast path. Their output stays the same.
  • Tests: test/number/approximately_test.exs pins ICU 78.3 strings (Node Intl.NumberFormat#formatRange(n, n)) for fr-BE, nb, en zero, long currency and accounting. It also covers nb spellout and roman, Decimal -0, and the one sign in :currency_long_with_symbol. test/number/number_symbol_test.exs checks that nb keeps "ca." and loses the "." time separator, and that an unknown symbol raises.
  • mix test: 5244 passed (282 doctests, 4962 tests). mix compile --force shows no warnings. The changed lines are formatted. mix format --check-formatted also fails on upstream main, on lines this change does not touch.

AI disclosure: AI models helped us find this issue, write the change, and review it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant