Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/babel/plural.py: 36%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

291 statements  

1""" 

2babel.numbers 

3~~~~~~~~~~~~~ 

4 

5CLDR Plural support. See UTS #35. 

6 

7:copyright: (c) 2013-2026 by the Babel Team. 

8:license: BSD, see LICENSE for more details. 

9""" 

10 

11from __future__ import annotations 

12 

13import decimal 

14import re 

15from collections.abc import Iterable, Mapping 

16from typing import Any, Callable, Literal 

17 

18_plural_tags = ('zero', 'one', 'two', 'few', 'many', 'other') 

19_fallback_tag = 'other' 

20 

21 

22def extract_operands( 

23 source: float | decimal.Decimal, 

24) -> tuple[decimal.Decimal | int, int, int, int, int, int, Literal[0], Literal[0]]: 

25 """Extract operands from a decimal, a float or an int, according to `CLDR rules`_. 

26 

27 The result is an 8-tuple (n, i, v, w, f, t, c, e), where those symbols are as follows: 

28 

29 ====== =============================================================== 

30 Symbol Value 

31 ------ --------------------------------------------------------------- 

32 n absolute value of the source number (integer and decimals). 

33 i integer digits of n. 

34 v number of visible fraction digits in n, with trailing zeros. 

35 w number of visible fraction digits in n, without trailing zeros. 

36 f visible fractional digits in n, with trailing zeros. 

37 t visible fractional digits in n, without trailing zeros. 

38 c compact decimal exponent value: exponent of the power of 10 used in compact decimal formatting. 

39 e currently, synonym for ‘c’. however, may be redefined in the future. 

40 ====== =============================================================== 

41 

42 .. _`CLDR rules`: https://www.unicode.org/reports/tr35/tr35-61/tr35-numbers.html#Operands 

43 

44 :param source: A real number 

45 :return: A n-i-v-w-f-t-c-e tuple 

46 :rtype: tuple[decimal.Decimal, int, int, int, int, int, int, int] 

47 """ 

48 n = abs(source) 

49 i = int(n) 

50 if isinstance(n, float): 

51 if i == n: 

52 n = i 

53 else: 

54 # Cast the `float` to a number via the string representation. 

55 # This is required for Python 2.6 anyway (it will straight out fail to 

56 # do the conversion otherwise), and it's highly unlikely that the user 

57 # actually wants the lossless conversion behavior (quoting the Python 

58 # documentation): 

59 # > If value is a float, the binary floating point value is losslessly 

60 # > converted to its exact decimal equivalent. 

61 # > This conversion can often require 53 or more digits of precision. 

62 # Should the user want that behavior, they can simply pass in a pre- 

63 # converted `Decimal` instance of desired accuracy. 

64 n = decimal.Decimal(str(n)) 

65 

66 if isinstance(n, decimal.Decimal): 

67 dec_tuple = n.as_tuple() 

68 exp = dec_tuple.exponent 

69 fraction_digits = dec_tuple.digits[exp:] if exp < 0 else () 

70 trailing = ''.join(str(d) for d in fraction_digits) 

71 no_trailing = trailing.rstrip('0') 

72 v = len(trailing) 

73 w = len(no_trailing) 

74 f = int(trailing or 0) 

75 t = int(no_trailing or 0) 

76 else: 

77 v = w = f = t = 0 

78 c = e = 0 # TODO: c and e are not supported 

79 return n, i, v, w, f, t, c, e 

80 

81 

82class PluralRule: 

83 """Represents a set of language pluralization rules. The constructor 

84 accepts a list of (tag, expr) tuples or a dict of `CLDR rules`_. The 

85 resulting object is callable and accepts one parameter with a positive or 

86 negative number (both integer and float) for the number that indicates the 

87 plural form for a string and returns the tag for the format: 

88 

89 >>> rule = PluralRule({'one': 'n is 1'}) 

90 >>> rule(1) 

91 'one' 

92 >>> rule(2) 

93 'other' 

94 

95 Currently the CLDR defines these tags: zero, one, two, few, many and 

96 other where other is an implicit default. Rules should be mutually 

97 exclusive; for a given numeric value, only one rule should apply (i.e. 

98 the condition should only be true for one of the plural rule elements. 

99 

100 .. _`CLDR rules`: https://www.unicode.org/reports/tr35/tr35-33/tr35-numbers.html#Language_Plural_Rules 

101 """ 

