Skip to content

Commit 1159940

Browse files
authored
Merge pull request #4 from jmespath-community/jep/arithmetic-expressions
JEP-16 Arithmetic Expressions
2 parents 974fd67 + a99a6d8 commit 1159940

File tree

7 files changed

+276
-12
lines changed

7 files changed

+276
-12
lines changed

jmespath/ast.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
# {"type": <node type>", children: [], "value": ""}
33

44

5+
def arithmetic_unary(operator, expression):
6+
return {'type': 'arithmetic_unary', 'children': [expression], 'value': operator}
7+
8+
9+
def arithmetic(operator, left, right):
10+
return {'type': 'arithmetic', 'children': [left, right], 'value': operator}
11+
12+
513
def comparator(name, first, second):
614
return {'type': 'comparator', 'children': [first, second], 'value': name}
715

jmespath/lexer.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ class Lexer(object):
2121
')': 'rparen',
2222
'{': 'lbrace',
2323
'}': 'rbrace',
24+
'+': 'plus',
25+
'%': 'modulo',
26+
u'\u2212': 'minus',
27+
u'\u00d7': 'multiply',
28+
u'\u00f7': 'divide',
2429
}
2530

2631
def tokenize(self, expression):
@@ -68,16 +73,30 @@ def tokenize(self, expression):
6873
yield {'type': 'number', 'value': int(buff),
6974
'start': start, 'end': start + len(buff)}
7075
elif self._current == '-':
71-
# Negative number.
72-
start = self._position
73-
buff = self._consume_number()
74-
if len(buff) > 1:
75-
yield {'type': 'number', 'value': int(buff),
76-
'start': start, 'end': start + len(buff)}
76+
if not self._peek_is_next_digit():
77+
self._next()
78+
yield {'type': 'minus', 'value': '-',
79+
'start': self._position - 1, 'end': self._position}
80+
else:
81+
# Negative number.
82+
start = self._position
83+
buff = self._consume_number()
84+
if len(buff) > 1:
85+
yield {'type': 'number', 'value': int(buff),
86+
'start': start, 'end': start + len(buff)}
87+
else:
88+
raise LexerError(lexer_position=start,
89+
lexer_value=buff,
90+
message="Unknown token '%s'" % buff)
91+
elif self._current == '/':
92+
self._next()
93+
if self._current == '/':
94+
self._next()
95+
yield {'type': 'div', 'value': '//',
96+
'start': self._position - 1, 'end': self._position}
7797
else:
78-
raise LexerError(lexer_position=start,
79-
lexer_value=buff,
80-
message="Unknown token '%s'" % buff)
98+
yield {'type': 'divide', 'value': '/',
99+
'start': self._position, 'end': self._position + 1}
81100
elif self._current == '"':
82101
yield self._consume_quoted_identifier()
83102
elif self._current == '<':
@@ -117,6 +136,13 @@ def _consume_number(self):
117136
buff += self._current
118137
return buff
119138

139+
def _peek_is_next_digit(self):
140+
if (self._position == self._length - 1):
141+
return False
142+
else:
143+
next = self._chars[self._position + 1]
144+
return next in self.VALID_NUMBER
145+
120146
def _initialize_for_expression(self, expression):
121147
if not expression:
122148
raise EmptyExpressionError()

jmespath/parser.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ class Parser(object):
5757
'gte': 5,
5858
'lte': 5,
5959
'ne': 5,
60+
'minus': 6,
61+
'plus': 6,
62+
'div': 7,
63+
'divide': 7,
64+
'modulo': 7,
65+
'multiply': 7,
6066
'flatten': 9,
6167
# Everything above stops a projection.
6268
'star': 20,
@@ -170,6 +176,12 @@ def _token_nud_lparen(self, token):
170176
self._match('rparen')
171177
return expression
172178

