Spaces:
Runtime error
Runtime error
| """ | |
| babel.numbers | |
| ~~~~~~~~~~~~~ | |
| CLDR Plural support. See UTS #35. | |
| :copyright: (c) 2013-2023 by the Babel Team. | |
| :license: BSD, see LICENSE for more details. | |
| """ | |
| from __future__ import annotations | |
| import decimal | |
| import re | |
| from collections.abc import Iterable, Mapping | |
| from typing import TYPE_CHECKING, Any, Callable | |
| if TYPE_CHECKING: | |
| from typing_extensions import Literal | |
| _plural_tags = ('zero', 'one', 'two', 'few', 'many', 'other') | |
| _fallback_tag = 'other' | |
| def extract_operands(source: float | decimal.Decimal) -> tuple[decimal.Decimal | int, int, int, int, int, int, Literal[0], Literal[0]]: | |
| """Extract operands from a decimal, a float or an int, according to `CLDR rules`_. | |
| The result is an 8-tuple (n, i, v, w, f, t, c, e), where those symbols are as follows: | |
| ====== =============================================================== | |
| Symbol Value | |
| ------ --------------------------------------------------------------- | |
| n absolute value of the source number (integer and decimals). | |
| i integer digits of n. | |
| v number of visible fraction digits in n, with trailing zeros. | |
| w number of visible fraction digits in n, without trailing zeros. | |
| f visible fractional digits in n, with trailing zeros. | |
| t visible fractional digits in n, without trailing zeros. | |
| c compact decimal exponent value: exponent of the power of 10 used in compact decimal formatting. | |
| e currently, synonym for ‘c’. however, may be redefined in the future. | |
| ====== =============================================================== | |
| .. _`CLDR rules`: https://www.unicode.org/reports/tr35/tr35-61/tr35-numbers.html#Operands | |
| :param source: A real number | |
| :type source: int|float|decimal.Decimal | |
| :return: A n-i-v-w-f-t-c-e tuple | |
| :rtype: tuple[decimal.Decimal, int, int, int, int, int, int, int] | |
| """ | |
| n = abs(source) | |
| i = int(n) | |
| if isinstance(n, float): | |
| if i == n: | |
| n = i | |
| else: | |
| # Cast the `float` to a number via the string representation. | |
| # This is required for Python 2.6 anyway (it will straight out fail to | |
| # do the conversion otherwise), and it's highly unlikely that the user | |
| # actually wants the lossless conversion behavior (quoting the Python | |
| # documentation): | |
| # > If value is a float, the binary floating point value is losslessly | |
| # > converted to its exact decimal equivalent. | |
| # > This conversion can often require 53 or more digits of precision. | |
| # Should the user want that behavior, they can simply pass in a pre- | |
| # converted `Decimal` instance of desired accuracy. | |
| n = decimal.Decimal(str(n)) | |
| if isinstance(n, decimal.Decimal): | |
| dec_tuple = n.as_tuple() | |
| exp = dec_tuple.exponent | |
| fraction_digits = dec_tuple.digits[exp:] if exp < 0 else () | |
| trailing = ''.join(str(d) for d in fraction_digits) | |
| no_trailing = trailing.rstrip('0') | |
| v = len(trailing) | |
| w = len(no_trailing) | |
| f = int(trailing or 0) | |
| t = int(no_trailing or 0) | |
| else: | |
| v = w = f = t = 0 | |
| c = e = 0 # TODO: c and e are not supported | |
| return n, i, v, w, f, t, c, e | |
| class PluralRule: | |
| """Represents a set of language pluralization rules. The constructor | |
| accepts a list of (tag, expr) tuples or a dict of `CLDR rules`_. The | |
| resulting object is callable and accepts one parameter with a positive or | |
| negative number (both integer and float) for the number that indicates the | |
| plural form for a string and returns the tag for the format: | |
| >>> rule = PluralRule({'one': 'n is 1'}) | |
| >>> rule(1) | |
| 'one' | |
| >>> rule(2) | |
| 'other' | |
| Currently the CLDR defines these tags: zero, one, two, few, many and | |
| other where other is an implicit default. Rules should be mutually | |
| exclusive; for a given numeric value, only one rule should apply (i.e. | |
| the condition should only be true for one of the plural rule elements. | |
| .. _`CLDR rules`: https://www.unicode.org/reports/tr35/tr35-33/tr35-numbers.html#Language_Plural_Rules | |
| """ | |
| __slots__ = ('abstract', '_func') | |
| def __init__(self, rules: Mapping[str, str] | Iterable[tuple[str, str]]) -> None: | |
| """Initialize the rule instance. | |
| :param rules: a list of ``(tag, expr)``) tuples with the rules | |
| conforming to UTS #35 or a dict with the tags as keys | |
| and expressions as values. | |
| :raise RuleError: if the expression is malformed | |
| """ | |
| if isinstance(rules, Mapping): | |
| rules = rules.items() | |
| found = set() | |
| self.abstract: list[tuple[str, Any]] = [] | |
| for key, expr in sorted(rules): | |
| if key not in _plural_tags: | |
| raise ValueError(f"unknown tag {key!r}") | |
| elif key in found: | |
| raise ValueError(f"tag {key!r} defined twice") | |
| found.add(key) | |
| ast = _Parser(expr).ast | |
| if ast: | |
| self.abstract.append((key, ast)) | |
| def __repr__(self) -> str: | |
| rules = self.rules | |
| args = ", ".join([f"{tag}: {rules[tag]}" for tag in _plural_tags if tag in rules]) | |
| return f"<{type(self).__name__} {args!r}>" | |
| def parse(cls, rules: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule) -> PluralRule: | |
| """Create a `PluralRule` instance for the given rules. If the rules | |
| are a `PluralRule` object, that object is returned. | |
| :param rules: the rules as list or dict, or a `PluralRule` object | |
| :raise RuleError: if the expression is malformed | |
| """ | |
| if isinstance(rules, PluralRule): | |
| return rules | |
| return cls(rules) | |
| def rules(self) -> Mapping[str, str]: | |
| """The `PluralRule` as a dict of unicode plural rules. | |
| >>> rule = PluralRule({'one': 'n is 1'}) | |
| >>> rule.rules | |
| {'one': 'n is 1'} | |
| """ | |
| _compile = _UnicodeCompiler().compile | |
| return {tag: _compile(ast) for tag, ast in self.abstract} | |
| def tags(self) -> frozenset[str]: | |
| """A set of explicitly defined tags in this rule. The implicit default | |
| ``'other'`` rules is not part of this set unless there is an explicit | |
| rule for it. | |
| """ | |
| return frozenset(i[0] for i in self.abstract) | |
| def __getstate__(self) -> list[tuple[str, Any]]: | |
| return self.abstract | |
| def __setstate__(self, abstract: list[tuple[str, Any]]) -> None: | |
| self.abstract = abstract | |
| def __call__(self, n: float | decimal.Decimal) -> str: | |
| if not hasattr(self, '_func'): | |
| self._func = to_python(self) | |
| return self._func(n) | |
| def to_javascript(rule: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule) -> str: | |
| """Convert a list/dict of rules or a `PluralRule` object into a JavaScript | |
| function. This function depends on no external library: | |
| >>> to_javascript({'one': 'n is 1'}) | |
| "(function(n) { return (n == 1) ? 'one' : 'other'; })" | |
| Implementation detail: The function generated will probably evaluate | |
| expressions involved into range operations multiple times. This has the | |
| advantage that external helper functions are not required and is not a | |
| big performance hit for these simple calculations. | |
| :param rule: the rules as list or dict, or a `PluralRule` object | |
| :raise RuleError: if the expression is malformed | |
| """ | |
| to_js = _JavaScriptCompiler().compile | |
| result = ['(function(n) { return '] | |
| for tag, ast in PluralRule.parse(rule).abstract: | |
| result.append(f"{to_js(ast)} ? {tag!r} : ") | |
| result.append('%r; })' % _fallback_tag) | |
| return ''.join(result) | |
| def to_python(rule: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule) -> Callable[[float | decimal.Decimal], str]: | |
| """Convert a list/dict of rules or a `PluralRule` object into a regular | |
| Python function. This is useful in situations where you need a real | |
| function and don't are about the actual rule object: | |
| >>> func = to_python({'one': 'n is 1', 'few': 'n in 2..4'}) | |
| >>> func(1) | |
| 'one' | |
| >>> func(3) | |
| 'few' | |
| >>> func = to_python({'one': 'n in 1,11', 'few': 'n in 3..10,13..19'}) | |
| >>> func(11) | |
| 'one' | |
| >>> func(15) | |
| 'few' | |
| :param rule: the rules as list or dict, or a `PluralRule` object | |
| :raise RuleError: if the expression is malformed | |
| """ | |
| namespace = { | |
| 'IN': in_range_list, | |
| 'WITHIN': within_range_list, | |
| 'MOD': cldr_modulo, | |
| 'extract_operands': extract_operands, | |
| } | |
| to_python_func = _PythonCompiler().compile | |
| result = [ | |
| 'def evaluate(n):', | |
| ' n, i, v, w, f, t, c, e = extract_operands(n)', | |
| ] | |
| for tag, ast in PluralRule.parse(rule).abstract: | |
| # the str() call is to coerce the tag to the native string. It's | |
| # a limited ascii restricted set of tags anyways so that is fine. | |
| result.append(f" if ({to_python_func(ast)}): return {str(tag)!r}") | |
| result.append(f" return {_fallback_tag!r}") | |
| code = compile('\n'.join(result), '<rule>', 'exec') | |
| eval(code, namespace) | |
| return namespace['evaluate'] | |
| def to_gettext(rule: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule) -> str: | |
| """The plural rule as gettext expression. The gettext expression is | |
| technically limited to integers and returns indices rather than tags. | |
| >>> to_gettext({'one': 'n is 1', 'two': 'n is 2'}) | |
| 'nplurals=3; plural=((n == 1) ? 0 : (n == 2) ? 1 : 2);' | |
| :param rule: the rules as list or dict, or a `PluralRule` object | |
| :raise RuleError: if the expression is malformed | |
| """ | |
| rule = PluralRule.parse(rule) | |
| used_tags = rule.tags | {_fallback_tag} | |
| _compile = _GettextCompiler().compile | |
| _get_index = [tag for tag in _plural_tags if tag in used_tags].index | |
| result = [f"nplurals={len(used_tags)}; plural=("] | |
| for tag, ast in rule.abstract: | |
| result.append(f"{_compile(ast)} ? {_get_index(tag)} : ") | |
| result.append(f"{_get_index(_fallback_tag)});") | |
| return ''.join(result) | |
| def in_range_list(num: float | decimal.Decimal, range_list: Iterable[Iterable[float | decimal.Decimal]]) -> bool: | |
| """Integer range list test. This is the callback for the "in" operator | |
| of the UTS #35 pluralization rule language: | |
| >>> in_range_list(1, [(1, 3)]) | |
| True | |
| >>> in_range_list(3, [(1, 3)]) | |
| True | |
| >>> in_range_list(3, [(1, 3), (5, 8)]) | |
| True | |
| >>> in_range_list(1.2, [(1, 4)]) | |
| False | |
| >>> in_range_list(10, [(1, 4)]) | |
| False | |
| >>> in_range_list(10, [(1, 4), (6, 8)]) | |
| False | |
| """ | |
| return num == int(num) and within_range_list(num, range_list) | |
| def within_range_list(num: float | decimal.Decimal, range_list: Iterable[Iterable[float | decimal.Decimal]]) -> bool: | |
| """Float range test. This is the callback for the "within" operator | |
| of the UTS #35 pluralization rule language: | |
| >>> within_range_list(1, [(1, 3)]) | |
| True | |
| >>> within_range_list(1.0, [(1, 3)]) | |
| True | |
| >>> within_range_list(1.2, [(1, 4)]) | |
| True | |
| >>> within_range_list(8.8, [(1, 4), (7, 15)]) | |
| True | |
| >>> within_range_list(10, [(1, 4)]) | |
| False | |
| >>> within_range_list(10.5, [(1, 4), (20, 30)]) | |
| False | |
| """ | |
| return any(num >= min_ and num <= max_ for min_, max_ in range_list) | |
| def cldr_modulo(a: float, b: float) -> float: | |
| """Javaish modulo. This modulo operator returns the value with the sign | |
| of the dividend rather than the divisor like Python does: | |
| >>> cldr_modulo(-3, 5) | |
| -3 | |
| >>> cldr_modulo(-3, -5) | |
| -3 | |
| >>> cldr_modulo(3, 5) | |
| 3 | |
| """ | |
| reverse = 0 | |
| if a < 0: | |
| a *= -1 | |
| reverse = 1 | |
| if b < 0: | |
| b *= -1 | |
| rv = a % b | |
| if reverse: | |
| rv *= -1 | |
| return rv | |
| class RuleError(Exception): | |
| """Raised if a rule is malformed.""" | |
| _VARS = { | |
| 'n', # absolute value of the source number. | |
| 'i', # integer digits of n. | |
| 'v', # number of visible fraction digits in n, with trailing zeros.* | |
| 'w', # number of visible fraction digits in n, without trailing zeros.* | |
| 'f', # visible fraction digits in n, with trailing zeros.* | |
| 't', # visible fraction digits in n, without trailing zeros.* | |
| 'c', # compact decimal exponent value: exponent of the power of 10 used in compact decimal formatting. | |
| 'e', # currently, synonym for `c`. however, may be redefined in the future. | |
| } | |
| _RULES: list[tuple[str | None, re.Pattern[str]]] = [ | |
| (None, re.compile(r'\s+', re.UNICODE)), | |
| ('word', re.compile(fr'\b(and|or|is|(?:with)?in|not|mod|[{"".join(_VARS)}])\b')), | |
| ('value', re.compile(r'\d+')), | |
| ('symbol', re.compile(r'%|,|!=|=')), | |
| ('ellipsis', re.compile(r'\.{2,3}|\u2026', re.UNICODE)) # U+2026: ELLIPSIS | |
| ] | |
| def tokenize_rule(s: str) -> list[tuple[str, str]]: | |
| s = s.split('@')[0] | |
| result: list[tuple[str, str]] = [] | |
| pos = 0 | |
| end = len(s) | |
| while pos < end: | |
| for tok, rule in _RULES: | |
| match = rule.match(s, pos) | |
| if match is not None: | |
| pos = match.end() | |
| if tok: | |
| result.append((tok, match.group())) | |
| break | |
| else: | |
| raise RuleError('malformed CLDR pluralization rule. ' | |
| 'Got unexpected %r' % s[pos]) | |
| return result[::-1] | |
| def test_next_token( | |
| tokens: list[tuple[str, str]], | |
| type_: str, | |
| value: str | None = None, | |
| ) -> list[tuple[str, str]] | bool: | |
| return tokens and tokens[-1][0] == type_ and \ | |
| (value is None or tokens[-1][1] == value) | |
| def skip_token(tokens: list[tuple[str, str]], type_: str, value: str | None = None): | |
| if test_next_token(tokens, type_, value): | |
| return tokens.pop() | |
| def value_node(value: int) -> tuple[Literal['value'], tuple[int]]: | |
| return 'value', (value, ) | |
| def ident_node(name: str) -> tuple[str, tuple[()]]: | |
| return name, () | |
| def range_list_node( | |
| range_list: Iterable[Iterable[float | decimal.Decimal]], | |
| ) -> tuple[Literal['range_list'], Iterable[Iterable[float | decimal.Decimal]]]: | |
| return 'range_list', range_list | |
| def negate(rv: tuple[Any, ...]) -> tuple[Literal['not'], tuple[tuple[Any, ...]]]: | |
| return 'not', (rv,) | |
| class _Parser: | |
| """Internal parser. This class can translate a single rule into an abstract | |
| tree of tuples. It implements the following grammar:: | |
| condition = and_condition ('or' and_condition)* | |
| ('@integer' samples)? | |
| ('@decimal' samples)? | |
| and_condition = relation ('and' relation)* | |
| relation = is_relation | in_relation | within_relation | |
| is_relation = expr 'is' ('not')? value | |
| in_relation = expr (('not')? 'in' | '=' | '!=') range_list | |
| within_relation = expr ('not')? 'within' range_list | |
| expr = operand (('mod' | '%') value)? | |
| operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w' | |
| range_list = (range | value) (',' range_list)* | |
| value = digit+ | |
| digit = 0|1|2|3|4|5|6|7|8|9 | |
| range = value'..'value | |
| samples = sampleRange (',' sampleRange)* (',' ('…'|'...'))? | |
| sampleRange = decimalValue '~' decimalValue | |
| decimalValue = value ('.' value)? | |
| - Whitespace can occur between or around any of the above tokens. | |
| - Rules should be mutually exclusive; for a given numeric value, only one | |
| rule should apply (i.e. the condition should only be true for one of | |
| the plural rule elements). | |
| - The in and within relations can take comma-separated lists, such as: | |
| 'n in 3,5,7..15'. | |
| - Samples are ignored. | |
| The translator parses the expression on instantiation into an attribute | |
| called `ast`. | |
| """ | |
| def __init__(self, string): | |
| self.tokens = tokenize_rule(string) | |
| if not self.tokens: | |
| # If the pattern is only samples, it's entirely possible | |
| # no stream of tokens whatsoever is generated. | |
| self.ast = None | |
| return | |
| self.ast = self.condition() | |
| if self.tokens: | |
| raise RuleError(f"Expected end of rule, got {self.tokens[-1][1]!r}") | |
| def expect(self, type_, value=None, term=None): | |
| token = skip_token(self.tokens, type_, value) | |
| if token is not None: | |
| return token | |
| if term is None: | |
| term = repr(value is None and type_ or value) | |
| if not self.tokens: | |
| raise RuleError(f"expected {term} but end of rule reached") | |
| raise RuleError(f"expected {term} but got {self.tokens[-1][1]!r}") | |
| def condition(self): | |
| op = self.and_condition() | |
| while skip_token(self.tokens, 'word', 'or'): | |
| op = 'or', (op, self.and_condition()) | |
| return op | |
| def and_condition(self): | |
| op = self.relation() | |
| while skip_token(self.tokens, 'word', 'and'): | |
| op = 'and', (op, self.relation()) | |
| return op | |
| def relation(self): | |
| left = self.expr() | |
| if skip_token(self.tokens, 'word', 'is'): | |
| return skip_token(self.tokens, 'word', 'not') and 'isnot' or 'is', \ | |
| (left, self.value()) | |
| negated = skip_token(self.tokens, 'word', 'not') | |
| method = 'in' | |
| if skip_token(self.tokens, 'word', 'within'): | |
| method = 'within' | |
| else: | |
| if not skip_token(self.tokens, 'word', 'in'): | |
| if negated: | |
| raise RuleError('Cannot negate operator based rules.') | |
| return self.newfangled_relation(left) | |
| rv = 'relation', (method, left, self.range_list()) | |
| return negate(rv) if negated else rv | |
| def newfangled_relation(self, left): | |
| if skip_token(self.tokens, 'symbol', '='): | |
| negated = False | |
| elif skip_token(self.tokens, 'symbol', '!='): | |
| negated = True | |
| else: | |
| raise RuleError('Expected "=" or "!=" or legacy relation') | |
| rv = 'relation', ('in', left, self.range_list()) | |
| return negate(rv) if negated else rv | |
| def range_or_value(self): | |
| left = self.value() | |
| if skip_token(self.tokens, 'ellipsis'): | |
| return left, self.value() | |
| else: | |
| return left, left | |
| def range_list(self): | |
| range_list = [self.range_or_value()] | |
| while skip_token(self.tokens, 'symbol', ','): | |
| range_list.append(self.range_or_value()) | |
| return range_list_node(range_list) | |
| def expr(self): | |
| word = skip_token(self.tokens, 'word') | |
| if word is None or word[1] not in _VARS: | |
| raise RuleError('Expected identifier variable') | |
| name = word[1] | |
| if skip_token(self.tokens, 'word', 'mod'): | |
| return 'mod', ((name, ()), self.value()) | |
| elif skip_token(self.tokens, 'symbol', '%'): | |
| return 'mod', ((name, ()), self.value()) | |
| return ident_node(name) | |
| def value(self): | |
| return value_node(int(self.expect('value')[1])) | |
| def _binary_compiler(tmpl): | |
| """Compiler factory for the `_Compiler`.""" | |
| return lambda self, left, right: tmpl % (self.compile(left), self.compile(right)) | |
| def _unary_compiler(tmpl): | |
| """Compiler factory for the `_Compiler`.""" | |
| return lambda self, x: tmpl % self.compile(x) | |
| compile_zero = lambda x: '0' | |
| class _Compiler: | |
| """The compilers are able to transform the expressions into multiple | |
| output formats. | |
| """ | |
| def compile(self, arg): | |
| op, args = arg | |
| return getattr(self, f"compile_{op}")(*args) | |
| compile_n = lambda x: 'n' | |
| compile_i = lambda x: 'i' | |
| compile_v = lambda x: 'v' | |
| compile_w = lambda x: 'w' | |
| compile_f = lambda x: 'f' | |
| compile_t = lambda x: 't' | |
| compile_c = lambda x: 'c' | |
| compile_e = lambda x: 'e' | |
| compile_value = lambda x, v: str(v) | |
| compile_and = _binary_compiler('(%s && %s)') | |
| compile_or = _binary_compiler('(%s || %s)') | |
| compile_not = _unary_compiler('(!%s)') | |
| compile_mod = _binary_compiler('(%s %% %s)') | |
| compile_is = _binary_compiler('(%s == %s)') | |
| compile_isnot = _binary_compiler('(%s != %s)') | |
| def compile_relation(self, method, expr, range_list): | |
| raise NotImplementedError() | |
| class _PythonCompiler(_Compiler): | |
| """Compiles an expression to Python.""" | |
| compile_and = _binary_compiler('(%s and %s)') | |
| compile_or = _binary_compiler('(%s or %s)') | |
| compile_not = _unary_compiler('(not %s)') | |
| compile_mod = _binary_compiler('MOD(%s, %s)') | |
| def compile_relation(self, method, expr, range_list): | |
| ranges = ",".join([f"({self.compile(a)}, {self.compile(b)})" for (a, b) in range_list[1]]) | |
| return f"{method.upper()}({self.compile(expr)}, [{ranges}])" | |
| class _GettextCompiler(_Compiler): | |
| """Compile into a gettext plural expression.""" | |
| compile_i = _Compiler.compile_n | |
| compile_v = compile_zero | |
| compile_w = compile_zero | |
| compile_f = compile_zero | |
| compile_t = compile_zero | |
| def compile_relation(self, method, expr, range_list): | |
| rv = [] | |
| expr = self.compile(expr) | |
| for item in range_list[1]: | |
| if item[0] == item[1]: | |
| rv.append(f"({expr} == {self.compile(item[0])})") | |
| else: | |
| min, max = map(self.compile, item) | |
| rv.append(f"({expr} >= {min} && {expr} <= {max})") | |
| return f"({' || '.join(rv)})" | |
| class _JavaScriptCompiler(_GettextCompiler): | |
| """Compiles the expression to plain of JavaScript.""" | |
| # XXX: presently javascript does not support any of the | |
| # fraction support and basically only deals with integers. | |
| compile_i = lambda x: 'parseInt(n, 10)' | |
| compile_v = compile_zero | |
| compile_w = compile_zero | |
| compile_f = compile_zero | |
| compile_t = compile_zero | |
| def compile_relation(self, method, expr, range_list): | |
| code = _GettextCompiler.compile_relation( | |
| self, method, expr, range_list) | |
| if method == 'in': | |
| expr = self.compile(expr) | |
| code = f"(parseInt({expr}, 10) == {expr} && {code})" | |
| return code | |
| class _UnicodeCompiler(_Compiler): | |
| """Returns a unicode pluralization rule again.""" | |
| # XXX: this currently spits out the old syntax instead of the new | |
| # one. We can change that, but it will break a whole bunch of stuff | |
| # for users I suppose. | |
| compile_is = _binary_compiler('%s is %s') | |
| compile_isnot = _binary_compiler('%s is not %s') | |
| compile_and = _binary_compiler('%s and %s') | |
| compile_or = _binary_compiler('%s or %s') | |
| compile_mod = _binary_compiler('%s mod %s') | |
| def compile_not(self, relation): | |
| return self.compile_relation(*relation[1], negated=True) | |
| def compile_relation(self, method, expr, range_list, negated=False): | |
| ranges = [] | |
| for item in range_list[1]: | |
| if item[0] == item[1]: | |
| ranges.append(self.compile(item[0])) | |
| else: | |
| ranges.append(f"{self.compile(item[0])}..{self.compile(item[1])}") | |
| return f"{self.compile(expr)}{' not' if negated else ''} {method} {','.join(ranges)}" | |