Packages

ex_cldr

2.47.4
2.47.5 2.47.4 2.47.3 2.47.2 2.47.1 2.47.0 2.46.0 2.45.2 2.45.1 2.45.0 2.44.1 2.44.0 2.43.2 2.43.1 2.43.0 2.42.0 2.41.1 2.41.0 2.40.4 2.40.3 2.40.2 2.40.1 2.40.0 2.39.2 2.39.1 2.39.0 2.38.1 2.38.0 2.37.5 2.37.4 2.37.3 retired 2.37.2 2.37.1 2.37.0 2.36.0 2.35.1 2.35.0 retired 2.34.2 2.34.1 2.34.0 2.33.2 2.33.1 2.33.1-rc.0 2.33.0 2.32.1 2.32.0 2.31.0 2.30.0 2.29.0 2.28.0 2.27.1 2.27.0 retired 2.26.3 2.26.2 2.26.1 2.26.0 2.25.0 2.24.2 2.24.1 2.24.0 2.24.0-rc.6 2.24.0-rc.5 2.24.0-rc.4 2.24.0-rc.3 2.24.0-rc.2 2.24.0-rc.1 2.24.0-rc.0 2.23.2 2.23.1 2.23.0 2.22.1 2.22.0 2.21.0 2.20.0 2.20.0-rc.3 2.20.0-rc.2 2.20.0-rc.1 2.20.0-rc.0 2.19.1 2.19.0 2.18.2 2.18.1 2.18.0 2.18.0-rc.0 2.17.2 2.17.1 2.17.0 2.16.2 2.16.1 2.16.1-rc.0 2.16.0 2.15.0 2.14.1 2.14.0 2.14.0-rc.0 2.13.0 2.12.1 2.12.0 2.11.1 2.11.0 2.10.2 2.10.1 2.10.0 retired 2.9.0 2.8.1 2.8.0 2.7.2 2.7.1 2.7.0 2.6.2 2.6.1 2.6.0 2.5.0 2.4.3 2.4.2 2.4.1 2.4.0 2.3.2 2.3.1 2.3.0 2.2.7 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 2.2.1 2.2.0 2.1.0 2.0.4 2.0.3 2.0.2 2.0.1 2.0.0 2.0.0-rc.3 retired 2.0.0-rc.2 retired 2.0.0-rc.1 retired 2.0.0-rc.0 retired 1.8.2 1.8.1 1.8.0 1.7.1 1.7.0 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.2 1.5.1 1.5.0 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 retired 1.4.0-rc.3 retired 1.4.0-rc.2 retired 1.4.0-rc.1 retired 1.4.0-rc.0 retired 1.3.2 1.3.1 1.3.0 1.2.0 1.1.0 1.0.0 1.0.0-rc.3 retired 1.0.0-rc.2 retired 1.0.0-rc.1 retired 1.0.0-rc.0 retired 0.13.1 0.13.0 0.12.2 retired 0.12.1 retired 0.12.0 retired 0.11.0 retired 0.10.0 retired 0.9.0 retired 0.8.3 retired 0.8.2 retired 0.8.1 retired 0.8.0 retired 0.7.0 retired 0.6.2 retired 0.6.1 retired 0.6.0 retired 0.5.2 retired 0.5.1 retired 0.5.0 retired 0.4.2 retired 0.4.1 retired 0.4.0 retired 0.3.0 retired 0.2.1 retired 0.2.0 retired 0.1.3 retired 0.1.2 retired 0.1.1 retired 0.1.0 retired 0.0.20 retired 0.0.19 retired 0.0.18 retired 0.0.17 retired 0.0.16 retired 0.0.15 retired 0.0.14 retired 0.0.13 retired 0.0.12 retired 0.0.11 retired 0.0.10 retired 0.0.8 retired 0.0.7 retired 0.0.6 retired 0.0.5 retired 0.0.4 retired 0.0.3 retired

Common Locale Data Repository (CLDR) functions for Elixir to localize and format numbers, dates, lists, messages, languages, territories and units with support for over 700 locales for internationalized (i18n) and localized (L10N) applications.

Current section

Files

Jump to
ex_cldr lib cldr locale distance_trie.ex
Raw

lib/cldr/locale/distance_trie.ex