179+
def _token_nud_minus(self, token):
180+
return self._parse_arithmetic_unary(token)
181+
182+
def _token_nud_plus(self, token):
183+
return self._parse_arithmetic_unary(token)
184+
173185
def _token_nud_flatten(self, token):
174186
left = ast.flatten(ast.identity())
175187
right = self._parse_projection_rhs(
@@ -318,6 +330,27 @@ def _token_led_lt(self, left):
318330
def _token_led_lte(self, left):
319331
return self._parse_comparator(left, 'lte')
320332

333+
def _token_led_div(self, left):
334+
return self._parse_arithmetic(left, 'div')
335+
336+
def _token_led_divide(self, left):
337+
return self._parse_arithmetic(left, 'divide')
338+
339+
def _token_led_minus(self, left):
340+
return self._parse_arithmetic(left, 'minus')
341+
342+
def _token_led_modulo(self, left):
343+
return self._parse_arithmetic(left, 'modulo')
344+
345+
def _token_led_multiply(self, left):
346+
return self._parse_arithmetic(left, 'multiply')
347+
348+
def _token_led_plus(self, left):
349+
return self._parse_arithmetic(left, 'plus')
350+
351+
def _token_led_star(self, left):
352+
return self._parse_arithmetic(left, 'multiply')
353+
321354
def _token_led_flatten(self, left):
322355
left = ast.flatten(left)
323356
right = self._parse_projection_rhs(
@@ -356,6 +389,14 @@ def _parse_comparator(self, left, comparator):
356389
right = self._expression(self.BINDING_POWER[comparator])
357390
return ast.comparator(comparator, left, right)
358391

392+
def _parse_arithmetic_unary(self, token):
393+
expression = self._expression(self.BINDING_POWER[token['type']])
394+
return ast.arithmetic_unary(token['type'], expression)
395+
396+
def _parse_arithmetic(self, left, operator):
397+
right = self._expression(self.BINDING_POWER[operator])
398+
return ast.arithmetic(operator, left, right)
399+
359400
def _parse_multi_select_list(self):
360401
expressions = []
361402
while True:

jmespath/visitor.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,18 @@ class TreeInterpreter(Visitor):
107107
'gte': operator.ge
108108
}
109109
_EQUALITY_OPS = ['eq', 'ne']
110+
_ARITHMETIC_UNARY_FUNC = {
111+
'minus': operator.neg,
112+
'plus': lambda x: x
113+
}
114+
_ARITHMETIC_FUNC = {
115+
'div': operator.floordiv,
116+
'divide': operator.truediv,
117+
'minus': operator.sub,
118+
'modulo': operator.mod,
119+
'multiply': operator.mul,
120+
'plus': operator.add,
121+
}
110122
MAP_TYPE = dict
111123

112124
def __init__(self, options=None):
@@ -159,6 +171,19 @@ def visit_comparator(self, node, value):
159171
return None
160172
return comparator_func(left, right)
161173

174+
def visit_arithmetic_unary(self, node, value):
175+
operation = self._ARITHMETIC_UNARY_FUNC[node['value']]
176+
return operation(
177+
self.visit(node['children'][0], value)
178+
)
179+
180+
def visit_arithmetic(self, node, value):
181+
operation = self._ARITHMETIC_FUNC[node['value']]
182+
return operation(
183+
self.visit(node['children'][0], value),
184+
self.visit(node['children'][1], value)
185+
)
186+
162187
def visit_current(self, node, value):
163188
return value
164189

tests/compliance/arithmetic.json

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
[
2+
{
3+
"given": {
4+
"a": {
5+
"b": 1
6+
},
7+
"c": {
8+
"d": 2
9+
}
10+
},
11+
"cases": [
12+
{
13+
"expression": "`1` + `2`",
14+
"result": 3.0
15+
},
16+
{
17+
"expression": "`1` - `2`",
18+
"result": -1.0
19+
},
20+
{
21+
"expression": "`2` * `4`",
22+
"result": 8.0
23+
},
24+
{
25+
"expression": "`2` × `4`",
26+
"result": 8.0
27+
},
28+
{
29+
"expression": "`2` / `3`",
30+
"result": 0.6666666666666666
31+
},
32+
{
33+
"expression": "`2` ÷ `3`",
34+
"result": 0.6666666666666666
35+
},
36+
{
37+
"expression": "`10` % `3`",
38+
"result": 1.0
39+
},
40+
{
41+
"expression": "`10` // `3`",
42+
"result": 3.0
43+
},
44+
{
45+
"expression": "-`1` - + `2`",
46+
"result": -3.0
47+
},
48+
{
49+
"expression": "{ ab: a.b, cd: c.d } | ab + cd",
50+
"result": 3.0
51+
},
52+
{
53+
"expression": "{ ab: a.b, cd: c.d } | ab + cd × cd",
54+
"result": 5.0
55+
},
56+
{
57+
"expression": "{ ab: a.b, cd: c.d } | (ab + cd) × cd",
58+
"result": 6.0
59+
}
60+
]
61+
}
62+
]

tests/test_lexer.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,50 @@ def test_negative_number(self):
4545
self.assert_tokens(tokens, [{'type': 'number',
4646
'value': -24}])
4747

48+
def test_plus(self):
49+
tokens = list(self.lexer.tokenize('+'))
50+
self.assert_tokens(tokens, [{'type': 'plus',
51+
'value': '+'}])
52+
53+
def test_minus(self):
54+
tokens = list(self.lexer.tokenize('-'))
55+
self.assert_tokens(tokens, [{'type': 'minus',
56+
'value': '-'}])
57+
def test_minus_unicode(self):
58+
tokens = list(self.lexer.tokenize(u'\u2212'))
59+
self.assert_tokens(tokens, [{'type': 'minus',
60+
'value': u'\u2212'}])
61+
62+
def test_multiplication(self):
63+
tokens = list(self.lexer.tokenize('*'))
64+
self.assert_tokens(tokens, [{'type': 'star',
65+
'value': '*'}])
66+
67+
def test_multiplication_unicode(self):
68+
tokens = list(self.lexer.tokenize(u'\u00d7'))
69+
self.assert_tokens(tokens, [{'type': 'multiply',
70+
'value': u'\u00d7'}])
71+
72+
def test_division(self):
73+
tokens = list(self.lexer.tokenize('/'))
74+
self.assert_tokens(tokens, [{'type': 'divide',
75+
'value': '/'}])
76+
77+
def test_division_unicode(self):
78+
tokens = list(self.lexer.tokenize('÷'))
79+
self.assert_tokens(tokens, [{'type': 'divide',
80+
'value': '÷'}])
81+
82+
def test_modulo(self):
83+
tokens = list(self.lexer.tokenize('%'))
84+
self.assert_tokens(tokens, [{'type': 'modulo',
85+
'value': '%'}])
86+
87+
def test_integer_division(self):
88+
tokens = list(self.lexer.tokenize('//'))
89+
self.assert_tokens(tokens, [{'type': 'div',
90+
'value': '//'}])
91+
4892
def test_quoted_identifier(self):
4993
tokens = list(self.lexer.tokenize('"foobar"'))
5094
self.assert_tokens(tokens, [{'type': 'quoted_identifier',
@@ -151,9 +195,17 @@ def test_bad_first_character(self):
151195
with self.assertRaises(LexerError):
152196
tokens = list(self.lexer.tokenize('^foo[0]'))
153197

154-
def test_unknown_character_with_identifier(self):
155-
with self.assertRaisesRegex(LexerError, "Unknown token"):
156-
list(self.lexer.tokenize('foo-bar'))
198+
def test_arithmetic_expression(self):
199+
tokens = list(self.lexer.tokenize('foo-bar'))
200+
self.assertEqual(
201+
tokens,
202+
[
203+
{'type': 'unquoted_identifier', 'value': 'foo', 'start': 0, 'end': 3},
204+
{'type': 'minus', 'value': '-', 'start': 3, 'end': 4},
205+
{'type': 'unquoted_identifier', 'value': 'bar', 'start': 4, 'end': 7},
206+
{'type': 'eof', 'value': '', 'start': 7, 'end': 7}
207+
]
208+
)
157209

158210

159211
if __name__ == '__main__':

tests/test_parser.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import random
44
import string
55
import threading
6+
from jmespath.ast import arithmetic
67
from tests import unittest, OrderedDict
78

89
from jmespath import parser
@@ -77,6 +78,55 @@ def test_or_repr(self):
7778
self.assert_parsed_ast('foo || bar', ast.or_expression(ast.field('foo'),
7879
ast.field('bar')))
7980

81+
def test_arithmetic_expressions(self):
82+
operations = {
83+
'+': 'plus',
84+
'-': 'minus',
85+
'//': 'div',
86+
'/': 'divide',
87+
'%': 'modulo',
88+
u'\u2212': 'minus',
89+
u'\u00d7': 'multiply',
90+
u'\u00f7': 'divide',
91+
}
92+
for sign in operations:
93+
operation = operations[sign]
94+
expression = 'foo {} bar'.format(sign)
95+
print(expression)
96+
self.assert_parsed_ast(
97+
expression,
98+
ast.arithmetic(
99+
operation,
100+
ast.field('foo'),
101+
ast.field('bar')
102+
))
103+
104+
def test_arithmetic_unary(self):
105+
operations = {
106+
'+': 'plus',
107+
'-': 'minus',
108+
u'\u2212': 'minus',
109+
}
110+
for sign in operations:
111+
operation = operations[sign]
112+
expression = '{} foo'.format(sign)
113+
print(expression)
114+
self.assert_parsed_ast(
115+
expression,
116+
ast.arithmetic_unary(
117+
operation,
118+
ast.field('foo'),
119+
))
120+
121+
def test_arithmetic_multiplication(self):
122+
self.assert_parsed_ast(
123+
'foo * bar',
124+
ast.arithmetic(
125+
'multiply',
126+
ast.field('foo'),
127+
ast.field('bar')
128+
))
129+
80130
def test_unicode_literals_escaped(self):
81131
self.assert_parsed_ast(r'`"\u2713"`', ast.literal(u'\u2713'))
82132

0 commit comments

Comments
 (0)