Skip to content

Raise on a CLDR number symbol that Cldr.Number.Symbol does not know - #274

Closed
rubas wants to merge 1 commit into
elixir-cldr:mainfrom
rubas:fix/strict-symbol-struct
Closed

rubas wants to merge 1 commit into
elixir-cldr:mainfrom
rubas:fix/strict-symbol-struct

Conversation

@rubas

@rubas rubas commented Sep 26, 2026

Copy link
Copy Markdown

Before

Cldr.Config.number_symbols_for/2 builds Cldr.Number.Symbol with struct/2, which drops every key the struct does not define. With ex_cldr_numbers 2.38.3 it drops these CLDR symbols:

# nb, nn, no, sv and their regional locales: 7 records
time_separator_alt_variant: "."
# every locale: 840 records (fr-BE "≃", nb "ca.")
approximately_sign: "~"
# fr-CH and de-AT: 1 record each
currency_decimal: ".", currency_group: "."

Problem

  • Nobody decided to drop these symbols. They disappear because the struct has no field for them.
  • When CLDR adds a symbol, ex_cldr drops it and nobody sees it.

After

@excluded_number_symbols [:time_separator_alt_variant]
@optional_number_symbols [:approximately_sign, :currency_decimal, :currency_group]

def number_symbol!(symbols) do
  unsupported = @optional_number_symbols -- Map.keys(struct(Cldr.Number.Symbol))
  struct!(Cldr.Number.Symbol, Map.drop(symbols, @excluded_number_symbols ++ unsupported))
end

Any other symbol that the struct does not define raises a KeyError.

Details
Symbol Records Decision Why
time_separator_alt_variant 7 Left out by name The "." that nb, nn, no, and sv use as an alternative time separator. No formatter reads it.
approximately_sign 840 Kept when Cldr.Number.Symbol has the field Real data: in 60 records it differs from the "approximately" pattern (fr-BE sign "≃", pattern "≈{0}"). No ex_cldr_numbers version has the field yet.
currency_decimal, currency_group 1 each Kept when Cldr.Number.Symbol has the field fr-CH "." and de-AT ".". ex_cldr_numbers adds the fields in elixir-cldr/cldr_numbers#66.
  • Compatibility: Cldr.Number.Symbol lives in ex_cldr_numbers, and ex_cldr does not depend on it. The optional symbols follow the struct, so this works with ex_cldr_numbers 2.38.3 and with the fields from Use CLDR currency separators when formatting a currency cldr_numbers#66, and the two can be released in any order.
  • Check: test/number_symbol_test.exs builds the symbols of every locale in priv/cldr/locales (about 5 s), so a CLDR update with a new symbol fails the test suite. Other tests: sv leaves out its "." time separator, and an unknown symbol (digit_marker) raises.
  • test/support/number_symbol.ex stands in for the ex_cldr_numbers 2.38.3 struct in the test env.
  • ex_cldr_numbers mix test against this branch (path dependency): upstream main 5232 passed, Use CLDR currency separators when formatting a currency cldr_numbers#66 5234 passed. Its test backend compiles all 766 locales, which runs number_symbols_for!/2 for each.
  • mix test passes (13359). mix format --check-formatted fails on main too (Elixir 1.20.4), in 21 files, on lines this change does not touch. The changed code is formatted.
  • The ex_cldr_numbers backend (lib/cldr/number/backend/symbol.ex) builds the same struct with its own struct/2, so it still drops these symbols. It could call Cldr.Config.number_symbol!/1.

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

number_symbols_for/2 built Cldr.Number.Symbol with struct/2, which
drops every key the struct does not define. time_separator_alt_variant
("." in nb, nn, no and sv) is now left out by name. approximately_sign,
currency_decimal and currency_group are kept when the struct defines
them, so this works with every ex_cldr_numbers version. Any other
symbol raises a KeyError.
@kipcole9

kipcole9 commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @rubas - the symbol table in your write-up is very useful. Since ex_cldr is now in maintenance mode I am trying to avoid changes users could see. This change doesn’t alter any output today: struct/2 already drops the same symbols. Its effect would be a new KeyError while consumers compile their backends if the data ever gains a symbol, and that’s a failure mode I’d rather not add.

elixir-cldr/cldr_numbers#66 doesn’t depend on it either, since struct/2 keeps the currency fields once Cldr.Number.Symbol defines them. So I’m closing this PR, but I’ll look at elixir-cldr/cldr_numbers#66 on its own merits. Thanks again!

@kipcole9 kipcole9 closed this Nov 6, 2026
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.

2 participants