102 

103 __slots__ = ('abstract', '_func') 

104 

105 def __init__(self, rules: Mapping[str, str] | Iterable[tuple[str, str]]) -> None: 

106 """Initialize the rule instance. 

107 

108 :param rules: a list of ``(tag, expr)``) tuples with the rules 

109 conforming to UTS #35 or a dict with the tags as keys 

110 and expressions as values. 

111 :raise RuleError: if the expression is malformed 

112 """ 

113 if isinstance(rules, Mapping): 

114 rules = rules.items() 

115 found = set() 

116 self.abstract: list[tuple[str, Any]] = [] 

117 for key, expr in sorted(rules): 

118 if key not in _plural_tags: 

119 raise ValueError(f"unknown tag {key!r}") 

120 elif key in found: 

121 raise ValueError(f"tag {key!r} defined twice") 

122 found.add(key) 

123 ast = _Parser(expr).ast 

124 if ast: 

125 self.abstract.append((key, ast)) 

126 

127 def __repr__(self) -> str: 

128 rules = self.rules 

129 args = ", ".join(f"{tag}: {rules[tag]}" for tag in _plural_tags if tag in rules) 

130 return f"<{type(self).__name__} {args!r}>" 

131 

132 @classmethod 

133 def parse( 

134 cls, 

135 rules: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule, 

136 ) -> PluralRule: 

137 """Create a `PluralRule` instance for the given rules. If the rules 

138 are a `PluralRule` object, that object is returned. 

139 

140 :param rules: the rules as list or dict, or a `PluralRule` object 

141 :raise RuleError: if the expression is malformed 

142 """ 

143 if isinstance(rules, PluralRule): 

144 return rules 

145 return cls(rules) 

146 

147 @property 

148 def rules(self) -> Mapping[str, str]: 

149 """The `PluralRule` as a dict of unicode plural rules. 

150 

151 >>> rule = PluralRule({'one': 'n is 1'}) 

152 >>> rule.rules 

153 {'one': 'n is 1'} 

154 """ 

155 _compile = _UnicodeCompiler().compile 

156 return {tag: _compile(ast) for tag, ast in self.abstract} 

157 

158 @property 

159 def tags(self) -> frozenset[str]: 

160 """A set of explicitly defined tags in this rule. The implicit default 

161 ``'other'`` rules is not part of this set unless there is an explicit 

162 rule for it. 

163 """ 

164 return frozenset(i[0] for i in self.abstract) 

165 

166 def __getstate__(self) -> list[tuple[str, Any]]: 

167 return self.abstract 

168 

169 def __setstate__(self, abstract: list[tuple[str, Any]]) -> None: 

170 self.abstract = abstract 

171 

172 def __call__(self, n: float | decimal.Decimal) -> str: 

173 if not hasattr(self, '_func'): 

174 self._func = to_python(self) 

175 return self._func(n) 

176 

177 

178def to_javascript(rule: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule) -> str: 

179 """Convert a list/dict of rules or a `PluralRule` object into a JavaScript 

180 function. This function depends on no external library: 

181 

182 >>> to_javascript({'one': 'n is 1'}) 

183 "(function(n) { return (n == 1) ? 'one' : 'other'; })" 

184 

185 Implementation detail: The function generated will probably evaluate 

186 expressions involved into range operations multiple times. This has the 

187 advantage that external helper functions are not required and is not a 

188 big performance hit for these simple calculations. 

189 

190 :param rule: the rules as list or dict, or a `PluralRule` object 

191 :raise RuleError: if the expression is malformed 

192 """ 

193 to_js = _JavaScriptCompiler().compile 

194 result = ['(function(n) { return '] 

195 for tag, ast in PluralRule.parse(rule).abstract: 

196 result.append(f"{to_js(ast)} ? {tag!r} : ") 

197 result.append('%r; })' % _fallback_tag) 

198 return ''.join(result) 

199 

200 

201def to_python( 

202 rule: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule, 

203) -> Callable[[float | decimal.Decimal], str]: 

204 """Convert a list/dict of rules or a `PluralRule` object into a regular 

205 Python function. This is useful in situations where you need a real 

206 function and don't are about the actual rule object: 

207 

208 >>> func = to_python({'one': 'n is 1', 'few': 'n in 2..4'}) 

209 >>> func(1) 

210 'one' 

211 >>> func(3) 

212 'few' 

213 >>> func = to_python({'one': 'n in 1,11', 'few': 'n in 3..10,13..19'}) 

214 >>> func(11) 

215 'one' 

216 >>> func(15) 

217 'few' 

218 

219 :param rule: the rules as list or dict, or a `PluralRule` object 

220 :raise RuleError: if the expression is malformed 

221 """ 

