Skip to content

Updated postfix_evaluation.py to support Unary operators #8787

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from 11 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
52 changes: 0 additions & 52 deletions data_structures/stacks/evaluate_postfix_notations.py

This file was deleted.

222 changes: 191 additions & 31 deletions data_structures/stacks/postfix_evaluation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
"""
The Reverse Polish Nation also known as Polish postfix notation
or simply postfix notation.
https://en.wikipedia.org/wiki/Reverse_Polish_notation
Classic examples of simple stack implementations
Valid operators are +, -, *, /.
Each operand may be an integer or another expression.

Output:

Enter a Postfix Equation (space separated) = 5 6 9 * +
Expand All @@ -17,52 +24,205 @@
Result = 59
"""

import operator as op
# Defining valid unary operator symbols
UNARY_OP_SYMBOLS = ("-", "+")

# Defining valid binary operator symbols
BINARY_OP_SYMBOLS = ("-", "+", "*", "^", "/")


def parse_token(token: str | float) -> float | str:
"""
Converts the given data to appropriate number if it is indeed a number, else returns
the data as it is with a False flag. This function also serves as a check of whether
the input is a number or not.

Parameters
----------
token : str or float
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def parse_token(token: str | float) -> float | str:
"""
Converts the given data to appropriate number if it is indeed a number, else returns
the data as it is with a False flag. This function also serves as a check of whether
the input is a number or not.
Parameters
----------
token : str or float
def parse_token(token: str) -> float | str:
"""
Converts the given data to appropriate number if it is indeed a number, else returns
the data as it is with a False flag. This function also serves as a check of whether
the input is a number or not.
Parameters
----------
token : str

I meant for this suggestion to be for is_operator, not this function. I believe this function is only used when initially parsing the input list, so all tokens passed into this function should be strings.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes understood. I have made the necessary changes.

The data which needs to be converted to the appropriate number

Returns
-------
float or str
Returns a float if `token` is a number and returns `token` if it's an operator
"""
if is_operator(token):
return token
try:
return float(token)
except ValueError:
msg = f"{token} is neither a number nor a valid operator"
raise ValueError(msg)


def is_operator(token: str | float) -> bool:
"""
Checks whether a given input is one of the valid operators or not.
Valid operators being '-', '+', '*', '^' and '/'.

Parameters
----------
token : str
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
token : str
token : str or float

Copy link
Member

@cclauss cclauss Aug 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be str | float instead of or?

Why would we want token to be a float?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each element in the token list valid_expression is passed into this function to check if it's an operator—those elements may be numbers.

The value that needs to be checked for operator

Returns
-------
bool
True if data is an operator else False.
"""
return token in BINARY_OP_SYMBOLS


def evaluate(post_fix: list[str], verbose: bool = False) -> float:
"""
Function that evaluates postfix expression using a stack.
>>> evaluate(["2", "1", "+", "3", "*"])
9.0
>>> evaluate(["4", "13", "5", "/", "+"])
Copy link
Member

@cclauss cclauss Aug 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add doctests for:

  • evaluate(["0"])
  • evaluate(["-0"])
  • evaluate(["1"])
  • evaluate(["-1"])
  • evaluate(["-1.1"])
  • evaluate(["2", "1.9", "+", "3", "*"])
  • evaluate(["2", "-1.9", "+", "3", "*"])
  • evaluate(["2", "--1.9", "+", "3", "*"])

6.6
>>> evaluate(["2", "-", "3", "+"])
1.0
>>> evaluate(["-4", "5", "*", "6", "-"])
-26.0
>>> evaluate([])
0
>>> evaluate(["4", "-", "6", "7", "/", "9", "8"])
Traceback (most recent call last):
...
ArithmeticError: Input is not a valid postfix expression

Parameters
----------
post_fix : list
The postfix expression tokenized into operators and operands and stored as a
python list

verbose : bool
Display stack contents while evaluating the expression if verbose is True

def solve(post_fix):
Returns
-------
float
The evaluated value
"""
stack = []
div = lambda x, y: int(x / y) # noqa: E731 integer division operation
valid_expression = []
opr = {
"^": op.pow,
"*": op.mul,
"/": div,
"+": op.add,
"-": op.sub,
"^": lambda p, q: p**q,
"*": lambda p, q: p * q,
"/": lambda p, q: p / q,
"+": lambda p, q: p + q,
"-": lambda p, q: p - q,
} # operators & their respective operation

