Skip to content

Commit 430e443

Browse files
committed
Add :gender & :grammatical_case options to Number.to_string/3
1 parent 1a8eed0 commit 430e443

5 files changed

Lines changed: 198 additions & 25 deletions

File tree

lib/cldr/exception.ex

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,27 @@ defmodule Cldr.Number.NoRationalFormatError do
9494
%__MODULE__{message: message}
9595
end
9696
end
97+
98+
defmodule Cldr.Number.UnknownGenderError do
99+
@moduledoc """
100+
Exception raised when when an unknown
101+
grammatical gender is specified.
102+
"""
103+
defexception [:message]
104+
105+
def exception(message) do
106+
%__MODULE__{message: message}
107+
end
108+
end
109+
110+
defmodule Cldr.Number.UnknownGrammaticalCaseError do
111+
@moduledoc """
112+
Exception raised when when an unknown
113+
grammatical case is specified.
114+
"""
115+
defexception [:message]
116+
117+
def exception(message) do
118+
%__MODULE__{message: message}
119+
end
120+
end

lib/cldr/number.ex

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,10 @@ defmodule Cldr.Number do
183183
The most commonly used formats in this category are to spell out the
184184
number in a locale's language. The applicable formats are `:spellout`,
185185
`:spellout_year` and `:ordinal`. A number can also be formatted as roman
186-
numbers by using the format `:roman` or `:roman_lower`.
186+
numbers by using the format `:roman` or `:roman_lower`. If the format is
187+
an RBNF format then the options `:gender` and/or `:grammatical_case` may also
188+
be provided. If they are provided then they will be used to try to resolve
189+
an available RBNF rule for the given `:locale`.
187190
188191
* `currency`: is the currency for which the number is formatted. If `currency`
189192
is set and no `:format` is set, `:format` will be set to `:currency` as well.
@@ -320,6 +323,28 @@ defmodule Cldr.Number do
320323
element type. The wrapper function is required to return a string that is then
321324
inserted in the final formatted number.
322325
326+
### RBNF with Grammatical Gender and Case
327+
328+
RBNF rules are used to format numbers - often to spell out a number
329+
or format an ordinal number. In certain locales, such formats require
330+
the specification of a grammatical gender (such as `:masculine`,
331+
`:feminine` or `:neuter`) and/or grammatical case.
332+
333+
Not all locales have RBNF rules, and those that do have RBNF rules
334+
may not have rules supporting grammatical gender or case. Therefore a
335+
fallback mechansms is required.
336+
337+
* First, an attempt is made to find an RBNF rule
338+
that combines the required format (such as `:spellout` or `:ordinal`) and
339+
the specified gender and gramnmatical case.
340+
341+
* If not found, an attempt to find a rule that is the combnation of the
342+
format plus the specified gender is made.
343+
344+
* Finally, of not otherwise found, an attempt is made to find the format
345+
without the requested gender or grammatical case.
346+
347+
323348
### Returns
324349
325350
* `{:ok, string}` or
@@ -497,15 +522,16 @@ defmodule Cldr.Number do
497522
end
498523

499524
@format_mapping [
500-
{:ordinal, :digits_ordinal, Ordinal},
501-
{:spellout, :spellout_numbering, Spellout},
502-
{:spellout_verbose, :spellout_numbering_verbose, Spellout},
503-
{:spellout_year, :spellout_numbering_year, Spellout}
525+
{:ordinal, :digits_ordinal},
526+
{:spellout, :spellout_numbering},
527+
{:spellout_verbose, :spellout_numbering_verbose},
528+
{:spellout_year, :spellout_numbering_year}
504529
]
505530

506-
for {format, function, module} <- @format_mapping do
531+
for {format, expanded_format} <- @format_mapping do
507532
defp to_string(number, unquote(format), backend, options) do
508-
evaluate_rule(number, unquote(module), unquote(function), options.locale, backend)
533+
to_string(number, unquote(expanded_format), backend, options)
534+
# evaluate_rule(number, unquote(module), unquote(function), options.locale, backend)
509535
end
510536
end
511537

@@ -541,8 +567,8 @@ defmodule Cldr.Number do
541567

542568
# For executing arbitrary RBNF rules that might exist for a given locale
543569
defp to_string(number, format, backend, options) when is_atom(format) do
544-
with {:ok, module, locale} <- find_rbnf_format_module(options.locale, format, backend) do
545-
apply(module, format, [number, locale])
570+
with {:ok, module, function, locale} <- find_rbnf_format_module(format, backend, options) do
571+
apply(module, function, [number, locale])
546572
end
547573
end
548574

@@ -556,38 +582,63 @@ defmodule Cldr.Number do
556582
# Look for the RBNF rule in the given locale or in the
557583
# root locale (called "und")
558584

