forked from neoJINXD/Eternity
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathparse.py
More file actions
265 lines (232 loc) · 9.97 KB
/
Copy pathparse.py
File metadata and controls
265 lines (232 loc) · 9.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
from functions import common
from functions import exponents_and_logs
from functions import statistic
from functions import trigonometry
import functions.output_display as display
import exceptions.exceptions as exceptions
from pyparsing import (
Literal,
Word,
Group,
Forward,
alphas,
Regex,
ParseException,
CaselessKeyword,
Suppress,
delimitedList,
)
class Parser(object):
"""A class used to parse mathematical expressions.
Methods:
evaluate(expression, expected=None, parse_all=True)
Returns the result of an expression in string form.
"""
_symbol_stack = []
def push_first(self: object, tokens: list) -> None:
"""Push first element of 'tokens' onto symbol stack.
Used properly during parsing, this function can turn infix notation into postfix notation.
Args:
tokens (list): Tokens from which first token must be taken.
"""
self._symbol_stack.append(tokens[0])
def push_unary_operator(self: object, tokens: list) -> None:
"""Processes unary operators applied to atom and appends appropriate operators to symbol stack.
For now, we have only two possible unary operators ('-' and '+').
'+' does nothing and '-' turns the atom negative in odd numbers.
Args:
tokens (list): Tokens belonging to atom. Unary operators to apply to atom will be at start of list.
"""
# We will track parity of unary '-'s to push the 'unary -' symbol onto the stack only once per atom and improve runtime.
is_negative = False
for e in tokens:
if e == '-':
is_negative = not is_negative
elif e == '+':
continue
# The first symbol found that is not a unary operator marks the end of the unary operators.
else:
break
if is_negative:
self._symbol_stack.append('unary -')
def __init__(self: object, is_rad: bool, is_binary: bool) -> None:
"""Define grammar to be used by parser and parse actions to be used in constructing the symbol stack.
Args:
is_rad (bool): Angle mode
is_binary (bool): Binary input option
"""
# Settings
self._is_rad = is_rad
self._is_binary = is_binary
# Expressions
expr = Forward()
# Operations
plus, minus, multiply, divide, mod = map(Literal, "+-*/%")
left_bracket, right_bracket = map(Suppress, "()")
addition_operation = plus | minus
multiplication_operation = multiply | divide | mod
power_operation = Literal("^")
factorial_operation = Literal("!")
# Functions
def add_arg_count_to_tokens(tokens):
function_id = tokens.pop(0)
arg_count = len(tokens[0])
tokens.insert(0, (function_id, arg_count))
function_id = Word(alphas)
# Expressions must be in groups so that we can count them separately.
argument_list = delimitedList(Group(expr))
function = (function_id + left_bracket + Group(argument_list) + right_bracket
).setParseAction(add_arg_count_to_tokens)
# Values
e = CaselessKeyword("E")
pi = CaselessKeyword("PI")
number = Regex(r"[+-]?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?")
value = (e | pi | number)
# Expression grammars. Order of operations is determined here.
factor = Forward()
atom = (addition_operation[...] + (
(function | value).setParseAction(self.push_first) |
Group(left_bracket + expr + right_bracket)
)).setParseAction(self.push_unary_operator)
factor << atom + (
(power_operation + factor) |
factorial_operation
).setParseAction(self.push_first)[...]
term = factor + (
multiplication_operation + factor
).setParseAction(self.push_first)[...]
expr << term + (
addition_operation + term
).setParseAction(self.push_first)[...]
self.bnf = expr
# Mapping between constant names and providers
self.constant_map = {"PI": trigonometry.generate_pi(),
"E": exponents_and_logs.pow_e(1),
}
# Mapping between operators and appropriate function calls
self.operation_map = {"+": lambda a, b: a + b,
"-": lambda a, b: a - b,
"*": lambda a, b: a * b,
"/": lambda a, b: a / b,
"%": lambda a, b: a % b,
"^": exponents_and_logs.pow,
"!": common.factorial
}
# Mapping between function ids and appropriate function calls.
# Remember that function names must only contain letters.
self.function_map = {
# Exponential and logarithmic functions
"sqrt": lambda a: exponents_and_logs.radical(a, 2),
"radical": exponents_and_logs.radical,
"root": exponents_and_logs.radical,
"pow": exponents_and_logs.pow,
"powTen": exponents_and_logs.pow_10,
"powPi": exponents_and_logs.pow_pi,
"powE": exponents_and_logs.pow_e,
"exp": exponents_and_logs.pow_e,
"ln": exponents_and_logs.ln,
"log": exponents_and_logs.log,
# Statistics functions
"mean": statistic.mean,
"mad": statistic.mad,
"std": statistic.std
}
# Mapping between trig function ids and appropriate function calls.
# Remember that function names must only contain letters.
self.trig_map = {
# Basic trigonometry functions
"sin": trigonometry.sin,
"cos": trigonometry.cos,
"tan": trigonometry.tan,
# Hyperbolic trigonometry functions
"sinh": trigonometry.sinh,
"cosh": trigonometry.cosh,
"tanh": trigonometry.tanh,
}
def evaluate_stack(self: object, symbol_stack: list) -> str:
"""Return result of expression represented by postfix stack of symbols.
Args:
symbol_stack (list): Postfix stack of mathematical symbols.
Returns:
str: Result of expression
"""
# Get current symbol.
symbol = symbol_stack.pop()
# If symbol is tuple, symbol is a function. Get function identifier and argument count.
if isinstance(symbol, tuple):
symbol, num_args = symbol
# Process unary '-'
if symbol == 'unary -':
return -self.evaluate_stack(symbol_stack)
# Process other unary operators
if symbol == "!":
operand = self.evaluate_stack(symbol_stack)
return self.operation_map[symbol](operand)
# Process binary operators
operation = self.operation_map.get(symbol, False)
if operation:
operand_2 = self.evaluate_stack(symbol_stack)
operand_1 = self.evaluate_stack(symbol_stack)
return operation(operand_1, operand_2)
# Process function calls
operation = self.function_map.get(symbol, False)
if operation:
# Begin by getting arguments.
# Note that arguments are pushed onto tack in reverse order.
args = reversed([self.evaluate_stack(symbol_stack)
for _ in range(num_args)])
return operation(*args)
# Process trig function calls
operation = self.trig_map.get(symbol, False)
if operation:
# Begin by getting arguments.
# Note that arguments are pushed onto tack in reverse order.
args = reversed([self.evaluate_stack(symbol_stack)
for _ in range(num_args)])
return trigonometry.process_angle_mode(*args, self._is_rad, operation)
# Process constants.
constant = self.constant_map.get(symbol, False)
if constant:
return constant
# If symbol is not an operation or a constant, the symbol must be a numeral.
if self._is_binary:
return display.binary_to_decimal(symbol)
# Try casting to an int.
try:
return int(symbol)
except ValueError:
pass
# Try casting to a float.
try:
return float(symbol)
except ValueError:
pass
# If none of the casts worked, we have some unrecognized symbol. Raise an exception.
raise Exception("{0} is not a recognized symbol.".format(symbol))
def evaluate(self: object, expression: str) -> float:
"""Return result of expression passed as string.
Args:
expression (str): Expression to evaluate.
Returns:
str: Result of expression
"""
# Reset parser state
self._symbol_stack = []
# Try to evaluate expression.
try:
# Make postfix notation of infix notation.
_ = self.bnf.parseString(expression, True)
# Evaluate generated stack of postfix symbols.
return self.evaluate_stack(self._symbol_stack[:])
# Return appropriate message if error was encountered. Sometimes, we might want to print additional information to console.
except exceptions.InputError as e:
raise Exception("Input Error: " + e.message)
except ZeroDivisionError:
raise Exception("Arithmetic Error: Division by 0 is undefined.")
except OverflowError:
raise Exception("Error: Overflow error occured.")
except ParseException:
# Not using error message because it's unintuitive to non-technical users.
raise Exception("Parsing Error: Invalid input entered.")
except Exception as e:
raise Exception("Evaluation Error: " + str(e))