/
rbnf.ex
281 lines (215 loc) · 7.92 KB
/
rbnf.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
defmodule Cldr.Rbnf do
@moduledoc """
Functions to implement Rules Based Number Formatting (rbnf)
During compilation RBNF rules are extracted and generated
as function bodies by `Cldr.Rbnf.Ordinal`, `Cldr.Rbnf.Cardinal`
and `Cldr.Rbnf.NumberSystem`.
The functions in this module would not normally be of common
use outside of supporting the compilation phase.
"""
alias Cldr.LanguageTag
@doc """
Returns the list of locales that that have RBNF defined
This list is the set of known locales for which
there are rbnf rules defined.
Delegates to `Cldr.known_rbnf_locale_names/1`
"""
defdelegate known_locale_names(backend), to: Cldr, as: :known_rbnf_locale_names
@categories [:NumberSystem, :Spellout, :Ordinal]
@doc """
Returns the list of RBNF rules for a locale.
A rule name can be used as the `:format` parameter
in `Cldr.Number.to_string/3`.
## Arguments
* `locale` is any `Cldr.LanguageTag.t()`
## Returns
* `{:ok, [list_of_rule_names_as_atoms]}` or
* `{:error, {exception, reason}}`
## Examples
iex> Cldr.Rbnf.rule_names_for_locale "zh"
{:ok,
[:spellout_cardinal_alternate2, :spellout_ordinal, :spellout_cardinal,
:spellout_cardinal_financial, :spellout_numbering, :spellout_numbering_days,
:spellout_numbering_year, :digits_ordinal]}
iex> Cldr.Rbnf.rule_names_for_locale "fp"
{:error, {Cldr.InvalidLanguageError, "The language \"fp\" is invalid"}}
"""
@spec rule_names_for_locale(Cldr.LanguageTag.t()) ::
{:ok, list(atom())} | {:error, {module(), String.t()}}
def rule_names_for_locale(%LanguageTag{rbnf_locale_name: nil} = language_tag) do
{:error, rbnf_locale_error(language_tag)}
end
def rule_names_for_locale(%LanguageTag{rbnf_locale_name: rbnf_locale_name, backend: backend}) do
rule_names =
Enum.flat_map(@categories, fn category ->
rbnf_module = Module.concat([backend, :Rbnf, category])
rule_names = rbnf_module.rule_sets(rbnf_locale_name)
if rule_names, do: rule_names, else: []
end)
{:ok, rule_names}
end
def rule_names_for_locale(locale_name, backend \\ Cldr.default_backend!())
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
end
@doc """
Returns the list of RBNF rules for a locale.
A rule name can be used as the `:format` parameter
in `Cldr.Number.to_string/3`.
## Arguments
* `locale` is any `Cldr.LanguageTag.t()`
## Returns
* `[list_of_rule_names_as_atoms]`, or
* raises an exception
## Examples
iex> Cldr.Rbnf.rule_names_for_locale! "zh"
[:spellout_cardinal_alternate2, :spellout_ordinal, :spellout_cardinal,
:spellout_cardinal_financial, :spellout_numbering, :spellout_numbering_days,
:spellout_numbering_year, :digits_ordinal]
"""
@spec rule_names_for_locale!(Cldr.LanguageTag.t()) :: list(atom()) | no_return()
def rule_names_for_locale!(locale) do
case rule_names_for_locale(locale) do
{:ok, rule_names} -> rule_names
{:error, {exception, reason}} -> raise exception, reason
end
end
@doc """
Returns {:ok, rbnf_rules} for a `locale` or `{:error, {Cldr.NoRbnf, info}}`
* `locale` is any `t:Cldr.LanguageTag`
This function reads the raw locale definition and therefore
should *not* be called at runtime.
"""
@spec for_locale(LanguageTag.t()) ::
{:ok, map()} | {:error, {module(), String.t()}}
def for_locale(%LanguageTag{rbnf_locale_name: nil} = language_tag) do
{:error, rbnf_locale_error(language_tag)}
end
def for_locale(%LanguageTag{rbnf_locale_name: rbnf_locale_name, backend: backend}) do
rbnf_data =
rbnf_locale_name
|> Cldr.Locale.Loader.get_locale(backend)
|> Map.get(:rbnf)
{:ok, rbnf_data}
end
@doc """
Returns {:ok, rbnf_rules} for a `locale` or `{:error, {Cldr.NoRbnf, info}}`
* `locale` is any locale name returned by `Cldr.Rbnf.known_locale_names/1`
* `backend` is any module that includes `use Cldr` and therefore
is a `Cldr` backend module
"""
@spec for_locale(Cldr.Locale.locale_name() | LanguageTag.t(), Cldr.backend()) ::
{:ok, map()} | {:error, {module(), String.t()}}
def for_locale(locale, backend) do
with {:ok, language_tag} <- Cldr.Locale.canonical_language_tag(locale, backend) do
for_locale(language_tag)
end
end
@doc """
Returns rbnf_rules for a `locale` or raises an exception if
there are no rules.
* `locale` is any `Cldr.LanguageTag`
* `backend` is any module that includes `use Cldr` and therefore
is a `Cldr` backend module
"""
def for_locale!(%LanguageTag{} = locale) do
case for_locale(locale) do
{:ok, rules} -> rules
{:error, {exception, reason}} -> raise exception, reason
end
end
@doc """
Returns rbnf_rules for a `locale` and `backend` or raises an exception if
there are no rules.
* `locale` is any locale name returned by `Cldr.Rbnf.known_locale_names/1`
* `backend` is any module that includes `use Cldr` and therefore
is a `Cldr` backend module
"""
def for_locale!(locale, backend) when is_atom(backend) do
case for_locale(locale, backend) do
{:ok, rules} -> rules
{:error, {exception, reason}} -> raise exception, reason
end
end
@doc false
def categories_for_locale!(%LanguageTag{} = locale) do
Enum.reduce(@categories, [], fn category, acc ->
rbnf_module = Module.concat([locale.backend, :Rbnf, category])
case rbnf_module.rule_sets(locale) do
nil -> acc
_rules -> [category | acc]
end
end)
end
# Returns a map that merges all rules by the primary dimension of
# RuleGroup, within which rbnf rules are keyed by locale.
#
# This function is primarily intended to support compile-time generation
# of functions to process rbnf rules.
@doc false
@spec for_all_locales(Cldr.backend()) :: %{}
def for_all_locales(backend) do
config = Module.get_attribute(backend, :config)
known_rbnf_locale_names = Cldr.Locale.Loader.known_rbnf_locale_names(config)
Enum.map(known_rbnf_locale_names, fn locale_name ->
locale =
locale_name
|> Cldr.Locale.Loader.get_locale(config)
|> Map.get(:rbnf)
Enum.map(locale, fn {group, sets} ->
{group, %{locale_name => sets}}
end)
|> Enum.into(%{})
end)
|> Cldr.Map.merge_map_list()
end
def rbnf_locale_error(%LanguageTag{} = locale) do
{Cldr.Rbnf.NotAvailable, "RBNF is not available for locale #{inspect(locale)}"}
end
def rbnf_rule_error(%LanguageTag{} = locale, format) do
{
Cldr.Rbnf.NoRule,
"RBNF rule #{inspect(format)} is unknown to locale #{inspect(locale)}"
}
end
# Function names need to start with a letter. At least one
# rule in CLDR does not (2d_year). We prepend an "r" to the
# rule. This is also done in rbnf_parse.yrl so the strategies
# need to match.
@doc false
def force_valid_function_name(rule_group) do
function = to_string(rule_group)
case function do
<<digit::utf8, _rest :: binary>> when digit in ?0..?9 ->
"r" <> function
_other ->
function
end
|> String.to_atom()
end
if Mix.env() == :test do
# Returns all the rules in rbnf without any tagging for rulegroup or set.
# This is helpful for testing only.
@doc false
def all_rules(backend) do
# Get sets from groups
# Get rules from set
# Get the list of rules
known_locale_names(backend)
|> Enum.map(&Cldr.Locale.new!(&1, backend))
|> Enum.map(&for_locale!(&1))
|> Enum.flat_map(&Map.values/1)
|> Enum.flat_map(&Map.values/1)
|> Enum.flat_map(& &1.rules)
end
# Returns a list of unique rule definitions. Used for testing.
@doc false
def all_rule_definitions(backend) do
all_rules(backend)
|> Enum.map(& &1.definition)
|> Enum.uniq()
end
end
end