Skip to content

Commit 43d9993

Browse files
daniildavydziklenashanchuk
authored andcommitted
Added pure-python pycalc implementation and unit tests
1 parent 17385ed commit 43d9993

File tree

9 files changed

+427
-0
lines changed

9 files changed

+427
-0
lines changed

final_task/pycalc/__init__.py

Whitespace-only changes.

final_task/pycalc/evaluator.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from pycalc.operators import Operator, Function, Constant
2+
from pycalc.parser import Parser
3+
from pycalc.importmodules import FunctionParser
4+
from pycalc.validator import Validator
5+
6+
7+
def infix_to_postfix(parsed_exp):
8+
stack = []
9+
postfix_list = []
10+
for token in parsed_exp:
11+
if isinstance(token, Operator) or isinstance(token, Function):
12+
if token.name == '(':
13+
stack.append(token)
14+
elif token.name == ')':
15+
while stack and stack[-1].name != '(':
16+
postfix_list.append(stack.pop())
17+
if stack:
18+
stack.pop()
19+
else:
20+
if not token.associativity == 1:
21+
while stack and token.priority < stack[-1].priority:
22+
postfix_list.append(stack.pop())
23+
else:
24+
while stack and token.priority <= stack[-1].priority:
25+
postfix_list.append(stack.pop())
26+
stack.append(token)
27+
elif isinstance(token, Function):
28+
stack.append(token)
29+
elif isinstance(token, Constant):
30+
postfix_list.append(token)
31+
elif Parser.is_number(token):
32+
postfix_list.append(token)
33+
else:
34+
raise ValueError(f'name {token} is not defined')
35+
while stack:
36+
postfix_list.append(stack.pop())
37+
return postfix_list
38+
39+
40+
def calculate(exp):
41+
stack = []
42+
parser = Parser()
43+
parsed_exp = parser.parse_expression(exp)
44+
polish = infix_to_postfix(parsed_exp)
45+
if all(isinstance(token, Operator) for token in polish):
46+
raise ValueError('not valid input')
47+
for token in polish:
48+
if isinstance(token, Operator) or isinstance(token, Function) or isinstance(token, Constant):
49+
if isinstance(token, Function) and len(polish) == 1:
50+
stack.append(token.func())
51+
elif isinstance(token, Function):
52+
x = stack.pop()
53+
if type(x) is list:
54+
res = token.func(*x)
55+
else:
56+
res = token.func(*[x])
57+
stack.append(res)
58+
elif isinstance(token, Constant):
59+
stack.append(token.func)
60+
elif not token.is_binary:
61+
x = stack.pop()
62+
stack.append(token.func(x))
63+
else:
64+
try:
65+
y, x = stack.pop(), stack.pop()
66+
stack.append(token.func(x, y))
67+
except Exception as e:
68+
raise ValueError(f' binary operation must have two operands {e}')
69+
70+
else:
71+
stack.append(float(token))
72+
return stack[0]

final_task/pycalc/importmodules.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import importlib
2+
from pycalc.operators import Function, Constant
3+
4+
5+
class FunctionParser:
6+
functions_dict = {}
7+
constants_dict = {}
8+
9+
def __init__(self):
10+
self.parse_modules(['math'])
11+
self.functions_dict['pow'] = Function(object, 6, 1, True, pow)
12+
self.functions_dict['abs'] = Function(object, 6, 1, True, abs)
13+
self.functions_dict['round'] = Function(object, 6, 1, True, round)
14+
15+
def parse_modules(self, modules):
16+
''' Method that parse module names array and add to dictionary their name as a key and
17+
callable object as a value.
18+
:param modules: Array of modules names.
19+
'''
20+
for module in modules:
21+
modul = importlib.import_module(module)
22+
for object in vars(modul):
23+
if object[0:2] != '__':
24+
if isinstance(vars(modul)[object], (int, float, complex)):
25+
self.constants_dict[object] = Constant(object, 6, 1, True, vars(modul)[object])
26+
else:
27+
self.functions_dict[object] = Function(object, 6, 1, True, vars(modul)[object])

