Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions mathparse/mathparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []

Expand All @@ -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
(
Expand Down
27 changes: 27 additions & 0 deletions tests/test_unary_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading