Skip to content

Commit da2bfe8

Browse files
committed
feat(cql2-text): add modulo, integer division, and power arithmetic operators
Implements three CQL2 arithmetic operators missing from the text parser: - Modulo (%): e.g. 'attr % 2 = 0' - Integer division (div): e.g. 'attr div 2 = 1' - Power (^): e.g. 'attr ^ 2 = 4' (right-associative) Changes span all layers: - ast.py: adds ArithmeticOp.MOD/INTDIV/POW enums and Mod/IntDiv/Pow dataclasses - cql2.py: extends ARITHMETIC_MAP with the three new operators - grammar.lark: introduces a 'power' precedence level above 'product' for right-associative exponentiation; adds mod and intdiv production rules; uses named filter terminals (_PERCENT, _INTDIV) so they don't bleed into transformer callbacks - parser.py: adds mod(), intdiv(), and pow() transformer methods - tests: adds test_modulo, test_integer_division, test_power
1 parent f799198 commit da2bfe8

5 files changed

Lines changed: 60 additions & 3 deletions

File tree

pygeofilter/ast.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,9 @@ class ArithmeticOp(Enum):
632632
SUB = "-"
633633
MUL = "*"
634634
DIV = "/"
635+
MOD = "%"
636+
INTDIV = "div"
637+
POW = "^"
635638

636639

637640
@dataclass
@@ -671,6 +674,21 @@ class Div(Arithmetic):
671674
op: ClassVar[ArithmeticOp] = ArithmeticOp.DIV
672675

673676

677+
@dataclass
678+
class Mod(Arithmetic):
679+
op: ClassVar[ArithmeticOp] = ArithmeticOp.MOD
680+
681+
682+
@dataclass
683+
class IntDiv(Arithmetic):
684+
op: ClassVar[ArithmeticOp] = ArithmeticOp.INTDIV
685+
686+
687+
@dataclass
688+
class Pow(Arithmetic):
689+
op: ClassVar[ArithmeticOp] = ArithmeticOp.POW
690+
691+
674692
@dataclass
675693
class Function(Expression):
676694
"""Node class to represent function invocations."""

pygeofilter/cql2.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@
6464
"-": ast.Sub,
6565
"*": ast.Mul,
6666
"/": ast.Div,
67+
"%": ast.Mod,
68+
"div": ast.IntDiv,
69+
"^": ast.Pow,
6770
}
6871

6972
CONDITION_MAP: Dict[str, Type[ast.Node]] = {

pygeofilter/parsers/cql2_text/grammar.lark

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,14 @@
101101
| sum "+" product -> add
102102
| sum "-" product -> sub
103103

104-
?product: atom
105-
| product "*" atom -> mul
106-
| product "/" atom -> div
104+
?product: power
105+
| product "*" power -> mul
106+
| product "/" power -> div
107+
| product "%" power -> mod
108+
| product _INTDIV power -> intdiv
109+
110+
?power: atom
111+
| atom "^" power -> pow
107112

108113
?atom: func
109114
| attribute
@@ -132,6 +137,8 @@ envelope: "ENVELOPE"i "(" number number number number ")"
132137

133138
bbox: "BBOX"i "(" full_number "," full_number "," full_number "," full_number ")"
134139

140+
PERCENT: "%"
141+
_INTDIV.1: /div/i
135142
BOOLEAN.2: ( "TRUE"i | "FALSE"i)
136143

137144
DOUBLE_QUOTED: "\"" /.*?/ "\""

pygeofilter/parsers/cql2_text/parser.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,15 @@ def mul(self, lhs, rhs):
163163
def div(self, lhs, rhs):
164164
return ast.Div(lhs, rhs)
165165

166+
def mod(self, lhs, rhs):
167+
return ast.Mod(lhs, rhs)
168+
169+
def intdiv(self, lhs, rhs):
170+
return ast.IntDiv(lhs, rhs)
171+
172+
def pow(self, lhs, rhs):
173+
return ast.Pow(lhs, rhs)
174+
166175
def neg(self, value):
167176
return -value
168177

tests/parsers/cql2_text/test_parser.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,3 +477,23 @@ def test_not_eq():
477477
assert result == ast.Not(
478478
ast.Equal(ast.Attribute("attr"), 2)
479479
)
480+
481+
482+
def test_modulo():
483+
result = parse("attr % 2 = 0")
484+
assert result == ast.Equal(ast.Mod(ast.Attribute("attr"), 2), 0)
485+
486+
487+
def test_integer_division():
488+
result = parse("attr div 2 = 1")
489+
assert result == ast.Equal(ast.IntDiv(ast.Attribute("attr"), 2), 1)
490+
491+
492+
def test_power():
493+
result = parse("attr ^ 2 = 4")
494+
assert result == ast.Equal(ast.Pow(ast.Attribute("attr"), 2), 4)
495+
496+
497+
def test_power_right_associative():
498+
result = parse("attr ^ 2 ^ 3 = 256")
499+
assert result == ast.Equal(ast.Pow(ast.Attribute("attr"), ast.Pow(2, 3)), 256)

0 commit comments

Comments
 (0)