222 namespace = { 

223 'IN': in_range_list, 

224 'WITHIN': within_range_list, 

225 'MOD': cldr_modulo, 

226 'extract_operands': extract_operands, 

227 } 

228 to_python_func = _PythonCompiler().compile 

229 result = [ 

230 'def evaluate(n):', 

231 ' n, i, v, w, f, t, c, e = extract_operands(n)', 

232 ] 

233 for tag, ast in PluralRule.parse(rule).abstract: 

234 # the str() call is to coerce the tag to the native string. It's 

235 # a limited ascii restricted set of tags anyways so that is fine. 

236 result.append(f" if ({to_python_func(ast)}): return {str(tag)!r}") 

237 result.append(f" return {_fallback_tag!r}") 

238 code = compile('\n'.join(result), '<rule>', 'exec') 

239 eval(code, namespace) 

240 return namespace['evaluate'] 

241 

242 

243def to_gettext(rule: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule) -> str: 

244 """The plural rule as gettext expression. The gettext expression is 

245 technically limited to integers and returns indices rather than tags. 

246 

247 >>> to_gettext({'one': 'n is 1', 'two': 'n is 2'}) 

248 'nplurals=3; plural=((n == 1) ? 0 : (n == 2) ? 1 : 2);' 

249 

250 :param rule: the rules as list or dict, or a `PluralRule` object 

251 :raise RuleError: if the expression is malformed 

252 """ 

253 rule = PluralRule.parse(rule) 

254 

255 used_tags = rule.tags | {_fallback_tag} 

256 _compile = _GettextCompiler().compile 

257 _get_index = [tag for tag in _plural_tags if tag in used_tags].index 

258 

259 result = [f"nplurals={len(used_tags)}; plural=("] 

260 for tag, ast in rule.abstract: 

261 result.append(f"{_compile(ast)} ? {_get_index(tag)} : ") 

262 result.append(f"{_get_index(_fallback_tag)});") 

263 return ''.join(result) 

264 

265 

266def in_range_list( 

267 num: float | decimal.Decimal, 

268 range_list: Iterable[Iterable[float | decimal.Decimal]], 

269) -> bool: 

270 """Integer range list test. This is the callback for the "in" operator 

271 of the UTS #35 pluralization rule language: 

272 

273 >>> in_range_list(1, [(1, 3)]) 

274 True 

275 >>> in_range_list(3, [(1, 3)]) 

276 True 

277 >>> in_range_list(3, [(1, 3), (5, 8)]) 

278 True 

279 >>> in_range_list(1.2, [(1, 4)]) 

280 False 

281 >>> in_range_list(10, [(1, 4)]) 

282 False 

283 >>> in_range_list(10, [(1, 4), (6, 8)]) 

284 False 

285 """ 

286 return num == int(num) and within_range_list(num, range_list) 

287 

288 

289def within_range_list( 

290 num: float | decimal.Decimal, 

291 range_list: Iterable[Iterable[float | decimal.Decimal]], 

292) -> bool: 

293 """Float range test. This is the callback for the "within" operator 

294 of the UTS #35 pluralization rule language: 

295 

296 >>> within_range_list(1, [(1, 3)]) 

297 True 

298 >>> within_range_list(1.0, [(1, 3)]) 

299 True 

300 >>> within_range_list(1.2, [(1, 4)]) 

301 True 

302 >>> within_range_list(8.8, [(1, 4), (7, 15)]) 

303 True 

304 >>> within_range_list(10, [(1, 4)]) 

305 False 

306 >>> within_range_list(10.5, [(1, 4), (20, 30)]) 

307 False 

308 """ 

309 return any(min_ <= num <= max_ for min_, max_ in range_list) 

310 

311 

312def cldr_modulo(a: float, b: float) -> float: 

313 """Javaish modulo. This modulo operator returns the value with the sign 

314 of the dividend rather than the divisor like Python does: 

315 

316 >>> cldr_modulo(-3, 5) 

317 -3 

318 >>> cldr_modulo(-3, -5) 

319 -3 

320 >>> cldr_modulo(3, 5) 

321 3 

322 """ 

