Skip to content
Empty file added final_task/pycalc/__init__.py
Empty file.
73 changes: 73 additions & 0 deletions final_task/pycalc/evaluator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from pycalc.operators import Operator, Function, Constant
from pycalc.parser import Parser
from pycalc.importmodules import FunctionParser
from pycalc.validator import Validator
from inspect import getfullargspec


def infix_to_postfix(parsed_exp):
stack = []
postfix_list = []
for token in parsed_exp:
if isinstance(token, Operator) or isinstance(token, Function):
if token.name == '(':
stack.append(token)
elif token.name == ')':
while stack and stack[-1].name != '(':
postfix_list.append(stack.pop())
if stack:
stack.pop()
else:
if not token.associativity == 1:
while stack and token.priority < stack[-1].priority:
postfix_list.append(stack.pop())
else:
while stack and token.priority <= stack[-1].priority:
postfix_list.append(stack.pop())
stack.append(token)
elif isinstance(token, Function):
stack.append(token)
elif isinstance(token, Constant):
postfix_list.append(token)
elif Parser.is_number(token):
postfix_list.append(token)
else:
raise ValueError(f'name {token} is not defined')
while stack:
postfix_list.append(stack.pop())
return postfix_list


def calculate(exp):
stack = []
parser = Parser()
parsed_exp = parser.parse_expression(exp)
polish = infix_to_postfix(parsed_exp)
if all(isinstance(token, Operator) for token in polish):
raise ValueError('not valid input')
for token in polish:
if isinstance(token, Operator) or isinstance(token, Function) or isinstance(token, Constant):
if isinstance(token, Function) and len(polish) == 1:
stack.append(token.func())
elif isinstance(token, Function):
x = stack.pop()
if type(x) is list:
res = token.func(*x)
else:
res = token.func(*[x])
stack.append(res)
elif isinstance(token, Constant):
stack.append(token.func)
elif not token.is_binary:
x = stack.pop()
stack.append(token.func(x))
else:
try:
y, x = stack.pop(), stack.pop()
stack.append(token.func(x, y))
except Exception as e:
raise ValueError(f' binary operation must have two operands {e}')

else:
stack.append(float(token))
return stack[0]
27 changes: 27 additions & 0 deletions final_task/pycalc/importmodules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import importlib
from pycalc.operators import Function, Constant


class FunctionParser:
functions_dict = {}
constants_dict = {}

def __init__(self):
self.parse_modules(['math'])
self.functions_dict['pow'] = Function(object, 6, 1, True, pow)
self.functions_dict['abs'] = Function(object, 6, 1, True, abs)
self.functions_dict['round'] = Function(object, 6, 1, True, round)

def parse_modules(self, modules):
''' Method that parse module names array and add to dictionary their name as a key and
callable object as a value.
:param modules: Array of modules names.
'''
for module in modules:
modul = importlib.import_module(module)
for object in vars(modul):
if object[0:2] != '__':
if isinstance(vars(modul)[object], (int, float, complex)):
self.constants_dict[object] = Constant(object, 6, 1, True, vars(modul)[object])
else:
self.functions_dict[object] = Function(object, 6, 1, True, vars(modul)[object])
47 changes: 47 additions & 0 deletions final_task/pycalc/operators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
class Operator:
def __init__(self, name, priority, associativity, is_binary, func):
self.priority = priority
self.associativity = associativity
self.func = func
self.name = name
self.is_binary = is_binary


class Function:
def __init__(self, name, priority, associativity, is_binary, func):
self.priority = priority
self.associativity = associativity
self.func = func
self.name = name
self.is_binary = is_binary


class Constant:
def __init__(self, name, priority, associativity, is_binary, func):
self.priority = priority
self.associativity = associativity
self.func = func
self.name = name
self.is_binary = is_binary


operators_dict = {
'>=': Operator('>=', 0, 1, True, lambda x, y: x >= y),
'<=': Operator('<=', 0, 1, True, lambda x, y: x <= y),
'==': Operator('==', 0, 1, True, lambda x, y: x == y),
'!=': Operator('!=', 0, 1, True, lambda x, y: x != y),
'>': Operator('>', 0, 1, True, lambda x, y: x > y),
'<': Operator('<', 0, 1, True, lambda x, y: x >= y),
',': Operator(',', 1, 1, True, lambda x, y: [x, y]),
'+': Operator('+', 2, 1, True, lambda x, y: x+y),
'-': Operator('-', 2, 1, True, lambda x, y: x-y),
')': Operator(')', -1, 1, False, None),
'(': Operator('(', -1, 1, False, None),
'*': Operator('*', 3, 1, True, lambda x, y: x*y),
'/': Operator('/', 3, 1, True, lambda x, y: x/y),
'%': Operator('%', 3, 1, True, lambda x, y: x % y),
'//': Operator('//', 3, 1, True, lambda x, y: x // y),
'unary_minus': Operator('unary_minus', 5, 1, False, lambda x: -x),
'unary_plus': Operator('unary_plus', 5, 1, False, lambda x: x),
'^': Operator('^', 4, 2, True, lambda x, y: x**y),
}
115 changes: 115 additions & 0 deletions final_task/pycalc/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from pycalc.operators import operators_dict, Operator, Function, Constant
from pycalc.validator import Validator
from pycalc.importmodules import FunctionParser
functions_dict = {}
const_dict = {}


class Parser:
def __init__(self):
self.func_parser = FunctionParser()

@staticmethod
def is_number(s):
""" Returns True is string is a number. """
if isinstance(s, Operator) or isinstance(s, Function) or isinstance(s, Constant) or s == 'pow':
return False
return s.replace('.', '', 1).isdigit()

@staticmethod
def is_operator(s):
return s in operators_dict

@staticmethod
def is_function(s):
return s in FunctionParser.functions_dict

@staticmethod
def is_constant(s):
return s in FunctionParser.constants_dict

@staticmethod
def add_multiply_sign(lexem_list):
for i in range(1, len(lexem_list)):
if isinstance(lexem_list[i], Function) and not isinstance(lexem_list[i-1], Operator):
lexem_list.insert(i, operators_dict['*'])
elif (isinstance(lexem_list[i], Function) and isinstance(lexem_list[i-1], Operator) and
lexem_list[i-1].name == ')'):
lexem_list.insert(i, operators_dict['*'])
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])):
lexem_list.insert(i, operators_dict['*'])
elif (isinstance(lexem_list[i], Operator) and lexem_list[i].name == '(' and
isinstance(lexem_list[i-1], Operator) and lexem_list[i-1].name == ')'):
lexem_list.insert(i, operators_dict['*'])
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)):
lexem_list.insert(i, operators_dict['*'])
elif isinstance(lexem_list[i], Constant) and isinstance(lexem_list[i-1], Constant):
lexem_list.insert(i, operators_dict['*'])
return lexem_list

def parse_expression(self, exp):
exp = Validator.pre_tokinaze(exp)
exp.replace(" ", "")
lexem_array = []
start_index = 0
end_index = len(exp)
while start_index != len(exp):
substring = exp[start_index:end_index]
if Parser.is_number(substring):
lexem_array.append(substring)
start_index, end_index = end_index, len(exp)
elif Parser.is_operator(substring):
operator = operators_dict[substring]
lexem_array.append(operator)

start_index, end_index = end_index, len(exp)
elif Parser.is_constant(substring):
lexem_array.append(self.func_parser.constants_dict[substring])
start_index, end_index = end_index, len(exp)
elif Parser.is_function(substring):
lexem_array.append(self.func_parser.functions_dict[substring])
start_index, end_index = end_index, len(exp)
else:
end_index -= 1
lex_list = Parser.add_multiply_sign(lexem_array)
unary_signs = Parser.find_unary_signs(lex_list)
final_lexem_list = Parser.remove_redundant_unary_signs(unary_signs, lex_list)

return final_lexem_list

@staticmethod
def find_unary_signs(lexem_list):
final_list = []
for i in range(len(lexem_list)):
if i == 0 and isinstance(lexem_list[i], Operator) and lexem_list[i].name in ['+', '-']:
final_list.append(0)
elif ((isinstance(lexem_list[i], Operator) and lexem_list[i].name in ['+', '-']) and not
(isinstance(lexem_list[i-1], Constant) or Parser.is_number(lexem_list[i-1])) and not
(isinstance(lexem_list[i-1], Operator) and lexem_list[i-1].name == ')')):
final_list.append(i)
lexems_with_indicies = enumerate(lexem_list)
lexems_filter = list(filter(lambda x: x[0] in final_list, lexems_with_indicies))
for unary_sign in lexems_filter:
if unary_sign[1] == '+':
lexem_list[unary_sign[0]] = operators_dict['unary_plus']
else:
lexem_list[unary_sign[0]] = operators_dict['unary_minus']
return lexems_filter

