Skip to content

Commit f9a76cb

Browse files
authored
Support type parameter (ordinal/cardinal) in NUMBER() (#193)
1 parent ac21b16 commit f9a76cb

File tree

6 files changed

+84
-1
lines changed

6 files changed

+84
-1
lines changed

fluent.runtime/fluent/runtime/bundle.py

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ def __init__(self,
4747
self._babel_locale = self._get_babel_locale()
4848
self._plural_form = cast(Callable[[Any], Callable[[Union[int, float]], PluralCategory]],
4949
babel.plural.to_python)(self._babel_locale.plural_form)
50+
self._ordinal_form = cast(Callable[[Any], Callable[[Union[int, float]], PluralCategory]],
51+
babel.plural.to_python)(self._babel_locale.ordinal_form)
5052

5153
def add_resource(self, resource: FTL.Resource, allow_overrides: bool = False) -> None:
5254
# TODO - warn/error about duplicates

fluent.runtime/fluent/runtime/resolver.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,11 @@ def match(val1: Any, val2: Any, env: ResolverEnvironment) -> bool:
319319
if is_number(val1):
320320
if not is_number(val2):
321321
# Could be plural rule match
322-
return cast(bool, env.context._plural_form(val1) == val2)
322+
if isinstance(val1, (FluentInt, FluentFloat)) and val1.options.type == 'ordinal':
323+
val1_form = env.context._ordinal_form(val1)
324+
else:
325+
val1_form = env.context._plural_form(val1)
326+
return cast(bool, val1_form == val2)
323327
elif is_number(val2):
324328
return match(val2, val1, env)
325329

fluent.runtime/fluent/runtime/types.py

+10
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
CURRENCY_DISPLAY_NAME,
2929
}
3030

31+
NUMBER_TYPE_ORDINAL = "ordinal"
32+
NUMBER_TYPE_CARDINAL = "cardinal"
33+
NUMBER_TYPE_OPTIONS = {
34+
NUMBER_TYPE_ORDINAL,
35+
NUMBER_TYPE_CARDINAL,
36+
}
37+
3138
DATE_STYLE_OPTIONS = {
3239
"full",
3340
"long",
@@ -71,6 +78,9 @@ class NumberFormatOptions:
7178
style: Literal['decimal', 'currency', 'percent'] = attr.ib(
7279
default=FORMAT_STYLE_DECIMAL,
7380
validator=attr.validators.in_(FORMAT_STYLE_OPTIONS))
81+
type: Literal['ordinal', 'cardinal'] = attr.ib(
82+
default=NUMBER_TYPE_CARDINAL,
83+
validator=attr.validators.in_(NUMBER_TYPE_OPTIONS))
7484
currency: Union[str, None] = attr.ib(default=None)
7585
currencyDisplay: Literal['symbol', 'code', 'name'] = attr.ib(
7686
default=CURRENCY_DISPLAY_SYMBOL,

fluent.runtime/tests/format/test_primitives.py

+4
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ def setUp(self):
128128
*[0] Zero
129129
[1] One
130130
}
131+
position = { NUMBER(1, type: "ordinal") ->
132+
*[other] Zero
133+
[one] ${1}st
134+
}
131135
""")))
132136

133137
def test_int_number_used_in_placeable(self):

fluent.runtime/tests/format/test_select_expression.py

+48
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,18 @@ def setUp(self):
135135
[one] A
136136
*[other] B
137137
}
138+
139+
count = { NUMBER($num, type: "cardinal") ->
140+
*[other] B
141+
[one] A
142+
}
143+
144+
order = { NUMBER($num, type: "ordinal") ->
145+
*[other] {$num}th
146+
[one] {$num}st
147+
[two] {$num}nd
148+
[few] {$num}rd
149+
}
138150
""")))
139151

140152
def test_selects_the_right_category(self):
@@ -172,6 +184,42 @@ def test_with_argument_float(self):
172184
self.assertEqual(val, "A")
173185
self.assertEqual(len(errs), 0)
174186

187+
def test_with_cardinal_integer(self):
188+
val, errs = self.bundle.format_pattern(self.bundle.get_message('count').value, {'num': 1})
189+
self.assertEqual(val, "A")
190+
self.assertEqual(len(errs), 0)
191+
192+
val, errs = self.bundle.format_pattern(self.bundle.get_message('count').value, {'num': 2})
193+
self.assertEqual(val, "B")
194+
self.assertEqual(len(errs), 0)
195+
196+
def test_with_cardinal_float(self):
197+
val, errs = self.bundle.format_pattern(self.bundle.get_message('count').value, {'num': 1.0})
198+
self.assertEqual(val, "A")
199+
self.assertEqual(len(errs), 0)
200+
201+
def test_with_ordinal_integer(self):
202+
val, errs = self.bundle.format_pattern(self.bundle.get_message('order').value, {'num': 1})
203+
self.assertEqual(val, "1st")
204+
self.assertEqual(len(errs), 0)
205+
206+
val, errs = self.bundle.format_pattern(self.bundle.get_message('order').value, {'num': 2})
207+
self.assertEqual(val, "2nd")
208+
self.assertEqual(len(errs), 0)
209+
210+
val, errs = self.bundle.format_pattern(self.bundle.get_message('order').value, {'num': 11})
211+
self.assertEqual(val, "11th")
212+
self.assertEqual(len(errs), 0)
213+
214+
val, errs = self.bundle.format_pattern(self.bundle.get_message('order').value, {'num': 21})
215+
self.assertEqual(val, "21st")
216+
self.assertEqual(len(errs), 0)
217+
218+
def test_with_ordinal_float(self):
219+
val, errs = self.bundle.format_pattern(self.bundle.get_message('order').value, {'num': 1.0})
220+
self.assertEqual(val, "1st")
221+
self.assertEqual(len(errs), 0)
222+
175223

176224
class TestSelectExpressionWithTerms(unittest.TestCase):
177225

fluent.runtime/tests/test_bundle.py

+15
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,21 @@ def test_plural_form_french(self):
7575
self.assertEqual(bundle._plural_form(2),
7676
'other')
7777

78+
def test_ordinal_form_english_ints(self):
79+
bundle = FluentBundle(['en-US'])
80+
self.assertEqual(bundle._ordinal_form(0),
81+
'other')
82+
self.assertEqual(bundle._ordinal_form(1),
83+
'one')
84+
self.assertEqual(bundle._ordinal_form(2),
85+
'two')
86+
self.assertEqual(bundle._ordinal_form(3),
87+
'few')
88+
self.assertEqual(bundle._ordinal_form(11),
89+
'other')
90+
self.assertEqual(bundle._ordinal_form(21),
91+
'one')
92+
7893
def test_format_args(self):
7994
self.bundle.add_resource(FluentResource('foo = Foo'))
8095
val, errs = self.bundle.format_pattern(self.bundle.get_message('foo').value)

0 commit comments

Comments
 (0)