plural.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. """
  2. babel.numbers
  3. ~~~~~~~~~~~~~
  4. CLDR Plural support. See UTS #35.
  5. :copyright: (c) 2013-2026 by the Babel Team.
  6. :license: BSD, see LICENSE for more details.
  7. """
  8. from __future__ import annotations
  9. import decimal
  10. import re
  11. from collections.abc import Iterable, Mapping
  12. from typing import Any, Callable, Literal
  13. _plural_tags = ('zero', 'one', 'two', 'few', 'many', 'other')
  14. _fallback_tag = 'other'
  15. def extract_operands(
  16. source: float | decimal.Decimal,
  17. ) -> tuple[decimal.Decimal | int, int, int, int, int, int, Literal[0], Literal[0]]:
  18. """Extract operands from a decimal, a float or an int, according to `CLDR rules`_.
  19. The result is an 8-tuple (n, i, v, w, f, t, c, e), where those symbols are as follows:
  20. ====== ===============================================================
  21. Symbol Value
  22. ------ ---------------------------------------------------------------
  23. n absolute value of the source number (integer and decimals).
  24. i integer digits of n.
  25. v number of visible fraction digits in n, with trailing zeros.
  26. w number of visible fraction digits in n, without trailing zeros.
  27. f visible fractional digits in n, with trailing zeros.
  28. t visible fractional digits in n, without trailing zeros.
  29. c compact decimal exponent value: exponent of the power of 10 used in compact decimal formatting.
  30. e currently, synonym for ‘c’. however, may be redefined in the future.
  31. ====== ===============================================================
  32. .. _`CLDR rules`: https://www.unicode.org/reports/tr35/tr35-61/tr35-numbers.html#Operands
  33. :param source: A real number
  34. :type source: int|float|decimal.Decimal
  35. :return: A n-i-v-w-f-t-c-e tuple
  36. :rtype: tuple[decimal.Decimal, int, int, int, int, int, int, int]
  37. """
  38. n = abs(source)
  39. i = int(n)
  40. if isinstance(n, float):
  41. if i == n:
  42. n = i
  43. else:
  44. # Cast the `float` to a number via the string representation.
  45. # This is required for Python 2.6 anyway (it will straight out fail to
  46. # do the conversion otherwise), and it's highly unlikely that the user
  47. # actually wants the lossless conversion behavior (quoting the Python
  48. # documentation):
  49. # > If value is a float, the binary floating point value is losslessly
  50. # > converted to its exact decimal equivalent.
  51. # > This conversion can often require 53 or more digits of precision.
  52. # Should the user want that behavior, they can simply pass in a pre-
  53. # converted `Decimal` instance of desired accuracy.
  54. n = decimal.Decimal(str(n))
  55. if isinstance(n, decimal.Decimal):
  56. dec_tuple = n.as_tuple()
  57. exp = dec_tuple.exponent
  58. fraction_digits = dec_tuple.digits[exp:] if exp < 0 else ()
  59. trailing = ''.join(str(d) for d in fraction_digits)
  60. no_trailing = trailing.rstrip('0')
  61. v = len(trailing)
  62. w = len(no_trailing)
  63. f = int(trailing or 0)
  64. t = int(no_trailing or 0)
  65. else:
  66. v = w = f = t = 0
  67. c = e = 0 # TODO: c and e are not supported
  68. return n, i, v, w, f, t, c, e
  69. class PluralRule:
  70. """Represents a set of language pluralization rules. The constructor
  71. accepts a list of (tag, expr) tuples or a dict of `CLDR rules`_. The
  72. resulting object is callable and accepts one parameter with a positive or
  73. negative number (both integer and float) for the number that indicates the
  74. plural form for a string and returns the tag for the format:
  75. >>> rule = PluralRule({'one': 'n is 1'})
  76. >>> rule(1)
  77. 'one'
  78. >>> rule(2)
  79. 'other'
  80. Currently the CLDR defines these tags: zero, one, two, few, many and
  81. other where other is an implicit default. Rules should be mutually
  82. exclusive; for a given numeric value, only one rule should apply (i.e.
  83. the condition should only be true for one of the plural rule elements.
  84. .. _`CLDR rules`: https://www.unicode.org/reports/tr35/tr35-33/tr35-numbers.html#Language_Plural_Rules
  85. """
  86. __slots__ = ('abstract', '_func')
  87. def __init__(self, rules: Mapping[str, str] | Iterable[tuple[str, str]]) -> None:
  88. """Initialize the rule instance.
  89. :param rules: a list of ``(tag, expr)``) tuples with the rules
  90. conforming to UTS #35 or a dict with the tags as keys
  91. and expressions as values.
  92. :raise RuleError: if the expression is malformed
  93. """
  94. if isinstance(rules, Mapping):
  95. rules = rules.items()
  96. found = set()
  97. self.abstract: list[tuple[str, Any]] = []
  98. for key, expr in sorted(rules):
  99. if key not in _plural_tags:
  100. raise ValueError(f"unknown tag {key!r}")
  101. elif key in found:
  102. raise ValueError(f"tag {key!r} defined twice")
  103. found.add(key)
  104. ast = _Parser(expr).ast
  105. if ast:
  106. self.abstract.append((key, ast))
  107. def __repr__(self) -> str:
  108. rules = self.rules
  109. args = ", ".join(f"{tag}: {rules[tag]}" for tag in _plural_tags if tag in rules)
  110. return f"<{type(self).__name__} {args!r}>"
  111. @classmethod
  112. def parse(
  113. cls,
  114. rules: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule,
  115. ) -> PluralRule:
  116. """Create a `PluralRule` instance for the given rules. If the rules
  117. are a `PluralRule` object, that object is returned.
  118. :param rules: the rules as list or dict, or a `PluralRule` object
  119. :raise RuleError: if the expression is malformed
  120. """
  121. if isinstance(rules, PluralRule):
  122. return rules
  123. return cls(rules)
  124. @property
  125. def rules(self) -> Mapping[str, str]:
  126. """The `PluralRule` as a dict of unicode plural rules.
  127. >>> rule = PluralRule({'one': 'n is 1'})
  128. >>> rule.rules
  129. {'one': 'n is 1'}
  130. """
  131. _compile = _UnicodeCompiler().compile
  132. return {tag: _compile(ast) for tag, ast in self.abstract}
  133. @property
  134. def tags(self) -> frozenset[str]:
  135. """A set of explicitly defined tags in this rule. The implicit default
  136. ``'other'`` rules is not part of this set unless there is an explicit
  137. rule for it.
  138. """
  139. return frozenset(i[0] for i in self.abstract)
  140. def __getstate__(self) -> list[tuple[str, Any]]:
  141. return self.abstract
  142. def __setstate__(self, abstract: list[tuple[str, Any]]) -> None:
  143. self.abstract = abstract
  144. def __call__(self, n: float | decimal.Decimal) -> str:
  145. if not hasattr(self, '_func'):
  146. self._func = to_python(self)
  147. return self._func(n)
  148. def to_javascript(rule: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule) -> str:
  149. """Convert a list/dict of rules or a `PluralRule` object into a JavaScript
  150. function. This function depends on no external library:
  151. >>> to_javascript({'one': 'n is 1'})
  152. "(function(n) { return (n == 1) ? 'one' : 'other'; })"
  153. Implementation detail: The function generated will probably evaluate
  154. expressions involved into range operations multiple times. This has the
  155. advantage that external helper functions are not required and is not a
  156. big performance hit for these simple calculations.
  157. :param rule: the rules as list or dict, or a `PluralRule` object
  158. :raise RuleError: if the expression is malformed
  159. """
  160. to_js = _JavaScriptCompiler().compile
  161. result = ['(function(n) { return ']
  162. for tag, ast in PluralRule.parse(rule).abstract:
  163. result.append(f"{to_js(ast)} ? {tag!r} : ")
  164. result.append('%r; })' % _fallback_tag)
  165. return ''.join(result)
  166. def to_python(
  167. rule: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule,
  168. ) -> Callable[[float | decimal.Decimal], str]:
  169. """Convert a list/dict of rules or a `PluralRule` object into a regular
  170. Python function. This is useful in situations where you need a real
  171. function and don't are about the actual rule object:
  172. >>> func = to_python({'one': 'n is 1', 'few': 'n in 2..4'})
  173. >>> func(1)
  174. 'one'
  175. >>> func(3)
  176. 'few'
  177. >>> func = to_python({'one': 'n in 1,11', 'few': 'n in 3..10,13..19'})
  178. >>> func(11)
  179. 'one'
  180. >>> func(15)
  181. 'few'
  182. :param rule: the rules as list or dict, or a `PluralRule` object
  183. :raise RuleError: if the expression is malformed
  184. """
  185. namespace = {
  186. 'IN': in_range_list,
  187. 'WITHIN': within_range_list,
  188. 'MOD': cldr_modulo,
  189. 'extract_operands': extract_operands,
  190. }
  191. to_python_func = _PythonCompiler().compile
  192. result = [
  193. 'def evaluate(n):',
  194. ' n, i, v, w, f, t, c, e = extract_operands(n)',
  195. ]
  196. for tag, ast in PluralRule.parse(rule).abstract:
  197. # the str() call is to coerce the tag to the native string. It's
  198. # a limited ascii restricted set of tags anyways so that is fine.
  199. result.append(f" if ({to_python_func(ast)}): return {str(tag)!r}")
  200. result.append(f" return {_fallback_tag!r}")
  201. code = compile('\n'.join(result), '<rule>', 'exec')
  202. eval(code, namespace)
  203. return namespace['evaluate']
  204. def to_gettext(rule: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule) -> str:
  205. """The plural rule as gettext expression. The gettext expression is
  206. technically limited to integers and returns indices rather than tags.
  207. >>> to_gettext({'one': 'n is 1', 'two': 'n is 2'})
  208. 'nplurals=3; plural=((n == 1) ? 0 : (n == 2) ? 1 : 2);'
  209. :param rule: the rules as list or dict, or a `PluralRule` object
  210. :raise RuleError: if the expression is malformed
  211. """
  212. rule = PluralRule.parse(rule)
  213. used_tags = rule.tags | {_fallback_tag}
  214. _compile = _GettextCompiler().compile
  215. _get_index = [tag for tag in _plural_tags if tag in used_tags].index
  216. result = [f"nplurals={len(used_tags)}; plural=("]
  217. for tag, ast in rule.abstract:
  218. result.append(f"{_compile(ast)} ? {_get_index(tag)} : ")
  219. result.append(f"{_get_index(_fallback_tag)});")
  220. return ''.join(result)
  221. def in_range_list(
  222. num: float | decimal.Decimal,
  223. range_list: Iterable[Iterable[float | decimal.Decimal]],
  224. ) -> bool:
  225. """Integer range list test. This is the callback for the "in" operator
  226. of the UTS #35 pluralization rule language:
  227. >>> in_range_list(1, [(1, 3)])
  228. True
  229. >>> in_range_list(3, [(1, 3)])
  230. True
  231. >>> in_range_list(3, [(1, 3), (5, 8)])
  232. True
  233. >>> in_range_list(1.2, [(1, 4)])
  234. False
  235. >>> in_range_list(10, [(1, 4)])
  236. False
  237. >>> in_range_list(10, [(1, 4), (6, 8)])
  238. False
  239. """
  240. return num == int(num) and within_range_list(num, range_list)
  241. def within_range_list(
  242. num: float | decimal.Decimal,
  243. range_list: Iterable[Iterable[float | decimal.Decimal]],
  244. ) -> bool:
  245. """Float range test. This is the callback for the "within" operator
  246. of the UTS #35 pluralization rule language:
  247. >>> within_range_list(1, [(1, 3)])
  248. True
  249. >>> within_range_list(1.0, [(1, 3)])
  250. True
  251. >>> within_range_list(1.2, [(1, 4)])
  252. True
  253. >>> within_range_list(8.8, [(1, 4), (7, 15)])
  254. True
  255. >>> within_range_list(10, [(1, 4)])
  256. False
  257. >>> within_range_list(10.5, [(1, 4), (20, 30)])
  258. False
  259. """
  260. return any(min_ <= num <= max_ for min_, max_ in range_list)
  261. def cldr_modulo(a: float, b: float) -> float:
  262. """Javaish modulo. This modulo operator returns the value with the sign
  263. of the dividend rather than the divisor like Python does:
  264. >>> cldr_modulo(-3, 5)
  265. -3
  266. >>> cldr_modulo(-3, -5)
  267. -3
  268. >>> cldr_modulo(3, 5)
  269. 3
  270. """
  271. reverse = 0
  272. if a < 0:
  273. a *= -1
  274. reverse = 1
  275. if b < 0:
  276. b *= -1
  277. rv = a % b
  278. if reverse:
  279. rv *= -1
  280. return rv
  281. class RuleError(Exception):
  282. """Raised if a rule is malformed."""
  283. _VARS = {
  284. 'n', # absolute value of the source number.
  285. 'i', # integer digits of n.
  286. 'v', # number of visible fraction digits in n, with trailing zeros.*
  287. 'w', # number of visible fraction digits in n, without trailing zeros.*
  288. 'f', # visible fraction digits in n, with trailing zeros.*
  289. 't', # visible fraction digits in n, without trailing zeros.*
  290. 'c', # compact decimal exponent value: exponent of the power of 10 used in compact decimal formatting.
  291. 'e', # currently, synonym for `c`. however, may be redefined in the future.
  292. }
  293. _RULES: list[tuple[str | None, re.Pattern[str]]] = [
  294. (None, re.compile(r'\s+', re.UNICODE)),
  295. ('word', re.compile(rf'\b(and|or|is|(?:with)?in|not|mod|[{"".join(_VARS)}])\b')),
  296. ('value', re.compile(r'\d+')),
  297. ('symbol', re.compile(r'%|,|!=|=')),
  298. ('ellipsis', re.compile(r'\.{2,3}|\u2026', re.UNICODE)), # U+2026: ELLIPSIS
  299. ]
  300. def tokenize_rule(s: str) -> list[tuple[str, str]]:
  301. s = s.split('@')[0]
  302. result: list[tuple[str, str]] = []
  303. pos = 0
  304. end = len(s)
  305. while pos < end:
  306. for tok, rule in _RULES:
  307. match = rule.match(s, pos)
  308. if match is not None:
  309. pos = match.end()
  310. if tok:
  311. result.append((tok, match.group()))
  312. break
  313. else:
  314. raise RuleError(f"malformed CLDR pluralization rule. Got unexpected {s[pos]!r}")
  315. return result[::-1]
  316. def test_next_token(
  317. tokens: list[tuple[str, str]],
  318. type_: str,
  319. value: str | None = None,
  320. ) -> list[tuple[str, str]] | bool:
  321. return tokens and tokens[-1][0] == type_ and (value is None or tokens[-1][1] == value)
  322. def skip_token(tokens: list[tuple[str, str]], type_: str, value: str | None = None):
  323. if test_next_token(tokens, type_, value):
  324. return tokens.pop()
  325. def value_node(value: int) -> tuple[Literal['value'], tuple[int]]:
  326. return 'value', (value,)
  327. def ident_node(name: str) -> tuple[str, tuple[()]]:
  328. return name, ()
  329. def range_list_node(
  330. range_list: Iterable[Iterable[float | decimal.Decimal]],
  331. ) -> tuple[Literal['range_list'], Iterable[Iterable[float | decimal.Decimal]]]:
  332. return 'range_list', range_list
  333. def negate(rv: tuple[Any, ...]) -> tuple[Literal['not'], tuple[tuple[Any, ...]]]:
  334. return 'not', (rv,)
  335. class _Parser:
  336. """Internal parser. This class can translate a single rule into an abstract
  337. tree of tuples. It implements the following grammar::
  338. condition = and_condition ('or' and_condition)*
  339. ('@integer' samples)?
  340. ('@decimal' samples)?
  341. and_condition = relation ('and' relation)*
  342. relation = is_relation | in_relation | within_relation
  343. is_relation = expr 'is' ('not')? value
  344. in_relation = expr (('not')? 'in' | '=' | '!=') range_list
  345. within_relation = expr ('not')? 'within' range_list
  346. expr = operand (('mod' | '%') value)?
  347. operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
  348. range_list = (range | value) (',' range_list)*
  349. value = digit+
  350. digit = 0|1|2|3|4|5|6|7|8|9
  351. range = value'..'value
  352. samples = sampleRange (',' sampleRange)* (',' ('…'|'...'))?
  353. sampleRange = decimalValue '~' decimalValue
  354. decimalValue = value ('.' value)?
  355. - Whitespace can occur between or around any of the above tokens.
  356. - Rules should be mutually exclusive; for a given numeric value, only one
  357. rule should apply (i.e. the condition should only be true for one of
  358. the plural rule elements).
  359. - The in and within relations can take comma-separated lists, such as:
  360. 'n in 3,5,7..15'.
  361. - Samples are ignored.
  362. The translator parses the expression on instantiation into an attribute
  363. called `ast`.
  364. """
  365. def __init__(self, string):
  366. self.tokens = tokenize_rule(string)
  367. if not self.tokens:
  368. # If the pattern is only samples, it's entirely possible
  369. # no stream of tokens whatsoever is generated.
  370. self.ast = None
  371. return
  372. self.ast = self.condition()
  373. if self.tokens:
  374. raise RuleError(f"Expected end of rule, got {self.tokens[-1][1]!r}")
  375. def expect(self, type_, value=None, term=None):
  376. token = skip_token(self.tokens, type_, value)
  377. if token is not None:
  378. return token
  379. if term is None:
  380. term = repr(value is None and type_ or value)
  381. if not self.tokens:
  382. raise RuleError(f"expected {term} but end of rule reached")
  383. raise RuleError(f"expected {term} but got {self.tokens[-1][1]!r}")
  384. def condition(self):
  385. op = self.and_condition()
  386. while skip_token(self.tokens, 'word', 'or'):
  387. op = 'or', (op, self.and_condition())
  388. return op
  389. def and_condition(self):
  390. op = self.relation()
  391. while skip_token(self.tokens, 'word', 'and'):
  392. op = 'and', (op, self.relation())
  393. return op
  394. def relation(self):
  395. left = self.expr()
  396. if skip_token(self.tokens, 'word', 'is'):
  397. op = 'isnot' if skip_token(self.tokens, 'word', 'not') else 'is'
  398. return op, (left, self.value())
  399. negated = skip_token(self.tokens, 'word', 'not')
  400. method = 'in'
  401. if skip_token(self.tokens, 'word', 'within'):
  402. method = 'within'
  403. else:
  404. if not skip_token(self.tokens, 'word', 'in'):
  405. if negated:
  406. raise RuleError('Cannot negate operator based rules.')
  407. return self.newfangled_relation(left)
  408. rv = 'relation', (method, left, self.range_list())
  409. return negate(rv) if negated else rv
  410. def newfangled_relation(self, left):
  411. if skip_token(self.tokens, 'symbol', '='):
  412. negated = False
  413. elif skip_token(self.tokens, 'symbol', '!='):
  414. negated = True
  415. else:
  416. raise RuleError('Expected "=" or "!=" or legacy relation')
  417. rv = 'relation', ('in', left, self.range_list())
  418. return negate(rv) if negated else rv
  419. def range_or_value(self):
  420. left = self.value()
  421. if skip_token(self.tokens, 'ellipsis'):
  422. return left, self.value()
  423. else:
  424. return left, left
  425. def range_list(self):
  426. range_list = [self.range_or_value()]
  427. while skip_token(self.tokens, 'symbol', ','):
  428. range_list.append(self.range_or_value())
  429. return range_list_node(range_list)
  430. def expr(self):
  431. word = skip_token(self.tokens, 'word')
  432. if word is None or word[1] not in _VARS:
  433. raise RuleError('Expected identifier variable')
  434. name = word[1]
  435. if skip_token(self.tokens, 'word', 'mod'):
  436. return 'mod', ((name, ()), self.value())
  437. elif skip_token(self.tokens, 'symbol', '%'):
  438. return 'mod', ((name, ()), self.value())
  439. return ident_node(name)
  440. def value(self):
  441. return value_node(int(self.expect('value')[1]))
  442. def _binary_compiler(tmpl):
  443. """Compiler factory for the `_Compiler`."""
  444. return lambda self, left, right: tmpl % (self.compile(left), self.compile(right))
  445. def _unary_compiler(tmpl):
  446. """Compiler factory for the `_Compiler`."""
  447. return lambda self, x: tmpl % self.compile(x)
  448. compile_zero = lambda x: '0'
  449. class _Compiler:
  450. """The compilers are able to transform the expressions into multiple
  451. output formats.
  452. """
  453. def compile(self, arg):
  454. op, args = arg
  455. return getattr(self, f"compile_{op}")(*args)
  456. compile_n = lambda x: 'n'
  457. compile_i = lambda x: 'i'
  458. compile_v = lambda x: 'v'
  459. compile_w = lambda x: 'w'
  460. compile_f = lambda x: 'f'
  461. compile_t = lambda x: 't'
  462. compile_c = lambda x: 'c'
  463. compile_e = lambda x: 'e'
  464. compile_value = lambda x, v: str(v)
  465. compile_and = _binary_compiler('(%s && %s)')
  466. compile_or = _binary_compiler('(%s || %s)')
  467. compile_not = _unary_compiler('(!%s)')
  468. compile_mod = _binary_compiler('(%s %% %s)')
  469. compile_is = _binary_compiler('(%s == %s)')
  470. compile_isnot = _binary_compiler('(%s != %s)')
  471. def compile_relation(self, method, expr, range_list):
  472. raise NotImplementedError()
  473. class _PythonCompiler(_Compiler):
  474. """Compiles an expression to Python."""
  475. compile_and = _binary_compiler('(%s and %s)')
  476. compile_or = _binary_compiler('(%s or %s)')
  477. compile_not = _unary_compiler('(not %s)')
  478. compile_mod = _binary_compiler('MOD(%s, %s)')
  479. def compile_relation(self, method, expr, range_list):
  480. ranges = ",".join(
  481. f"({self.compile(a)}, {self.compile(b)})" for (a, b) in range_list[1]
  482. )
  483. return f"{method.upper()}({self.compile(expr)}, [{ranges}])"
  484. class _GettextCompiler(_Compiler):
  485. """Compile into a gettext plural expression."""
  486. compile_i = _Compiler.compile_n
  487. compile_v = compile_zero
  488. compile_w = compile_zero
  489. compile_f = compile_zero
  490. compile_t = compile_zero
  491. def compile_relation(self, method, expr, range_list):
  492. rv = []
  493. expr = self.compile(expr)
  494. for item in range_list[1]:
  495. if item[0] == item[1]:
  496. rv.append(f"({expr} == {self.compile(item[0])})")
  497. else:
  498. min = self.compile(item[0])
  499. max = self.compile(item[1])
  500. rv.append(f"({expr} >= {min} && {expr} <= {max})")
  501. return f"({' || '.join(rv)})"
  502. class _JavaScriptCompiler(_GettextCompiler):
  503. """Compiles the expression to plain of JavaScript."""
  504. # XXX: presently javascript does not support any of the
  505. # fraction support and basically only deals with integers.
  506. compile_i = lambda x: 'parseInt(n, 10)'
  507. compile_v = compile_zero
  508. compile_w = compile_zero
  509. compile_f = compile_zero
  510. compile_t = compile_zero
  511. def compile_relation(self, method, expr, range_list):
  512. code = _GettextCompiler.compile_relation(self, method, expr, range_list)
  513. if method == 'in':
  514. expr = self.compile(expr)
  515. code = f"(parseInt({expr}, 10) == {expr} && {code})"
  516. return code
  517. class _UnicodeCompiler(_Compiler):
  518. """Returns a unicode pluralization rule again."""
  519. # XXX: this currently spits out the old syntax instead of the new
  520. # one. We can change that, but it will break a whole bunch of stuff
  521. # for users I suppose.
  522. compile_is = _binary_compiler('%s is %s')
  523. compile_isnot = _binary_compiler('%s is not %s')
  524. compile_and = _binary_compiler('%s and %s')
  525. compile_or = _binary_compiler('%s or %s')
  526. compile_mod = _binary_compiler('%s mod %s')
  527. def compile_not(self, relation):
  528. return self.compile_relation(*relation[1], negated=True)
  529. def compile_relation(self, method, expr, range_list, negated=False):
  530. ranges = []
  531. for item in range_list[1]:
  532. if item[0] == item[1]:
  533. ranges.append(self.compile(item[0]))
  534. else:
  535. ranges.append(f"{self.compile(item[0])}..{self.compile(item[1])}")
  536. return f"{self.compile(expr)}{' not' if negated else ''} {method} {','.join(ranges)}"