323 reverse = 0 

324 if a < 0: 

325 a *= -1 

326 reverse = 1 

327 if b < 0: 

328 b *= -1 

329 rv = a % b 

330 if reverse: 

331 rv *= -1 

332 return rv 

333 

334 

335class RuleError(Exception): 

336 """Raised if a rule is malformed.""" 

337 

338 

339_VARS = { 

340 'n', # absolute value of the source number. 

341 'i', # integer digits of n. 

342 'v', # number of visible fraction digits in n, with trailing zeros.* 

343 'w', # number of visible fraction digits in n, without trailing zeros.* 

344 'f', # visible fraction digits in n, with trailing zeros.* 

345 't', # visible fraction digits in n, without trailing zeros.* 

346 'c', # compact decimal exponent value: exponent of the power of 10 used in compact decimal formatting. 

347 'e', # currently, synonym for `c`. however, may be redefined in the future. 

348} 

349 

350_RULES: list[tuple[str | None, re.Pattern[str]]] = [ 

351 (None, re.compile(r'\s+', re.UNICODE)), 

352 ('word', re.compile(rf'\b(and|or|is|(?:with)?in|not|mod|[{"".join(_VARS)}])\b')), 

353 ('value', re.compile(r'\d+')), 

354 ('symbol', re.compile(r'%|,|!=|=')), 

355 ('ellipsis', re.compile(r'\.{2,3}|\u2026', re.UNICODE)), # U+2026: ELLIPSIS 

356] 

357 

358 

359def tokenize_rule(s: str) -> list[tuple[str, str]]: 

360 s = s.split('@')[0] 

361 result: list[tuple[str, str]] = [] 

362 pos = 0 

363 end = len(s) 

364 while pos < end: 

365 for tok, rule in _RULES: 

366 match = rule.match(s, pos) 

367 if match is not None: 

368 pos = match.end() 

369 if tok: 

370 result.append((tok, match.group())) 

371 break 

372 else: 

373 raise RuleError(f"malformed CLDR pluralization rule. Got unexpected {s[pos]!r}") 

374 return result[::-1] 

375 

376 

377def test_next_token( 

378 tokens: list[tuple[str, str]], 

379 type_: str, 

380 value: str | None = None, 

381) -> list[tuple[str, str]] | bool: 

382 return tokens and tokens[-1][0] == type_ and (value is None or tokens[-1][1] == value) 

383 

384 

385def skip_token(tokens: list[tuple[str, str]], type_: str, value: str | None = None): 

386 if test_next_token(tokens, type_, value): 

387 return tokens.pop() 

388 

389 

390def value_node(value: int) -> tuple[Literal['value'], tuple[int]]: 

391 return 'value', (value,) 

392 

393 

394def ident_node(name: str) -> tuple[str, tuple[()]]: 

395 return name, () 

396 

397 

398def range_list_node( 

399 range_list: Iterable[Iterable[float | decimal.Decimal]], 

400) -> tuple[Literal['range_list'], Iterable[Iterable[float | decimal.Decimal]]]: 

401 return 'range_list', range_list 

402 

403 

404def negate(rv: tuple[Any, ...]) -> tuple[Literal['not'], tuple[tuple[Any, ...]]]: 

405 return 'not', (rv,) 

406 

407 

408class _Parser: 

409 """Internal parser. This class can translate a single rule into an abstract 

410 tree of tuples. It implements the following grammar:: 

411 

412 condition = and_condition ('or' and_condition)* 

413 ('@integer' samples)? 

414 ('@decimal' samples)? 

415 and_condition = relation ('and' relation)* 

416 relation = is_relation | in_relation | within_relation 

417 is_relation = expr 'is' ('not')? value 

418 in_relation = expr (('not')? 'in' | '=' | '!=') range_list 

419 within_relation = expr ('not')? 'within' range_list 

420 expr = operand (('mod' | '%') value)? 

421 operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w' 

422 range_list = (range | value) (',' range_list)* 

423 value = digit+ 

424 digit = 0|1|2|3|4|5|6|7|8|9 

425 range = value'..'value 

426 samples = sampleRange (',' sampleRange)* (',' ('…'|'...'))? 

427 sampleRange = decimalValue '~' decimalValue 

428 decimalValue = value ('.' value)? 

429 

430 - Whitespace can occur between or around any of the above tokens. 

431 - Rules should be mutually exclusive; for a given numeric value, only one 

432 rule should apply (i.e. the condition should only be true for one of 

433 the plural rule elements). 

434 - The in and within relations can take comma-separated lists, such as: 

435 'n in 3,5,7..15'. 

436 - Samples are ignored. 

437 

438 The translator parses the expression on instantiation into an attribute 

439 called `ast`. 

440 """ 