559-
defp find_rbnf_format_module(locale, format, backend) do
585+
defp find_rbnf_format_module(format, backend, %Cldr.Number.Format.Options{locale: locale} = options) do
560586
root_locale = Map.put(@root_locale, :backend, backend)
587+
%Cldr.Number.Format.Options{gender: gender, grammatical_case: grammatical_case} = options
561588

562589
cond do
563-
module = find_rbnf_module(locale, format, backend) -> {:ok, module, locale}
564-
module = find_rbnf_module(root_locale, format, backend) -> {:ok, module, root_locale}
565-
true -> {:error, Cldr.Rbnf.rbnf_rule_error(locale, format)}
590+
module_and_function = find_rbnf_module(locale, format, gender, grammatical_case, backend) ->
591+
{module, function} = module_and_function
592+
{:ok, module, function, locale}
593+
module_and_function = find_rbnf_module(root_locale, format, gender, grammatical_case, backend) ->
594+
{module, function} = module_and_function
595+
{:ok, module, function, root_locale}
596+
true ->
597+
{:error, Cldr.Rbnf.rbnf_rule_error(locale, format)}
566598
end
567599
end
568600

569-
defp find_rbnf_module(locale, format, backend) do
601+
defp find_rbnf_module(locale, format, nil, nil, backend) do
570602
Enum.reduce_while(Cldr.Rbnf.categories_for_locale!(locale), nil, fn category, acc ->
571603
format_module = Module.concat([backend, :Rbnf, category])
572604
rules = format_module.rule_sets(locale)
573605

574606
if rules && format in rules do
575-
{:halt, format_module}
607+
{:halt, {format_module, format}}
576608
else
577609
{:cont, acc}
578610
end
579611
end)
580612
end
581613

582-
defp evaluate_rule(number, module, function, locale, backend) do
583-
module = Module.concat([backend, :Rbnf, module])
584-
rule_sets = module.rule_sets(locale)
614+
defp find_rbnf_module(locale, format, gender, nil, backend) do
615+
gendered_format = String.to_existing_atom("#{format}_#{gender}")
585616

586-
if rule_sets && function in rule_sets do
587-
apply(module, function, [number, locale])
588-
else
589-
{:error, Cldr.Rbnf.rbnf_rule_error(locale, function)}
617+
case find_rbnf_module(locale, gendered_format, nil, nil, backend) do
618+
{format_module, format} ->
619+
{format_module, format}
620+
621+
nil ->
622+
find_rbnf_module(locale, format, nil, nil, backend)
623+
end
624+
625+
rescue ArgumentError ->
626+
find_rbnf_module(locale, format, nil, nil, backend)
627+
end
628+
629+
defp find_rbnf_module(locale, format, gender, grammatical_case, backend) do
630+
gendered_cased_format = String.to_existing_atom("#{format}_#{gender}_#{grammatical_case}")
631+
632+
case find_rbnf_module(locale, gendered_cased_format, nil, nil, backend) do
633+
{format_module, format} ->
634+
{format_module, format}
635+
636+
nil ->
637+
find_rbnf_module(locale, format, gender, nil, backend)
590638
end
639+
640+
rescue ArgumentError ->
641+
find_rbnf_module(locale, format, gender, nil, backend)
591642
end
592643

