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) { n = Math.abs(n); return (n == 1) ? 'one' : 'other'; })" 

184 

185 Negative numbers use their absolute value, as in :class:`PluralRule`. 

186 

187 Implementation detail: The function generated will probably evaluate 

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

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

190 big performance hit for these simple calculations. 

191 

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

193 :raise RuleError: if the expression is malformed 

194 """ 

195 to_js = _JavaScriptCompiler().compile 

196 result = ['(function(n) { n = Math.abs(n); return '] 

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

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

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

200 return ''.join(result) 

201 

202 

203def to_python( 

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

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

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

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

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

209 

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

211 >>> func(1) 

212 'one' 

213 >>> func(3) 

214 'few' 

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

216 >>> func(11) 

217 'one' 

218 >>> func(15) 

219 'few' 

220 

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

222 :raise RuleError: if the expression is malformed 

223 """ 

224 namespace = { 

225 'IN': in_range_list, 

226 'WITHIN': within_range_list, 

227 'MOD': cldr_modulo, 

228 'extract_operands': extract_operands, 

229 } 

230 to_python_func = _PythonCompiler().compile 

231 result = [ 

232 'def evaluate(n):', 

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

234 ] 

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

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

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

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

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

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

241 eval(code, namespace) 

242 return namespace['evaluate'] 

243 

244 

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

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

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

248 

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

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

251 

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

253 :raise RuleError: if the expression is malformed 

254 """ 

255 rule = PluralRule.parse(rule) 

256 

257 used_tags = rule.tags | {_fallback_tag} 

258 _compile = _GettextCompiler().compile 

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

260 

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

262 for tag, ast in rule.abstract: 

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

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

265 return ''.join(result) 

266 

267 

268def in_range_list( 

269 num: float | decimal.Decimal, 

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

271) -> bool: 

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

273 of the UTS #35 pluralization rule language: 

274 

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

276 True 

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

278 True 

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

280 True 

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

282 False 

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

284 False 

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

286 False 

287 """ 

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

289 

290 

291def within_range_list( 

292 num: float | decimal.Decimal, 

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

294) -> bool: 

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

296 of the UTS #35 pluralization rule language: 

297 

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

299 True 

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

301 True 

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

303 True 

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

305 True 

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

307 False 

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

309 False 

310 """ 

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

312 

313 

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

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

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

317 

318 >>> cldr_modulo(-3, 5) 

319 -3 

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

321 -3 

322 >>> cldr_modulo(3, 5) 

323 3 

324 """ 

325 reverse = 0 

326 if a < 0: 

327 a *= -1 

328 reverse = 1 

329 if b < 0: 

330 b *= -1 

331 rv = a % b 

332 if reverse: 

333 rv *= -1 

334 return rv 

335 

336 

337class RuleError(Exception): 

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

339 

340 

341_VARS = { 

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

343 'i', # integer digits of n. 

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

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

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

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

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

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

350} 

351 

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

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

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

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

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

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

358] 

359 

360 

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

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

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

364 pos = 0 

365 end = len(s) 

366 while pos < end: 

367 for tok, rule in _RULES: 

368 match = rule.match(s, pos) 

369 if match is not None: 

370 pos = match.end() 

371 if tok: 

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

373 break 

374 else: 

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

376 return result[::-1] 

377 

378 