441 

442 def __init__(self, string): 

443 self.tokens = tokenize_rule(string) 

444 if not self.tokens: 

445 # If the pattern is only samples, it's entirely possible 

446 # no stream of tokens whatsoever is generated. 

447 self.ast = None 

448 return 

449 self.ast = self.condition() 

450 if self.tokens: 

451 raise RuleError(f"Expected end of rule, got {self.tokens[-1][1]!r}") 

452 

453 def expect(self, type_, value=None, term=None): 

454 token = skip_token(self.tokens, type_, value) 

455 if token is not None: 

456 return token 

457 if term is None: 

458 term = repr(value is None and type_ or value) 

459 if not self.tokens: 

460 raise RuleError(f"expected {term} but end of rule reached") 

461 raise RuleError(f"expected {term} but got {self.tokens[-1][1]!r}") 

462 

463 def condition(self): 

464 op = self.and_condition() 

465 while skip_token(self.tokens, 'word', 'or'): 

466 op = 'or', (op, self.and_condition()) 

467 return op 

468 

469 def and_condition(self): 

470 op = self.relation() 

471 while skip_token(self.tokens, 'word', 'and'): 

472 op = 'and', (op, self.relation()) 

473 return op 

474 

475 def relation(self): 

476 left = self.expr() 

477 if skip_token(self.tokens, 'word', 'is'): 

478 op = 'isnot' if skip_token(self.tokens, 'word', 'not') else 'is' 

479 return op, (left, self.value()) 

480 negated = skip_token(self.tokens, 'word', 'not') 

481 method = 'in' 

482 if skip_token(self.tokens, 'word', 'within'): 

483 method = 'within' 

484 else: 

485 if not skip_token(self.tokens, 'word', 'in'): 

486 if negated: 

487 raise RuleError('Cannot negate operator based rules.') 

488 return self.newfangled_relation(left) 

489 rv = 'relation', (method, left, self.range_list()) 

490 return negate(rv) if negated else rv 

491 

492 def newfangled_relation(self, left): 

493 if skip_token(self.tokens, 'symbol', '='): 

494 negated = False 

495 elif skip_token(self.tokens, 'symbol', '!='): 

496 negated = True 

497 else: 

498 raise RuleError('Expected "=" or "!=" or legacy relation') 

499 rv = 'relation', ('in', left, self.range_list()) 

500 return negate(rv) if negated else rv 

501 

502 def range_or_value(self): 

503 left = self.value() 

504 if skip_token(self.tokens, 'ellipsis'): 

505 return left, self.value() 

506 else: 

507 return left, left 

508 

509 def range_list(self): 

510 range_list = [self.range_or_value()] 

511 while skip_token(self.tokens, 'symbol', ','): 

512 range_list.append(self.range_or_value()) 

513 return range_list_node(range_list) 

514 

515 def expr(self): 

516 word = skip_token(self.tokens, 'word') 

517 if word is None or word[1] not in _VARS: 

518 raise RuleError('Expected identifier variable') 

519 name = word[1] 

520 if skip_token(self.tokens, 'word', 'mod'): 

521 return 'mod', ((name, ()), self.value()) 

522 elif skip_token(self.tokens, 'symbol', '%'): 

523 return 'mod', ((name, ()), self.value()) 

524 return ident_node(name) 

525 

526 def value(self): 

527 return value_node(int(self.expect('value')[1])) 

528 

529 

530def _binary_compiler(tmpl): 

531 """Compiler factory for the `_Compiler`.""" 

532 return lambda self, left, right: tmpl % (self.compile(left), self.compile(right)) 

533 

534 

535def _unary_compiler(tmpl): 

536 """Compiler factory for the `_Compiler`.""" 

537 return lambda self, x: tmpl % self.compile(x) 

538 

539 

540compile_zero = lambda x: '0' 

541 

542 

543class _Compiler: 

544 """The compilers are able to transform the expressions into multiple 

545 output formats. 

546 """ 

