diff --git a/mathparse/mathparse.py b/mathparse/mathparse.py index f029dd9..8aaab68 100644 --- a/mathparse/mathparse.py +++ b/mathparse/mathparse.py @@ -466,17 +466,21 @@ def to_postfix(tokens: list) -> list: """ precedence = { '.': 5, - '/': 4, - '*': 4, - '+': 3, - '-': 3, - '^': 2, + '^': 4, + '/': 3, + '*': 3, + '+': 2, + '-': 2, '(': 1 } # Unary functions have a higher precedence than binary operators unary_precedence = max(precedence.values()) + 1 + # Right-associative operators use strict inequality when popping, + # so that e.g. 2^3^2 evaluates as 2^(3^2) = 512, not (2^3)^2 = 64 + right_associative = {'^'} + postfix = [] opstack = [] @@ -497,12 +501,18 @@ def to_postfix(tokens: list) -> list: postfix.append(top_token) top_token = opstack.pop() elif is_binary(token): - # For binary operators, pop operators with higher or - # equal precedence + # Pop operators with higher precedence, or equal precedence when + # the current token is left-associative (right-associative + # operators like ^ only yield to strictly higher precedence so + # that 2^3^2 = 2^(3^2) = 512 rather than (2^3)^2 = 64). + is_left_assoc = token not in right_associative while (opstack != []) and ( ( opstack[-1] in precedence and token in precedence and ( - precedence[opstack[-1]] >= precedence[token] + precedence[opstack[-1]] > precedence[token] or ( + is_left_assoc and + precedence[opstack[-1]] == precedence[token] + ) ) ) or ( diff --git a/tests/test_unary_operations.py b/tests/test_unary_operations.py index 3ee3071..3da2210 100644 --- a/tests/test_unary_operations.py +++ b/tests/test_unary_operations.py @@ -10,6 +10,33 @@ def test_exponent(self): self.assertEqual(result, 256) + def test_exponent_precedence_over_addition_and_multiplication(self): + """ + Exponentiation must bind tighter than + and *. + 2 ^ 3 + 4 * 5 = (2^3) + (4*5) = 8 + 20 = 28 + """ + result = mathparse.parse('2 ^ 3 + 4 * 5') + + self.assertEqual(result, 28) + + def test_exponent_precedence_mixed_expression(self): + """ + Exponentiation must bind tighter than * and +. + 2 + 3 * 4 ^ 5 = 2 + 3 * (4^5) = 2 + 3*1024 = 3074 + """ + result = mathparse.parse('2 + 3 * 4 ^ 5') + + self.assertEqual(result, 3074) + + def test_exponent_right_associative(self): + """ + Exponentiation is right-associative: 2^3^2 = 2^(3^2) = 512, + not (2^3)^2 = 64. + """ + result = mathparse.parse('2 ^ 3 ^ 2') + + self.assertEqual(result, 512) + def test_without_unary_operator_fre(self): result = mathparse.parse('50 * (85 / 100)', language='FRE') self.assertEqual(result, 42.5)