@staticmethod
def remove_redundant_unary_signs(lexems_with_indicies, lex_list):
final_index = len(lexems_with_indicies)-1
while final_index != -1:
last_index, last_sign = lexems_with_indicies[final_index]
prev_index, prev_sign = lexems_with_indicies[final_index - 1]
if last_index - 1 == prev_index and last_sign.name == prev_sign.name:
lex_list[prev_index:last_index + 1] = [operators_dict['unary_plus']]
lexems_with_indicies[final_index - 1: final_index + 1] = [(prev_index, operators_dict['+'])]
elif last_index - 1 == prev_index and last_sign != prev_sign:
lex_list[prev_index: last_index + 1] = [operators_dict['unary_minus']]
lexems_with_indicies[final_index - 1: final_index + 1] = [(prev_index, operators_dict['-'])]
final_index -= 1
return lex_list
30 changes: 30 additions & 0 deletions final_task/pycalc/pycalc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import argparse
import sys
from pycalc.importmodules import FunctionParser
from pycalc.evaluator import calculate


def get_args():
'''This function parses and return arguments passed in'''
parser = argparse.ArgumentParser(
description='Script retrieves schedules from a given server')
parser.add_argument(
'expression', help='')

parser.add_argument(
'-m', '--use-modules', nargs='+', help='', required=False)
return parser.parse_args()


def main():
try:
args = get_args()
parser = FunctionParser()
if args.use_modules:
parser.parse_modules(args.use_modules)
parser.parse_modules(['time'])
result = calculate(args.expression)
print(f'{result}')
except Exception as e:
print(f"ERROR: {e}")
sys.exit(1)
63 changes: 63 additions & 0 deletions final_task/pycalc/validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import re


class Validator:
sign_arr = ['<', '>', '=', '!', '/']

@staticmethod
def normalize_string(str):
''' Method that normalize string with expression. If we have more than one space between symbol,
it change multiply spaces with one space.
:param str: String with a math expression.
:return : Normalized string with a math expression.
'''
return re.sub(r'\s+', ' ', str).strip()

@staticmethod
def pre_tokinaze(str):
''' Method that do a number of operations before tokenization.
:param str: String with a math expression.
:return : Amended string with a math expression.
'''
str.lower()
if Validator.par_check(str):
normalize_str = Validator.normalize_string(str)
valid_string = Validator.validate_string(normalize_str).replace(" ", "")
return valid_string
else:
raise ValueError('Brackets not balanced')

@staticmethod
def par_check(expression):
''' Method that check for validity of brackets.
:param expression: String with math expression.
:return : True or False, depends on validity of brackets of a given expression.
'''
mapping = dict(zip('({[', ')}]'))
queue = []
for letter in expression:
if letter in mapping:
queue.append(mapping[letter])
elif letter not in mapping.values():
continue
elif not (queue and letter == queue.pop()):
return False
return not queue

@staticmethod
def validate_string(str):
''' Method that raise error if string with a math expression is not valid.
:param str: String with a math expression.
:return : string with a math expression if it is valid.
'''
indices = enumerate(str)
for i, char in indices:
if char in Validator.sign_arr:
if str[i + 1] == ' ' and str[i + 2] == '=':
raise ValueError('invalid syntax')
elif char == '/' and str[i + 1] == ' ' and str[i + 2] == '/':
raise ValueError('invalid syntax')
elif char.isdigit() and i != len(str) - 1:
if str[i + 1] == ' ' and str[i + 2].isdigit():
raise ValueError('invalid syntax')
return str
13 changes: 13 additions & 0 deletions final_task/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from setuptools import setup, find_packages

setup(
name='pycalc',
version='1.0',
packages=find_packages(),
__version__='1.0',
entry_points={
'console_scripts': [
'pycalc = pycalc.pycalc:main'
]
},
)
Empty file added final_task/tests/__init__.py
Empty file.
Loading