547 

548 def compile(self, arg): 

549 op, args = arg 

550 return getattr(self, f"compile_{op}")(*args) 

551 

552 compile_n = lambda x: 'n' 

553 compile_i = lambda x: 'i' 

554 compile_v = lambda x: 'v' 

555 compile_w = lambda x: 'w' 

556 compile_f = lambda x: 'f' 

557 compile_t = lambda x: 't' 

558 compile_c = lambda x: 'c' 

559 compile_e = lambda x: 'e' 

560 compile_value = lambda x, v: str(v) 

561 compile_and = _binary_compiler('(%s && %s)') 

562 compile_or = _binary_compiler('(%s || %s)') 

563 compile_not = _unary_compiler('(!%s)') 

564 compile_mod = _binary_compiler('(%s %% %s)') 

565 compile_is = _binary_compiler('(%s == %s)') 

566 compile_isnot = _binary_compiler('(%s != %s)') 

567 

568 def compile_relation(self, method, expr, range_list): 

569 raise NotImplementedError() 

570 

571 

572class _PythonCompiler(_Compiler): 

573 """Compiles an expression to Python.""" 

574 

575 compile_and = _binary_compiler('(%s and %s)') 

576 compile_or = _binary_compiler('(%s or %s)') 

577 compile_not = _unary_compiler('(not %s)') 

578 compile_mod = _binary_compiler('MOD(%s, %s)') 

579 

580 def compile_relation(self, method, expr, range_list): 

581 ranges = ",".join( 

582 f"({self.compile(a)}, {self.compile(b)})" for (a, b) in range_list[1] 

583 ) 

584 return f"{method.upper()}({self.compile(expr)}, [{ranges}])" 

585 

586 

587class _GettextCompiler(_Compiler): 

588 """Compile into a gettext plural expression.""" 

589 

590 compile_i = _Compiler.compile_n 

591 compile_v = compile_zero 

592 compile_w = compile_zero 

593 compile_f = compile_zero 

594 compile_t = compile_zero 

595 

596 def compile_relation(self, method, expr, range_list): 

597 rv = [] 

598 expr = self.compile(expr) 

599 for item in range_list[1]: 

600 if item[0] == item[1]: 

601 rv.append(f"({expr} == {self.compile(item[0])})") 

602 else: 

603 min = self.compile(item[0]) 

604 max = self.compile(item[1]) 

605 rv.append(f"({expr} >= {min} && {expr} <= {max})") 

606 return f"({' || '.join(rv)})" 

607 

608 

609class _JavaScriptCompiler(_GettextCompiler): 

610 """Compiles the expression to plain of JavaScript.""" 

611 

612 # XXX: presently javascript does not support any of the 

613 # fraction support and basically only deals with integers. 

614 compile_i = lambda x: 'parseInt(n, 10)' 

615 compile_v = compile_zero 

616 compile_w = compile_zero 

617 compile_f = compile_zero 

618 compile_t = compile_zero 

619 

620 def compile_relation(self, method, expr, range_list): 

621 code = _GettextCompiler.compile_relation(self, method, expr, range_list) 

622 if method == 'in': 

623 expr = self.compile(expr) 

624 code = f"(parseInt({expr}, 10) == {expr} && {code})" 

625 return code 

626 

627 

628class _UnicodeCompiler(_Compiler): 

629 """Returns a unicode pluralization rule again.""" 

630 

631 # XXX: this currently spits out the old syntax instead of the new 

632 # one. We can change that, but it will break a whole bunch of stuff 

633 # for users I suppose. 

634 

635 compile_is = _binary_compiler('%s is %s') 

636 compile_isnot = _binary_compiler('%s is not %s') 

637 compile_and = _binary_compiler('%s and %s') 

638 compile_or = _binary_compiler('%s or %s') 

639 compile_mod = _binary_compiler('%s mod %s') 

640 

641 def compile_not(self, relation): 

642 return self.compile_relation(*relation[1], negated=True) 

643 

644 def compile_relation(self, method, expr, range_list, negated=False): 

645 ranges = [] 

646 for item in range_list[1]: 

647 if item[0] == item[1]: 

648 ranges.append(self.compile(item[0])) 

649 else: 

650 ranges.append(f"{self.compile(item[0])}..{self.compile(item[1])}") 

651 return f"{self.compile(expr)}{' not' if negated else ''} {method} {','.join(ranges)}"