From bb9f009d0983df2c3be9a344cda666f508594132 Mon Sep 17 00:00:00 2001 From: Alex Fraser Date: Fri, 19 Aug 2016 15:52:34 +1000 Subject: [PATCH 1/6] Fixed parsing of multi-character operators Multi-character operators were failing when the length of the input expression was short --- py_expression_eval/__init__.py | 70 +++++++++++++--------------------- py_expression_eval/tests.py | 2 + 2 files changed, 28 insertions(+), 44 deletions(-) diff --git a/py_expression_eval/__init__.py b/py_expression_eval/__init__.py index 9bb107e..1eeaf82 100644 --- a/py_expression_eval/__init__.py +++ b/py_expression_eval/__init__.py @@ -752,60 +752,42 @@ def isConst(self): return False def isOperator(self): - code = self.expression[self.pos] - if code == '+': + if self.expression[self.pos] == '+': self.tokenprio = 0 self.tokenindex = '+' - elif code == '-': + elif self.expression[self.pos] == '-': self.tokenprio = 0 self.tokenindex = '-' - elif code == '|': - if self.expression[self.pos] == '|': - self.pos += 1 - self.tokenprio = 0 - self.tokenindex = '||' - else: - return False - elif code == '=': - if self.expression[self.pos + 1] == "=": - self.pos += 1 - self.tokenprio = 1 - self.tokenindex = "==" - - else: - return False - elif code == "!": - if self.expression[self.pos + 1] == "=": - self.pos += 1 - self.tokenprio = 1 - self.tokenindex = "!=" - else: - return False - elif code == 'a': - if self.expression[self.pos + 1] == 'n' and self.expression[self.pos + 2] == 'd': - self.pos += 2 - self.tokenprio = 0 - self.tokenindex = "and" - else: - return False - elif code == 'o': - if self.expression[self.pos + 1] == 'r': - self.pos += 1 - self.tokenprio = 0 - self.tokenindex = "or" - else: - return False - - elif code == '*': + elif self.expression[self.pos:self.pos + 2] == '||': + self.pos += 1 + self.tokenprio = 0 + self.tokenindex = '||' + elif self.expression[self.pos:self.pos + 2] == '==': + self.pos += 1 + self.tokenprio = 1 + self.tokenindex = "==" + elif self.expression[self.pos:self.pos + 2] == '!=': + self.pos += 1 + self.tokenprio = 1 + self.tokenindex = "!=" + elif self.expression[self.pos:self.pos + 3] == 'and': + self.pos += 2 + self.tokenprio = 0 + self.tokenindex = "and" + elif self.expression[self.pos:self.pos + 2] == 'or': + self.pos += 1 + self.tokenprio = 0 + self.tokenindex = "or" + elif self.expression[self.pos] == '*': self.tokenprio = 1 self.tokenindex = '*' - elif code == '/': + elif self.expression[self.pos] == '/': self.tokenprio = 2 self.tokenindex = '/' - elif code == '%': + elif self.expression[self.pos] == '%': self.tokenprio = 2 self.tokenindex = '%' - elif code == '^': + elif self.expression[self.pos] == '^': self.tokenprio = 3 self.tokenindex = '^' else: diff --git a/py_expression_eval/tests.py b/py_expression_eval/tests.py index d9afe23..c6c5b0a 100755 --- a/py_expression_eval/tests.py +++ b/py_expression_eval/tests.py @@ -28,6 +28,8 @@ def test_parser(self): self.assertEqual(parser.parse('lulu(x,y)').variables(), ['lulu','x','y']) #evaluate + self.assertEqual(parser.parse('1').evaluate({}), 1) + self.assertEqual(parser.parse('a').evaluate({'a': 2}), 2) self.assertEqual(parser.parse('2 * 3').evaluate({}), 6) self.assertEqual(parser.parse('2 ^ x').evaluate({'x': 3}), 8) self.assertEqual(parser.parse('2 * x + 1').evaluate({'x': 3}), 7) From 3ec25a6abc9aef1bddf4a71db9e29a844c7b6306 Mon Sep 17 00:00:00 2001 From: Alex Fraser Date: Fri, 19 Aug 2016 15:55:53 +1000 Subject: [PATCH 2/6] Simplified conditions in isOperator method --- py_expression_eval/__init__.py | 62 +++++++++++----------------------- 1 file changed, 20 insertions(+), 42 deletions(-) diff --git a/py_expression_eval/__init__.py b/py_expression_eval/__init__.py index 1eeaf82..d525729 100644 --- a/py_expression_eval/__init__.py +++ b/py_expression_eval/__init__.py @@ -752,48 +752,26 @@ def isConst(self): return False def isOperator(self): - if self.expression[self.pos] == '+': - self.tokenprio = 0 - self.tokenindex = '+' - elif self.expression[self.pos] == '-': - self.tokenprio = 0 - self.tokenindex = '-' - elif self.expression[self.pos:self.pos + 2] == '||': - self.pos += 1 - self.tokenprio = 0 - self.tokenindex = '||' - elif self.expression[self.pos:self.pos + 2] == '==': - self.pos += 1 - self.tokenprio = 1 - self.tokenindex = "==" - elif self.expression[self.pos:self.pos + 2] == '!=': - self.pos += 1 - self.tokenprio = 1 - self.tokenindex = "!=" - elif self.expression[self.pos:self.pos + 3] == 'and': - self.pos += 2 - self.tokenprio = 0 - self.tokenindex = "and" - elif self.expression[self.pos:self.pos + 2] == 'or': - self.pos += 1 - self.tokenprio = 0 - self.tokenindex = "or" - elif self.expression[self.pos] == '*': - self.tokenprio = 1 - self.tokenindex = '*' - elif self.expression[self.pos] == '/': - self.tokenprio = 2 - self.tokenindex = '/' - elif self.expression[self.pos] == '%': - self.tokenprio = 2 - self.tokenindex = '%' - elif self.expression[self.pos] == '^': - self.tokenprio = 3 - self.tokenindex = '^' - else: - return False - self.pos += 1 - return True + ops = ( + ('+', 0), + ('-', 0), + ('||', 0), + ('==', 1), + ('!=', 1), + ('and', 0), + ('or', 0), + ('*', 1), + ('/', 2), + ('%', 2), + ('^', 3), + ) + for operator, priority in ops: + if self.expression.startswith(operator, self.pos): + self.tokenprio = priority + self.tokenindex = operator + self.pos += len(operator) + return True + return False def isSign(self): code = self.expression[self.pos - 1] From a924628093744593062257f946ee177a2d8294d9 Mon Sep 17 00:00:00 2001 From: Alex Fraser Date: Fri, 19 Aug 2016 16:39:07 +1000 Subject: [PATCH 3/6] Added missing operators to isOperator method isOperator was not checking for inequality ops. It was also missing the variants of the multiplication operator that js-expression-eval supports. --- py_expression_eval/__init__.py | 36 ++++++++++++++++++++-------------- py_expression_eval/tests.py | 8 ++++++++ 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/py_expression_eval/__init__.py b/py_expression_eval/__init__.py index d525729..d3dd617 100644 --- a/py_expression_eval/__init__.py +++ b/py_expression_eval/__init__.py @@ -753,23 +753,29 @@ def isConst(self): def isOperator(self): ops = ( - ('+', 0), - ('-', 0), - ('||', 0), - ('==', 1), - ('!=', 1), - ('and', 0), - ('or', 0), - ('*', 1), - ('/', 2), - ('%', 2), - ('^', 3), + ('+', 0, '+'), + ('-', 0, '-'), + ('*', 1, '*'), + ('\u2219', 1, '*'), # bullet operator + ('\u2022', 1, '*'), # black small circle + ('/', 2, '/'), + ('%', 2, '%'), + ('^', 3, '^'), + ('||', 0, '||'), + ('==', 1, '=='), + ('!=', 1, '!='), + ('<=', 1, '<='), + ('>=', 1, '>='), + ('<', 1, '<'), + ('>', 1, '>'), + ('and', 0, 'and'), + ('or', 0, 'or'), ) - for operator, priority in ops: - if self.expression.startswith(operator, self.pos): + for token, priority, index in ops: + if self.expression.startswith(token, self.pos): self.tokenprio = priority - self.tokenindex = operator - self.pos += len(operator) + self.tokenindex = index + self.pos += len(token) return True return False diff --git a/py_expression_eval/tests.py b/py_expression_eval/tests.py index c6c5b0a..24f3025 100755 --- a/py_expression_eval/tests.py +++ b/py_expression_eval/tests.py @@ -31,7 +31,15 @@ def test_parser(self): self.assertEqual(parser.parse('1').evaluate({}), 1) self.assertEqual(parser.parse('a').evaluate({'a': 2}), 2) self.assertEqual(parser.parse('2 * 3').evaluate({}), 6) + self.assertEqual(parser.parse('2 \u2219 3').evaluate({}), 6) + self.assertEqual(parser.parse('2 • 3').evaluate({}), 6) self.assertEqual(parser.parse('2 ^ x').evaluate({'x': 3}), 8) + self.assertEqual(parser.parse('x < 3').evaluate({'x': 3}), False) + self.assertEqual(parser.parse('x < 3').evaluate({'x': 2}), True) + self.assertEqual(parser.parse('x <= 3').evaluate({'x': 3}), True) + self.assertEqual(parser.parse('x <= 3').evaluate({'x': 4}), False) + self.assertEqual(parser.parse('x > 3').evaluate({'x': 4}), True) + self.assertEqual(parser.parse('x >= 3').evaluate({'x': 3}), True) self.assertEqual(parser.parse('2 * x + 1').evaluate({'x': 3}), 7) self.assertEqual(parser.parse('2 + 3 * x').evaluate({'x': 4}), 14) self.assertEqual(parser.parse('(2 + 3) * x').evaluate({'x': 4}), 20) From 3fe4079c3602994aa943d210a01e5a2d779f595f Mon Sep 17 00:00:00 2001 From: Alex Fraser Date: Fri, 19 Aug 2016 16:43:28 +1000 Subject: [PATCH 4/6] Updated token priorities to match js-expression-eval --- py_expression_eval/__init__.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/py_expression_eval/__init__.py b/py_expression_eval/__init__.py index d3dd617..2d5418a 100644 --- a/py_expression_eval/__init__.py +++ b/py_expression_eval/__init__.py @@ -532,7 +532,7 @@ def parse(self, expr): if self.isOperator(): if self.isSign() and expected & self.SIGN: if self.isNegativeSign(): - self.tokenprio = 2 + self.tokenprio = 5 self.tokenindex = '-' noperators += 1 self.addfunc(tokenstack, operstack, TOP1) @@ -753,15 +753,15 @@ def isConst(self): def isOperator(self): ops = ( - ('+', 0, '+'), - ('-', 0, '-'), - ('*', 1, '*'), - ('\u2219', 1, '*'), # bullet operator - ('\u2022', 1, '*'), # black small circle - ('/', 2, '/'), - ('%', 2, '%'), - ('^', 3, '^'), - ('||', 0, '||'), + ('+', 2, '+'), + ('-', 2, '-'), + ('*', 3, '*'), + ('\u2219', 3, '*'), # bullet operator + ('\u2022', 3, '*'), # black small circle + ('/', 4, '/'), + ('%', 4, '%'), + ('^', 6, '^'), + ('||', 1, '||'), ('==', 1, '=='), ('!=', 1, '!='), ('<=', 1, '<='), @@ -833,7 +833,7 @@ def isOp1(self): str += c if len(str) > 0 and str in self.ops1: self.tokenindex = str - self.tokenprio = 5 + self.tokenprio = 7 self.pos += len(str) return True return False @@ -848,7 +848,7 @@ def isOp2(self): str += c if len(str) > 0 and (str in self.ops2): self.tokenindex = str - self.tokenprio = 5 + self.tokenprio = 7 self.pos += len(str) return True return False From 804f796eae1c3fa6feb4f249f2f89c222226b12c Mon Sep 17 00:00:00 2001 From: Alex Fraser Date: Fri, 19 Aug 2016 16:59:25 +1000 Subject: [PATCH 5/6] Added test for list append operation --- py_expression_eval/tests.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/py_expression_eval/tests.py b/py_expression_eval/tests.py index 24f3025..0a694b3 100755 --- a/py_expression_eval/tests.py +++ b/py_expression_eval/tests.py @@ -91,6 +91,9 @@ def test_parser(self): self.assertEqual(expr.variables(), ['x', 'y']) self.assertEqual(expr.simplify({'y': 4}).variables(), ['x']) + # list operations + self.assertEqual(parser.parse('a, 3').evaluate({'a': [1, 2]}), [1, 2, 3]) + def test_consts(self): # self.assertEqual(self.parser.parse("PI ").variables(), [""]) self.assertEqual(self.parser.parse("PI").variables(), []) From 241545f4b5d49e6538e04cde5e9c907eec70a79e Mon Sep 17 00:00:00 2001 From: Alex Fraser Date: Fri, 19 Aug 2016 17:14:34 +1000 Subject: [PATCH 6/6] Fixes for Python 2.7 Unicode string literals... --- py_expression_eval/__init__.py | 4 ++-- py_expression_eval/tests.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/py_expression_eval/__init__.py b/py_expression_eval/__init__.py index 2d5418a..b7496f8 100644 --- a/py_expression_eval/__init__.py +++ b/py_expression_eval/__init__.py @@ -756,8 +756,8 @@ def isOperator(self): ('+', 2, '+'), ('-', 2, '-'), ('*', 3, '*'), - ('\u2219', 3, '*'), # bullet operator - ('\u2022', 3, '*'), # black small circle + (u'\u2219', 3, '*'), # bullet operator + (u'\u2022', 3, '*'), # black small circle ('/', 4, '/'), ('%', 4, '%'), ('^', 6, '^'), diff --git a/py_expression_eval/tests.py b/py_expression_eval/tests.py index 0a694b3..ffa2a8e 100755 --- a/py_expression_eval/tests.py +++ b/py_expression_eval/tests.py @@ -31,8 +31,8 @@ def test_parser(self): self.assertEqual(parser.parse('1').evaluate({}), 1) self.assertEqual(parser.parse('a').evaluate({'a': 2}), 2) self.assertEqual(parser.parse('2 * 3').evaluate({}), 6) - self.assertEqual(parser.parse('2 \u2219 3').evaluate({}), 6) - self.assertEqual(parser.parse('2 • 3').evaluate({}), 6) + self.assertEqual(parser.parse(u'2 \u2219 3').evaluate({}), 6) + self.assertEqual(parser.parse(u'2 \u2022 3').evaluate({}), 6) self.assertEqual(parser.parse('2 ^ x').evaluate({'x': 3}), 8) self.assertEqual(parser.parse('x < 3').evaluate({'x': 3}), False) self.assertEqual(parser.parse('x < 3').evaluate({'x': 2}), True)