-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodel.py
More file actions
370 lines (308 loc) · 18.4 KB
/
model.py
File metadata and controls
370 lines (308 loc) · 18.4 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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
import math
import numpy as np
class CalculatorException(Exception):
pass
class CalculatorToken:
priority_dict = {"+": 1, "-": 1, "%": 4, "/": 2, "*": 2, "^": 3, "~": 4}
def __init__(self, t_type, t_value):
self.t_type = t_type
self.t_value = t_value
if t_type == "Operator":
self.t_priority = self.priority_dict[t_value]
def __eq__(self, other):
return self.t_type == other.t_type and self.t_value == other.t_value
def save_number_buff(tokenized_input: list, number_buff: str, add_mult=False) -> tuple[list, str]:
"""
Function to save number buffer to tokenized_input
:param tokenized_input: list of tokenized expression
:param number_buff: number buffer
:param add_mult: flag to add mult operator
:return: tuple with updated tokenized_input and cleaned number buffer
"""
try:
float(number_buff)
except ValueError:
raise CalculatorException(f"Некорректное число {number_buff} !")
tokenized_input.append(CalculatorToken("Digit", number_buff))
if add_mult:
tokenized_input.append(CalculatorToken("Operator", "*"))
return tokenized_input, ""
def save_letter_buff(tokenized_input: list, letter_buff: str) -> tuple[list, str]:
"""
Function to save letter buffer to tokenized_input
:param tokenized_input: list of tokenized expression
:param letter_buff: letter buffer
:return: tuple with updated tokenized_input and cleaned letter buffer
"""
tokenized_input.append(CalculatorToken("Function", letter_buff))
return tokenized_input, ""
def save_constants(tokenized_input: list, letter_buff: str) -> tuple[list, str] | CalculatorException:
"""
Function to save constants in letter buffer to tokenized_input
:param tokenized_input: list of tokenized expression
:param letter_buff: letter buffer
:return: tuple with updated tokenized_input and cleaned letter buffer
"""
if letter_buff == "pi" or letter_buff == "π":
tokenized_input.append(CalculatorToken("Digit", np.pi))
elif letter_buff == "e":
tokenized_input.append(CalculatorToken("Digit", np.e))
else:
raise CalculatorException(f"Некорректное значение: {letter_buff}")
return tokenized_input, ""
def factorial_check(a):
"""
Function to check if factorial parameter is an integer
:param a: factorial parameter
:return: factorial value of parameter or CalculatorException
"""
if a.is_integer():
return math.factorial(int(a))
else:
raise CalculatorException("Невозможно вычислить факториал нецелого числа!")
class Model:
def __init__(self):
pass
def evaluate(self, expression: list[CalculatorToken]):
"""
Function to evaluate expression in Reverse Polish Notation (RPN)
Algorithm taken from https://www.geeksforgeeks.org/evaluation-of-postfix-expression/?ysclid=m3a2cblkdp275675497
:param expression: list with CalculatorTokens sorted in RPN
:return: value of evaluated expression
"""
stack = []
unary_dict = {"~": lambda a: -a, "sin": lambda a: np.sin(a), "cos": lambda a: np.cos(a),
"tan": lambda a: np.tan(a), "cot": lambda a: 1 / np.tan(a), "sqrt": lambda a: math.sqrt(a),
"ln": lambda a: math.log(a), "fact": factorial_check,
"!": factorial_check}
binary_dict = {"+": 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, "log": lambda a, b: math.log(b, a),
"min": lambda a, b: min(a, b), "max": lambda a, b: max(a, b), "%": lambda a, b: a % b}
# Функции с фиксированным количеством аргументов
function_arg_count = {
"log": 2, "min": 2, "max": 2,
"sin": 1, "cos": 1, "tan": 1, "cot": 1, "sqrt": 1, "ln": 1, "fact": 1, "!": 1
}
for token in expression:
if token.t_type == "Digit":
# token is a digit -> push to stack
stack.append(float(token.t_value))
else:
if token.t_value in binary_dict:
# token is a binary operator or function
if len(stack) < 2:
raise CalculatorException(
f"Недостаточно операндов для оператора {token.t_value}!")
a = stack.pop()
b = stack.pop()
try:
stack.append(binary_dict[token.t_value](b, a))
except Exception as err:
raise CalculatorException(f"Ошибка во время вычисления {token.t_value} !")
elif token.t_value in unary_dict:
# token is a unary operator or function
if len(stack) < 1:
raise CalculatorException(
f"Недостаточно операндов для функции {token.t_value}!")
try:
stack.append(unary_dict[token.t_value](stack.pop()))
except Exception as err:
raise CalculatorException(f"Ошибка во время вычисления {token.t_value} !")
else:
raise CalculatorException(f"Неизвестный оператор или функция: {token.t_value}")
if len(stack) != 1:
raise CalculatorException("Некорректное выражение!")
return stack.pop()
def input_tokenize(self, input_string: str) -> list:
"""
Function to parse and tokenize input expression for CalculatorToken objects
Algorithm taken from https://proglib.io/p/math-expression-tokenizer?ysclid=m3a11zy0gv176555469
:param input_string: expression inputted
:return: list of CalculatorToken objects
"""
tokenized_input = []
operators = ["+", "-", "*", "/", "%", "^"]
# Проверка на пустую строку
if not input_string.strip():
raise CalculatorException("Пустое выражение!")
number_buffer = ""
letter_buffer = ""
input_clean = input_string.replace(" ", "")
try:
for i, symbol in enumerate(input_clean):
if symbol.isdigit():
# if symbol is a digit -> send symbol to number buffer
number_buffer += symbol
elif symbol.isalpha():
# if symbol is a letter -> save number buffer and send symbol to letter buffer
if len(number_buffer) > 0:
tokenized_input, number_buffer = save_number_buff(tokenized_input, number_buffer, add_mult=True)
letter_buffer += symbol
elif symbol == "!":
# checking for postfix factorial function
if len(number_buffer) > 0:
tokenized_input, number_buffer = save_number_buff(tokenized_input, number_buffer)
if len(letter_buffer) > 0:
tokenized_input, letter_buffer = save_letter_buff(tokenized_input, letter_buffer)
tokenized_input.append(CalculatorToken("Function", symbol))
elif symbol in operators:
# if symbol is an operator -> save number buffer and check for constants in letter buffer
if len(number_buffer) > 0:
tokenized_input, number_buffer = save_number_buff(tokenized_input, number_buffer)
if len(letter_buffer) > 0:
tokenized_input, letter_buffer = save_constants(tokenized_input, letter_buffer)
# Проверка на множественные операторы (кроме унарного минуса в начале)
if i > 0 and input_clean[i - 1] in operators + ["("] and symbol != "-":
raise CalculatorException(f"Некорректная последовательность операторов!")
if symbol == "-" and i > 0 and input_clean[i - 1] == "-":
raise CalculatorException("Некорректная последовательность операторов!")
# checking if minus is unary minus -> if it is a first char or previous token is operator or OpenBracket
if symbol == "-" and (i == 0 or (i > 0 and input_clean[i - 1] in operators + ["("])):
symbol = "~"
tokenized_input.append(CalculatorToken("Operator", symbol))
elif symbol == "(":
# if symbol is a OpenBracket -> save letter buffer and number buffer
# Проверка на неявное умножение: число( или )( или функция(
if len(tokenized_input) > 0:
last_token = tokenized_input[-1]
if (last_token.t_type == "Digit" or
last_token.t_type == "CloseBracket" or
(last_token.t_type == "Function" and last_token.t_value != "!")):
tokenized_input.append(CalculatorToken("Operator", "*"))
if len(letter_buffer) > 0:
tokenized_input, letter_buffer = save_letter_buff(tokenized_input, letter_buffer)
elif len(number_buffer) > 0:
tokenized_input, number_buffer = save_number_buff(tokenized_input, number_buffer, add_mult=True)
# Проверка на некорректные последовательности типа "(,"
if i > 0 and input_clean[i - 1] == ",":
raise CalculatorException("Некорректное использование запятой перед открывающей скобкой!")
tokenized_input.append(CalculatorToken("OpenBracket", symbol))
elif symbol == ")":
# if symbol is a CloseBracket -> save number buffer and check for constants in letter buffer
if len(number_buffer) > 0:
tokenized_input, number_buffer = save_number_buff(tokenized_input, number_buffer)
if len(letter_buffer) > 0:
tokenized_input, letter_buffer = save_constants(tokenized_input, letter_buffer)
# Проверка на пустые скобки ()
if (len(tokenized_input) > 0 and tokenized_input[-1].t_type == "OpenBracket"):
raise CalculatorException("Пустые скобки!")
tokenized_input.append(CalculatorToken("CloseBracket", symbol))
# Проверка на неявное умножение: )( или )число
if i < len(input_clean) - 1 and (input_clean[i + 1].isdigit() or
input_clean[i + 1] == "(" or
input_clean[i + 1].isalpha()):
tokenized_input.append(CalculatorToken("Operator", "*"))
elif symbol == ".":
# if symbol is a decimal point -> send symbol to number buffer
# Проверка на множественные точки в числе
if "." in number_buffer:
raise CalculatorException(f"Некорректное число!")
number_buffer += "."
elif symbol == ",":
# if symbol is a delimiter -> save number buffer and check for constants in letter buffer
if len(number_buffer) > 0:
tokenized_input, number_buffer = save_number_buff(tokenized_input, number_buffer)
if len(letter_buffer) > 0:
tokenized_input, letter_buffer = save_constants(tokenized_input, letter_buffer)
# Проверка на некорректное использование запятой
if (i == 0 or i == len(input_clean) - 1 or
input_clean[i - 1] in operators + ["("] or
input_clean[i + 1] in operators + [")", ","]):
raise CalculatorException("Некорректное использование запятой!")
tokenized_input.append(CalculatorToken("Separator", symbol))
else:
raise CalculatorException(f'Некорректный символ в выражении - {symbol}')
# after-saving number buffer
if len(number_buffer) > 0:
tokenized_input, number_buffer = save_number_buff(tokenized_input, number_buffer)
# after-saving letter buffer
if len(letter_buffer) > 0:
tokenized_input, letter_buffer = save_constants(tokenized_input, letter_buffer)
# Проверка на некорректное окончание выражения
if (len(tokenized_input) > 0 and
tokenized_input[-1].t_type in ["Operator", "Separator"]):
raise CalculatorException("Выражение не может заканчиваться оператором или разделителем!")
except CalculatorException as e:
raise CalculatorException(f'Ошибка во время токенизации: {e}')
return tokenized_input
def sort_machine_algo(self, parsed_tokens: list[CalculatorToken]):
"""
Realizationg of Shunting yard algorithm
Taken from https://en.wikipedia.org/wiki/Shunting_yard_algorithm (without separators)
and https://habr.com/ru/articles/777368/ (with separators)
:param parsed_tokens: CalculatorTokens of inputted expression
:return: list with CalculatorTokens in a postfix notation string, also known as reverse Polish notation (RPN)
"""
queue = []
stack = []
# Счетчик скобок для проверки сбалансированности
bracket_count = 0
for token in parsed_tokens:
if token.t_type == "OpenBracket":
bracket_count += 1
elif token.t_type == "CloseBracket":
bracket_count -= 1
if bracket_count < 0:
raise CalculatorException("В выражении не согласованы скобки!")
if bracket_count != 0:
raise CalculatorException("В выражении не согласованы скобки!")
for token in parsed_tokens:
match token.t_type:
case "Digit":
# token is a digit -> put it into the output queue
queue.append(token)
case "Function":
# token is a function -> push it onto the operator stack
if token.t_value == "!":
# handling postfix factorial function
queue.append(token)
else:
stack.append(token)
case "OpenBracket":
# token in an OpenBracket -> push it onto the operator stack
stack.append(token)
case "CloseBracket":
# token in an CloseBracket
try:
# while the operator at the top of the operator stack is not a OpenBracket
while stack[-1].t_type != "OpenBracket":
# pop the operator from the operator stack into the output queue
queue.append(stack.pop())
else:
# pop the OpenBracket from the operator stack and discard it
stack.pop()
if len(stack) > 0 and stack[-1].t_type == "Function":
# if there is a function token at the top of the operator stack ->
# pop the function from the operator stack into the output queue
queue.append(stack.pop())
except IndexError as err:
raise CalculatorException(
f"В выражении либо неверно поставлен разделитель, либо не согласованы скобки!")
case "Separator":
# while the operator at the top of the operator stack is not a OpenBracket
try:
while stack[-1].t_type != "OpenBracket":
# pop the operator from the operator stack into the output queue
queue.append(stack.pop())
except IndexError as err:
raise CalculatorException(
f"Проблема с разделителем - вероятно, использована десятичная запятая вместо точки!")
case "Operator":
# while there is an operator o2 at the top of the operator stack
# and o2 has same or greater precedence than o1
while (len(stack) > 0 and stack[-1].t_type == "Operator" and
stack[-1].t_priority >= token.t_priority):
# pop o2 from the operator stack into the output queue
queue.append(stack.pop())
# push o1 onto the operator stack
stack.append(token)
while len(stack) > 0:
# while there are tokens on the operator stack
temp = stack.pop()
if temp.t_type == "OpenBracket" or temp.t_type == "CloseBracket":
# there are mismatched parentheses
raise CalculatorException("В выражении не согласованы скобки")
else:
# pop the operator from the operator stack onto the output queue
queue.append(temp)
return queue