final_task/pycalc/operators.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
class Operator:
2+
def __init__(self, name, priority, associativity, is_binary, func):
3+
self.priority = priority
4+
self.associativity = associativity
5+
self.func = func
6+
self.name = name
7+
self.is_binary = is_binary
8+
9+
10+
class Function:
11+
def __init__(self, name, priority, associativity, is_binary, func):
12+
self.priority = priority
13+
self.associativity = associativity
14+
self.func = func
15+
self.name = name
16+
self.is_binary = is_binary
17+
18+
19+
class Constant:
20+
def __init__(self, name, priority, associativity, is_binary, func):
21+
self.priority = priority
22+
self.associativity = associativity
23+
self.func = func
24+
self.name = name
25+
self.is_binary = is_binary
26+
27+
28+
operators_dict = {
29+
'>=': Operator('>=', 0, 1, True, lambda x, y: x >= y),
30+
'<=': Operator('<=', 0, 1, True, lambda x, y: x <= y),
31+
'==': Operator('==', 0, 1, True, lambda x, y: x == y),
32+
'!=': Operator('!=', 0, 1, True, lambda x, y: x != y),
33+
'>': Operator('>', 0, 1, True, lambda x, y: x > y),
34+
'<': Operator('<', 0, 1, True, lambda x, y: x >= y),
35+
',': Operator(',', 1, 1, True, lambda x, y: [x, y]),
36+
'+': Operator('+', 2, 1, True, lambda x, y: x+y),
37+
'-': Operator('-', 2, 1, True, lambda x, y: x-y),
38+
')': Operator(')', -1, 1, False, None),
39+
'(': Operator('(', -1, 1, False, None),
40+
'*': Operator('*', 3, 1, True, lambda x, y: x*y),
41+
'/': Operator('/', 3, 1, True, lambda x, y: x/y),
42+
'%': Operator('%', 3, 1, True, lambda x, y: x % y),
43+
'//': Operator('//',3, 1,True, lambda x, y: x // y),
44+
'unary_minus': Operator('unary_minus', 4, 1, False, lambda x: -x),
45+
'unary_plus': Operator('unary_plus', 4, 1, False, lambda x: x),
46+
'^': Operator('^', 5, 2, True, lambda x, y: x**y),
47+
}
48+

final_task/pycalc/parser.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from pycalc.operators import operators_dict, Operator, Function, Constant
2+
from pycalc.validator import Validator
3+
from pycalc.importmodules import FunctionParser
4+
functions_dict = {}
5+
const_dict = {}
6+
7+
8+
class Parser:
9+
def __init__(self):
10+
self.func_parser = FunctionParser()
11+
12+
@staticmethod
13+
def is_number(s):
14+
""" Returns True is string is a number. """
15+
if isinstance(s, Operator) or isinstance(s, Function) or isinstance(s, Constant) or s == 'pow':
16+
return False
17+
return s.replace('.', '', 1).isdigit()
18+
19+
@staticmethod
20+
def is_operator(s):
21+
return s in operators_dict
22+
23+
@staticmethod
24+
def is_function(s):
25+
return s in FunctionParser.functions_dict
26+
27+
@staticmethod
28+
def is_constant(s):
29+
return s in FunctionParser.constants_dict
30+
31+
@staticmethod
32+
def add_multiply_sign(lexem_list):
33+
for i in range(1, len(lexem_list)):
34+
if isinstance(lexem_list[i], Function) and not isinstance(lexem_list[i-1], Operator):
35+
lexem_list.insert(i, operators_dict['*'])
36+
elif isinstance(lexem_list[i], Function) and isinstance(lexem_list[i-1], Operator) and lexem_list[i-1].name == ')':
37+
lexem_list.insert(i, operators_dict['*'])
38+
elif isinstance(lexem_list[i], Operator) and lexem_list[i].name == '(' and (isinstance(lexem_list[i-1], Constant) or Parser.is_number(lexem_list[i-1])):
39+
lexem_list.insert(i, operators_dict['*'])
40+
elif isinstance(lexem_list[i], Operator) and lexem_list[i].name == '(' and isinstance(lexem_list[i-1], Operator) and lexem_list[i-1].name == ')':
41+
lexem_list.insert(i, operators_dict['*'])
42+
elif isinstance(lexem_list[i], Operator) and lexem_list[i].name == '(' and not isinstance(lexem_list[i-1], Operator) and not isinstance(lexem_list[i-1], Function):
43+
lexem_list.insert(i, operators_dict['*'])
44+
return lexem_list
45+
46+
def parse_expression(self, exp):
47+
exp = Validator.pre_tokinaze(exp)
48+
exp.replace(" ", "")
49+
lexem_array = []
50+
start_index = 0
51+
end_index = len(exp)
52+
while start_index != len(exp):
53+
substring = exp[start_index:end_index]
54+
if Parser.is_number(substring):
55+
lexem_array.append(substring)
56+
start_index, end_index = end_index, len(exp)
57+
elif Parser.is_operator(substring):
58+
operator = operators_dict[substring]
59+
lexem_array.append(operator)
60+
61+
start_index, end_index = end_index, len(exp)
62+
elif Parser.is_constant(substring):
63+
lexem_array.append(self.func_parser.constants_dict[substring])
64+
start_index, end_index = end_index, len(exp)
65+
elif Parser.is_function(substring):
66+
lexem_array.append(self.func_parser.functions_dict[substring])
67+
start_index, end_index = end_index, len(exp)
68+
else:
69+
end_index -= 1
70+
lex_list =Parser.add_multiply_sign(lexem_array)
71+
unary_signs = Parser.find_unary_signs(lex_list)
72+
final_lexem_list = Parser.remove_redundant_unary_signs(unary_signs, lex_list)
73+
74+
return final_lexem_list
75+
76+
@staticmethod
77+
def find_unary_signs(lexem_list):
78+
final_list = []
79+
for i in range(len(lexem_list)):
80+
if i == 0 and isinstance(lexem_list[i], Operator) and lexem_list[i].name in ['+', '-']:
81+
final_list.append(0)
82+
elif (isinstance(lexem_list[i], Operator) and lexem_list[i].name in ['+', '-']) and not (Parser.is_constant(lexem_list[i-1]) or Parser.is_number(lexem_list[i-1])) \
83+
and not (isinstance(lexem_list[i-1], Operator) and lexem_list[i-1].name == ')'):
84+
final_list.append(i)
85+
lexems_with_indicies = enumerate(lexem_list)
86+
lexems_filter = list(filter(lambda x: x[0] in final_list, lexems_with_indicies))
87+
for unary_sign in lexems_filter:
88+
lexem_list[unary_sign[0]] = operators_dict['unary_plus'] if unary_sign[1] == '+' else operators_dict['unary_minus']
89+
return lexems_filter
90+
91+
@staticmethod
92+
def remove_redundant_unary_signs(lexems_with_indicies, lex_list):
93+
final_index = len(lexems_with_indicies)-1
94+
while final_index != -1:
95+
last_index, last_sign = lexems_with_indicies[final_index]
96+
prev_index, prev_sign = lexems_with_indicies[final_index - 1]
97+
if last_index - 1 == prev_index and last_sign.name == prev_sign.name:
98+
lex_list[prev_index:last_index + 1] = [operators_dict['unary_plus']]
99+
lexems_with_indicies[final_index - 1: final_index + 1] = [(prev_index, operators_dict['+'])]
100+
elif last_index - 1 == prev_index and last_sign != prev_sign:
101+
lex_list[prev_index: last_index + 1] = [operators_dict['unary_minus']]
102+
lexems_with_indicies[final_index - 1: final_index + 1] = [(prev_index, operators_dict['-'])]
103+
final_index -= 1
104+
return lex_list
105+

final_task/pycalc/pycalc.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import argparse
2+
import sys
3+
from pycalc.importmodules import FunctionParser
4+
from pycalc.evaluator import calculate
5+
6+
7+
def get_args():
8+
'''This function parses and return arguments passed in'''
9+
parser = argparse.ArgumentParser(
10+
description='Script retrieves schedules from a given server')
11+
parser.add_argument(
12+
'expression', help='')
13+
14+
parser.add_argument(
15+
'-m', '--use-modules', nargs='+', help='', required=False)
16+
return parser.parse_args()
17+
18+
19+
def main():
20+
try:
21+
# args = get_args()
22+
# parser = FunctionParser()
23+
# if args.use_modules:
24+
# parser.parse_modules(args.use_modules)
25+
# parser.parse_modules(['time'])
26+
# result = calculate(args.expression)
27+
# print(calculate('time()'))
28+
# print(f'{result}')
29+
30+
31+
except Exception as e:
32+
print(f"ERROR: {e}")
33+
sys.exit(1)
34+

final_task/pycalc/validator.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import re
2+
3+
4+
class Validator:
5+
spaces_reg = '\s+'
6+
sign_arr = ['<', '>', '=', '!']
7+
8+
@staticmethod
9+
def normalize_string(str):
10+
''' Method that normalize string with expression. If we have more than one space between symbol,
11+
it change multiply spaces with one space.
12+
:param str: String with a math expression.
13+
:return : Normalized string with a math expression.
14+
'''
15+
return re.sub(Validator.spaces_reg, ' ', str).strip()
16+
17+
@staticmethod
18+
def pre_tokinaze(str):
19+
''' Method that do a number of operations before tokenization.
20+
:param str: String with a math expression.
21+
:return : Amended string with a math expression.
22+
'''
23+
str.lower()
24+
if Validator.par_check(str):
25+
normalize_str = Validator.normalize_string(str)
26+
valid_string = Validator.validate_string(normalize_str).replace(" ", "")
27+
return valid_string
28+
else:
29+
raise ValueError('Brackets not balanced')
30+
31+
@staticmethod
32+
def par_check(expression):
33+
''' Method that check for validity of brackets.
34+
:param expression: String with math expression.
35+
:return : True or False, depends on validity of brackets of a given expression.
36+
'''
37+
mapping = dict(zip('({[', ')}]'))
38+
queue = []
39+
for letter in expression:
40+
if letter in mapping:
41+
queue.append(mapping[letter])
42+
elif letter not in mapping.values():
43+
continue
44+
elif not (queue and letter == queue.pop()):
45+
return False
46+
return not queue
47+
48+
49+
@staticmethod
50+
def validate_string(str):
51+
''' Method that raise error if string with a math expression is not valid.
52+
:param str: String with a math expression.
53+
:return : string with a math expression if it is valid.
54+
'''
55+
indices = enumerate(str)
56+
for i, char in indices:
57+
if char in Validator.sign_arr:
58+
if str[i + 1] == ' ' and str[i + 2] == '=':
59+
raise ValueError('invalid syntax')
60+
elif char.isdigit() and i != len(str) - 1:
61+
if str[i + 1] == ' ' and str[i + 2].isdigit():
62+
raise ValueError('invalid syntax')
63+
64+
return str
65+
66+

final_task/tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)