593644
@doc """

lib/cldr/number/format/options.ex

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ defmodule Cldr.Number.Format.Options do
1818
:number_system,
1919
:currency,
2020
:format,
21+
:gender,
22+
:grammatical_case,
2123
:currency_format,
2224
:currency_digits,
2325
:currency_spacing,
@@ -89,6 +91,50 @@ defmodule Cldr.Number.Format.Options do
8991
:us
9092
]
9193

94+
# See https://unicode.org/reports/tr35/tr35-general.html#Case
95+
@grammatical_case [
96+
:abessive,
97+
:ablative,
98+
:accusative,
99+
:adessive,
100+
:allative,
101+
:causal,
102+
:comitative,
103+
:dative,
104+
:delative,
105+
:elative,
106+
:ergative,
107+
:genitive,
108+
:illative,
109+
:inessive,
110+
:instrumental,
111+
:locative,
112+
:localtivecopulative,
113+
:nominative,
114+
:oblique,
115+
:partitive,
116+
:prepositional,
117+
:sociative,
118+
:sublative,
119+
:superessive,
120+
:terminative,
121+
:translative,
122+
:vocative,
123+
nil
124+
]
125+
126+
@grammatical_gender [
127+
:animate,
128+
:inanimate,
129+
:personal,
130+
:common,
131+
:feminine,
132+
:masculine,
133+
:neuter,
134+
:plural,
135+
nil
136+
]
137+
92138
@type fixed_format :: :standard | :currency | :accounting | :short | :long
93139
@type format :: binary() | fixed_format()
94140
@type currency_symbol :: :standard | :iso | :narrow | :symbol | :none
@@ -100,11 +146,18 @@ defmodule Cldr.Number.Format.Options do
100146
| :decimal_short
101147
| :decimal_long
102148

149+
type = &Enum.reduce(&1, fn x, acc -> {:|, [], [x, acc]} end)
150+
151+
@type gender :: unquote(type.(@grammatical_gender))
152+
@type grammatical_case :: unquote(type.(@grammatical_case))
153+
103154
@type t :: %__MODULE__{
104155
locale: LanguageTag.t(),
105156
number_system: System.system_name(),
106157
currency: Currency.t() | :from_locale,
107158
format: format(),
159+
gender: gender(),
160+
grammatical_case: grammatical_case(),
108161
currency_format: :currency | :accounting,
109162
currency_digits: pos_integer(),
110163
currency_spacing: map(),
@@ -121,6 +174,23 @@ defmodule Cldr.Number.Format.Options do
121174

122175
defstruct @options
123176

177+
@doc """
178+
Returns the list of valid grammatical genders.
179+
180+
"""
181+
def grammatical_gender do
182+
@grammatical_gender
183+
end
184+
185+
@doc """
186+
Returns the list of valid grammatical cases.
187+
188+
"""
189+
def grammatical_case do
190+
@grammatical_case
191+
end
192+
193+
124194
@spec validate_options(Cldr.Math.number_or_decimal(), Cldr.backend(), list({atom, term})) ::
125195
{:ok, t} | {:error, {module(), String.t()}}
126196

@@ -355,6 +425,16 @@ defmodule Cldr.Number.Format.Options do
355425
"does not define a format #{inspect(format)}"}
356426
end
357427

428+
defp unknown_gender_error(gender) do
429+
{Cldr.Number.UnknownGenderError,
430+
"The grammatical gender #{inspect gender} is not known"}
431+
end
432+
433+
defp unknown_grammatical_case_error(grammatical_case) do
434+
{Cldr.Number.UnknownGrammaticalCaseError,
435+
"The grammatical case #{inspect grammatical_case} is not known"}
436+
end
437+
358438
@currency_placeholder Compiler.placeholder(:currency)
359439
# @iso_placeholder Compiler.placeholder(:currency) <> Compiler.placeholder(:currency)
360440

@@ -581,6 +661,24 @@ defmodule Cldr.Number.Format.Options do
581661
end
582662
end
583663

664+
defp validate_option(:gender, _options, _backend, gender)
665+
when gender in @grammatical_gender do
666+
{:ok, gender}
667+
end
668+
669+
defp validate_option(:gender, _options, _backend, gender) do
670+
{:error, unknown_gender_error(gender)}
671+
end
672+
673+
defp validate_option(:grammatical_case, _options, _backend, grammatical_case)
674+
when grammatical_case in @grammatical_case do
675+
{:ok, grammatical_case}
676+
end
677+
678+
defp validate_option(:grammatical_case, _options, _backend, grammatical_case) do
679+
{:error, unknown_grammatical_case_error(grammatical_case)}
680+
end
681+
584682
@exclude_formats [
585683
:currency,
586684
:accounting,

lib/cldr/number/rbnf.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,13 +226,13 @@ defmodule Cldr.Rbnf do
226226
end
227227

228228
def rbnf_locale_error(%LanguageTag{} = locale) do
229-
{Cldr.Rbnf.NotAvailable, "RBNF is not available for locale #{inspect(locale)}"}
229+
{Cldr.Rbnf.NotAvailable, "RBNF is not available for locale #{inspect(to_string(locale))}"}
230230
end
231231

232232
def rbnf_rule_error(%LanguageTag{} = locale, format) do
233233
{
234234
Cldr.Rbnf.NoRule,
235-
"RBNF rule #{inspect(format)} is unknown to locale #{inspect(locale)}"
235+
"RBNF rule #{inspect(format)} is unknown to locale #{inspect(to_string(locale))}"
236236
}
237237
end
238238

test/number/number_format_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ defmodule Number.Format.Test do
6666
:error,
6767
{
6868
Cldr.Rbnf.NoRule,
69-
"RBNF rule :spellout_ordinal_verbose is unknown to locale TestBackend.Cldr.Locale.new!(\"zh-Hans-CN\")"
69+
"RBNF rule :spellout_ordinal_verbose is unknown to locale \"zh\""
7070
}
7171
}
7272
end

0 commit comments

Comments
 (0)