defmodule Cldr.Locale.DistanceTrie.Builder do
@moduledoc false
# Builds the distance trie from Cldr.Config.language_matching/0.
# Kept in a separate module so the parent module can bake the
# built trie into a module attribute at compile time.
@doc false
def build do
data = Cldr.Config.language_matching()
rules = Map.fetch!(data, :language_match)
variables = Map.fetch!(data, :match_variables)
{lang_rules, script_rules, territory_rules} = partition_rules(rules)
trie = %{}
trie = insert_language_rules(trie, lang_rules)
trie = insert_script_rules(trie, script_rules)
trie = insert_territory_rules(trie, territory_rules, variables)
{trie, variables}
end
defp partition_rules(rules) do
Enum.reduce(rules, {[], [], []}, fn rule, {lang, script, terr} ->
case length(rule.desired) do
1 -> {[rule | lang], script, terr}
2 -> {lang, [rule | script], terr}
3 -> {lang, script, [rule | terr]}
end
end)
|> then(fn {l, s, t} -> {Enum.reverse(l), Enum.reverse(s), Enum.reverse(t)} end)
end
# ── Language level ─────────────────────────────────────────
defp insert_language_rules(trie, rules) do
Enum.reduce(rules, trie, fn rule, acc ->
[desired_lang] = rule.desired
[supported_lang] = rule.supported
one_way = Map.get(rule, :one_way, false)
distance = rule.distance
acc = put_lang_entry(acc, desired_lang, supported_lang, distance)
if not one_way do
put_lang_entry(acc, supported_lang, desired_lang, distance)
else
acc
end
end)
end
defp put_lang_entry(trie, desired, supported, distance) do
key = {desired, supported}
Map.put_new(trie, key, %{distance: distance, script: %{}})
end
# ── Script level ───────────────────────────────────────────
defp insert_script_rules(trie, rules) do
Enum.reduce(rules, trie, fn rule, acc ->
[desired_lang, desired_script] = rule.desired
[supported_lang, supported_script] = rule.supported
one_way = Map.get(rule, :one_way, false)
distance = rule.distance
acc =
put_script_entry(
acc,
desired_lang,
supported_lang,
desired_script,
supported_script,
distance
)
if not one_way do
put_script_entry(
acc,
supported_lang,
desired_lang,
supported_script,
desired_script,
distance
)
else
acc
end
end)
end
defp put_script_entry(
trie,
desired_lang,
supported_lang,
desired_script,
supported_script,
distance
) do
lang_key = {desired_lang, supported_lang}
lang_node =
Map.get(trie, lang_key) || Map.get(trie, {desired_lang, :*}) || %{distance: 0, script: %{}}
script_key = {desired_script, supported_script}
script_table = Map.get(lang_node, :script, %{})
script_table = Map.put_new(script_table, script_key, %{distance: distance, territory: %{}})
updated_node = Map.put(lang_node, :script, script_table)
Map.put(trie, lang_key, updated_node)
end
# ── Territory level ────────────────────────────────────────
defp insert_territory_rules(trie, rules, variables) do
Enum.reduce(rules, trie, fn rule, acc ->
[desired_lang, desired_script, desired_terr] = rule.desired
[supported_lang, supported_script, supported_terr] = rule.supported
one_way = Map.get(rule, :one_way, false)
distance = rule.distance
desired_keys = expand_territory_keys(desired_terr, variables)
supported_keys = expand_territory_keys(supported_terr, variables)
acc =
for dk <- desired_keys, sk <- supported_keys, reduce: acc do
acc ->
put_territory_entry(
acc,
desired_lang,
supported_lang,
desired_script,
supported_script,
dk,
sk,
distance
)
end
if not one_way do
for dk <- supported_keys, sk <- desired_keys, reduce: acc do
acc ->
put_territory_entry(
acc,
supported_lang,
desired_lang,
supported_script,
desired_script,
dk,
sk,
distance
)
end
else
acc
end
end)
end
# Expand territory patterns to trie keys.
# {:in, var} expands to individual territory atoms.
# {:not_in, var} stays as-is (checked at lookup time).
# :* stays as :*.
defp expand_territory_keys(:*, _variables), do: [:*]
defp expand_territory_keys({:in, variable}, variables), do: Map.fetch!(variables, variable)
defp expand_territory_keys({:not_in, _variable} = not_in, _variables), do: [not_in]
defp expand_territory_keys(territory, _variables) when is_atom(territory), do: [territory]
defp put_territory_entry(
trie,
desired_lang,
supported_lang,
desired_script,
supported_script,
desired_terr,
supported_terr,
distance
) do
lang_key = {desired_lang, supported_lang}
lang_node =
Map.get(trie, lang_key) || Map.get(trie, {desired_lang, :*}) ||
%{distance: nil, script: %{}}
script_key = {desired_script, supported_script}
script_table = Map.get(lang_node, :script, %{})
script_node =
Map.get(script_table, script_key) || Map.get(script_table, {desired_script, :*}) ||
%{distance: nil, territory: %{}}
terr_key = {desired_terr, supported_terr}
territory_table = Map.get(script_node, :territory, %{})
territory_table = Map.put_new(territory_table, terr_key, distance)
updated_script_node = Map.put(script_node, :territory, territory_table)
updated_script_table = Map.put(script_table, script_key, updated_script_node)
updated_lang_node = Map.put(lang_node, :script, updated_script_table)
Map.put(trie, lang_key, updated_lang_node)
end
end
defmodule Cldr.Locale.DistanceTrie do
@moduledoc false
# A 3-level nested map implementing the ICU XLocaleDistance trie
# for locale distance lookups.
#
# The trie is keyed by {desired, supported} tuples at each level.
# Wildcard entries use :* and negative variable entries use
# {:not_in, variable} which are checked at lookup time.
#
# Built once at compile time from Cldr.Config.language_matching/0 and
# held as a module attribute. There is no runtime build, no GenServer,
# no :persistent_term — the data is fully static.
@distance_trie Cldr.Locale.DistanceTrie.Builder.build()
@doc false
def trie, do: @distance_trie
@spec lookup(String.t(), atom() | nil, atom() | nil, String.t(), atom() | nil, atom() | nil) ::
number()
def lookup(
desired_lang,
desired_script,
desired_territory,
supported_lang,
supported_script,
supported_territory
) do
{trie, variables} = trie()
default_lang_node = Map.get(trie, {:*, :*}) || %{distance: 0, script: %{}}
# Language level
{lang_distance, lang_node} =
if desired_lang == supported_lang do
node =
cascade_lookup(trie, desired_lang, supported_lang) ||
default_lang_node
{0, node}
else
node =
cascade_lookup(trie, desired_lang, supported_lang) ||
default_lang_node
distance = node[:distance] || default_lang_node.distance
{distance, node}
end
# Script level — check lang_node's table, fall back to default
script_table = lang_node[:script] || %{}
default_script_table = default_lang_node[:script] || %{}
{script_distance, script_node} =
if desired_script == supported_script do
node =
cascade_lookup(script_table, desired_script, supported_script) ||
cascade_lookup(default_script_table, desired_script, supported_script)
{0, node}
else
node =
cascade_lookup(script_table, desired_script, supported_script) ||
cascade_lookup(default_script_table, desired_script, supported_script)
cond do
node && node[:distance] -> {node.distance, node}
node -> {default_script_table[{:*, :*}][:distance] || 0, node}
true -> {0, nil}
end
end
# Territory level
default_script_node =
Map.get(script_table, {:*, :*}) ||
Map.get(default_script_table, {:*, :*}) ||
%{distance: 0, territory: %{}}
territory_table = (script_node && script_node[:territory]) || %{}
default_territory_table = default_script_node[:territory] || %{}
territory_distance =
if desired_territory == supported_territory do
0
else
resolve_territory_distance(
territory_table,
default_territory_table,
desired_territory,
supported_territory,
variables
)
end
lang_distance + script_distance + territory_distance
end
defp resolve_territory_distance(territory_table, default_table, desired, supported, variables) do
case territory_cascade_lookup(territory_table, desired, supported, variables) do
nil ->
case territory_cascade_lookup(default_table, desired, supported, variables) do
nil -> 0
distance -> distance
end
distance ->
distance
end
end
# Territory-level cascade with variable-aware matching.
# Tries in order: exact → {desired, :*} → {:*, supported} → not_in rules → {:*, :*}
defp territory_cascade_lookup(table, desired, supported, variables) do
Map.get(table, {desired, supported}) ||
Map.get(table, {desired, :*}) ||
Map.get(table, {:*, supported}) ||
find_not_in_match(table, desired, supported, variables) ||
Map.get(table, {:*, :*})
end
# Check {:not_in, var} entries — both desired and supported must NOT be in the variable
defp find_not_in_match(table, desired, supported, variables) do
# Try concrete + not_in combinations first (more specific),
# then not_in + not_in (less specific)
find_not_in_concrete(table, desired, supported, variables) ||
find_not_in_both(table, desired, supported, variables)
end
# One side is a concrete territory, the other is {:not_in, var}
defp find_not_in_concrete(table, desired, supported, variables) do
# Check {desired, {:not_in, var}} — desired is concrete, supported must not be in var
result =
Enum.find_value(table, fn
{{^desired, {:not_in, var}}, distance} ->
set = Map.fetch!(variables, var)
if supported not in set, do: distance
_ ->
nil
end)
# Check {{:not_in, var}, supported} — supported is concrete, desired must not be in var
result ||
Enum.find_value(table, fn
{{{:not_in, var}, ^supported}, distance} ->
set = Map.fetch!(variables, var)
if desired not in set, do: distance
_ ->
nil
end)
end
# Both sides are {:not_in, var} or {:not_in, var} + :*
defp find_not_in_both(table, desired, supported, variables) do
Enum.find_value(table, fn
{{{:not_in, var_d}, {:not_in, var_s}}, distance} ->
desired_set = Map.fetch!(variables, var_d)
supported_set = Map.fetch!(variables, var_s)
if desired not in desired_set and supported not in supported_set do
distance
end
{{{:not_in, var}, :*}, distance} ->
set = Map.fetch!(variables, var)
if desired not in set, do: distance
{{:*, {:not_in, var}}, distance} ->
set = Map.fetch!(variables, var)
if supported not in set, do: distance
_ ->
nil
end)
end
# Cascade lookup for language and script levels (no variable matching needed)
defp cascade_lookup(table, desired, supported) do
Map.get(table, {desired, supported}) ||
Map.get(table, {desired, :*}) ||
Map.get(table, {:*, supported}) ||
Map.get(table, {:*, :*})
end
end