379def test_next_token( 

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

381 type_: str, 

382 value: str | None = None, 

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

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

385 

386 

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

388 if test_next_token(tokens, type_, value): 

389 return tokens.pop() 

390 

391 

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

393 return 'value', (value,) 

394 

395 

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

397 return name, () 

398 

399 

400def range_list_node( 

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

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

403 return 'range_list', range_list 

404 

405 

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

407 return 'not', (rv,) 

408 

409 

410class _Parser: 

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

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

413 

414 condition = and_condition ('or' and_condition)* 

415 ('@integer' samples)? 

416 ('@decimal' samples)? 

417 and_condition = relation ('and' relation)* 

418 relation = is_relation | in_relation | within_relation 

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

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

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

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

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

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

425 value = digit+ 

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

427 range = value'..'value 

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

429 sampleRange = decimalValue '~' decimalValue 

430 decimalValue = value ('.' value)? 

431 

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

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

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

435 the plural rule elements). 

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

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

438 - Samples are ignored. 

439 

440 The translator parses the expression on instantiation into an attribute 

441 called `ast`. 

442 """ 

443 

444 def __init__(self, string): 

445 self.tokens = tokenize_rule(string) 

446 if not self.tokens: 

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

448 # no stream of tokens whatsoever is generated. 

449 self.ast = None 

450 return 

451 self.ast = self.condition() 

452 if self.tokens: 

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

454 

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

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

457 if token is not None: 

458 return token 

459 if term is None: 

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

461 if not self.tokens: 

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

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

464 

465 def condition(self): 

466 op = self.and_condition() 

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

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

469 return op 

470 

471 def and_condition(self): 

472 op = self.relation() 

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

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

475 return op 

476 

477 def relation(self): 

478 left = self.expr() 

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

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

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

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

483 method = 'in' 

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

485 method = 'within' 

486 else: 

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

488 if negated: 

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

490 return self.newfangled_relation(left) 

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

492 return negate(rv) if negated else rv 

493 

494 def newfangled_relation(self, left): 

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

496 negated = False 

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

498 negated = True 

499 else: 

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

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

502 return negate(rv) if negated else rv 

503 

504 def range_or_value(self): 

505 left = self.value() 

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

507 return left, self.value() 

508 else: 

509 return left, left 

510 

511 def range_list(self): 

512 range_list = [self.range_or_value()] 

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

514 range_list.append(self.range_or_value()) 

515 return range_list_node(range_list) 

516 

517 def expr(self): 

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

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

520 raise RuleError('Expected identifier variable') 

521 name = word[1] 

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

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

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

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

526 return ident_node(name) 

527 

528 def value(self): 

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

530 

531 

532def _binary_compiler(tmpl): 

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

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

535 

536 

537def _unary_compiler(tmpl): 

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

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

540 

541 

542compile_zero = lambda x: '0' 

543 

544 

545class _Compiler: 

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

547 output formats. 

548 """ 

549 

550 def compile(self, arg): 

551 op, args = arg 

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

553 

554 compile_n = lambda x: 'n' 

555 compile_i = lambda x: 'i' 

556 compile_v = lambda x: 'v' 

557 compile_w = lambda x: 'w' 

558 compile_f = lambda x: 'f' 

559 compile_t = lambda x: 't' 

560 compile_c = lambda x: 'c' 

561 compile_e = lambda x: 'e' 

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

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

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

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

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

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

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

569 

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

571 raise NotImplementedError() 

572 

573 

574class _PythonCompiler(_Compiler): 

575 """Compiles an expression to Python.""" 

576 

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

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

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

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

581 

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

583 ranges = ",".join( 

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

585 ) 

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

587 

588 

589class _GettextCompiler(_Compiler): 

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

591 

592 compile_i = _Compiler.compile_n 

593 compile_v = compile_zero 

594 compile_w = compile_zero 

595 compile_f = compile_zero 

596 compile_t = compile_zero 

597 

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

599 rv = [] 

600 expr = self.compile(expr) 

601 for item in range_list[1]: 

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

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

604 else: 

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

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

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

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

609 

610 

611class _JavaScriptCompiler(_GettextCompiler): 

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

613 

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

615 # fraction support and basically only deals with integers. 

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

617 compile_v = compile_zero 

618 compile_w = compile_zero 

619 compile_f = compile_zero 

620 compile_t = compile_zero 

621 

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

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

624 if method == 'in': 

625 expr = self.compile(expr) 

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

627 return code 

628 

629 

630class _UnicodeCompiler(_Compiler): 

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

632 

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

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

635 # for users I suppose. 

636 

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

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

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

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

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

642 

643 def compile_not(self, relation): 

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

645 

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

647 ranges = [] 

648 for item in range_list[1]: 

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

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

651 else: 

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

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