# print table header
print("Symbol".center(8), "Action".center(12), "Stack", sep=" | ")
print("-" * (30 + len(post_fix)))

for x in post_fix:
if x.isdigit(): # if x in digit
if len(post_fix) == 0:
return 0
# Checking the list to find out whether the postfix expression is valid
valid_expression = [parse_token(token) for token in post_fix]
if verbose:
# print table header
print("Symbol".center(8), "Action".center(12), "Stack", sep=" | ")
print("-" * (30 + len(post_fix)))
for x in valid_expression:
if not is_operator(x):
stack.append(x) # append x to stack
# output in tabular format
print(x.rjust(8), ("push(" + x + ")").ljust(12), ",".join(stack), sep=" | ")
else:
if verbose:
# output in tabular format
print(
str(x).rjust(8),
("push(" + str(x) + ")").ljust(12),
stack,
sep=" | ",
)
continue
# If x is operator
# If only 1 value is inside stack and + or - is encountered
# then this is unary + or - case
if x in UNARY_OP_SYMBOLS and len(stack) < 2:
b = stack.pop() # pop stack
if x == "-":
b *= -1 # negate b
stack.append(b)
if verbose:
# output in tabular format
print(
"".rjust(8),
("pop(" + str(b) + ")").ljust(12),
stack,
sep=" | ",
)
print(
str(x).rjust(8),
("push(" + str(x) + str(b) + ")").ljust(12),
stack,
sep=" | ",
)
continue
b = stack.pop() # pop stack
if verbose:
# output in tabular format
print("".rjust(8), ("pop(" + b + ")").ljust(12), ",".join(stack), sep=" | ")
print(
"".rjust(8),
("pop(" + str(b) + ")").ljust(12),
stack,
sep=" | ",
)

a = stack.pop() # pop stack
a = stack.pop() # pop stack
if verbose:
# output in tabular format
print("".rjust(8), ("pop(" + a + ")").ljust(12), ",".join(stack), sep=" | ")

stack.append(
str(opr[x](int(a), int(b)))
) # evaluate the 2 values popped from stack & push result to stack
print(
"".rjust(8),
("pop(" + str(a) + ")").ljust(12),
stack,
sep=" | ",
)
# evaluate the 2 values popped from stack & push result to stack
stack.append(opr[str(x)](a, b))
if verbose:
# output in tabular format
print(
x.rjust(8),
("push(" + a + x + b + ")").ljust(12),
",".join(stack),
str(x).rjust(8),
("push(" + str(a) + str(x) + str(b) + ")").ljust(12),
stack,
sep=" | ",
)
# If everything executed correctly, the stack will contain
# only one element which is the result
if len(stack) != 1:
raise ArithmeticError("Input is not a valid postfix expression")
return float(stack[0])


def is_yes(val: str) -> bool:
"""
Function that checks whether a user has entered any representation of a Yes (y, Y).
Any other input is considered as a No.

Parameters
-----------
val : str
The value entered by user

return int(stack[0])
Returns
-------
bool
True if Yes, otherwise False
"""
return val in ("Y", "y")


if __name__ == "__main__":
Postfix = input("\n\nEnter a Postfix Equation (space separated) = ").split(" ")
print("\n\tResult = ", solve(Postfix))
loop = True
# Creating a loop so that user can evaluate postfix expression multiple times
while True:
expression = input("Enter a Postfix Expression (space separated): ").split(" ")
choice = input(
"Do you want to see stack contents while evaluating? [y/N]: "
).strip()
display = is_yes(choice)
output = evaluate(expression, display)
print("Result = ", output)
choice = input("Do you want to enter another expression? [y/N]: ")
if not is_yes(choice):
break