Skip to content
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

Metelskiy Ivan #14

Open
wants to merge 43 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
60f1f56
add project files
DanoryHub Apr 14, 2019
a47ee67
solved some little issues
DanoryHub Apr 14, 2019
f7871af
push to fork
DanoryHub Apr 14, 2019
ee72bda
solve parse issues
DanoryHub Apr 14, 2019
f192457
solve log10 issue
DanoryHub Apr 14, 2019
636907d
add some comments
DanoryHub Apr 14, 2019
ff28467
a little code refactoring
DanoryHub Apr 14, 2019
e5431e8
add recursive function to pack math functions' parameters into lists
DanoryHub Apr 21, 2019
390dca1
gitignore
DanoryHub Apr 21, 2019
054e50e
gitignore
DanoryHub Apr 21, 2019
6ed0fc0
remove ide configuration files
DanoryHub Apr 21, 2019
53f55ac
add recursive part into polish notatin func
DanoryHub Apr 21, 2019
d5104ad
change expression calculation func to recursive calculation
DanoryHub Apr 21, 2019
a76e83e
fix some mistakes form mentor's comments
DanoryHub Apr 27, 2019
c2d6bd1
add comments to new functions, kinda remove bugs
DanoryHub Apr 28, 2019
7895f07
add setuptools and argparse
DanoryHub Apr 28, 2019
57dd907
fight with setup tools
DanoryHub Apr 28, 2019
c6612a2
little issue with sci notation check
DanoryHub Apr 28, 2019
7549c7e
error handling
DanoryHub Apr 28, 2019
113131b
oops forgot to remove test thing
DanoryHub Apr 28, 2019
55014af
and forgot to remove thing i dont need
DanoryHub Apr 28, 2019
0af8efe
very final changes(i hope...)
DanoryHub Apr 28, 2019
5123adc
super very final changes(float dont have len)
DanoryHub Apr 28, 2019
270bb4b
super very final changes(float dont have len)
DanoryHub Apr 28, 2019
8c636d6
solve negative func problem
DanoryHub Apr 28, 2019
d587c8b
add error handling func
DanoryHub Apr 29, 2019
5f759b5
add some error cases to error handling func
DanoryHub Apr 29, 2019
3ee0740
remove test thing
DanoryHub Apr 29, 2019
fc64eba
remove test thing
DanoryHub Apr 29, 2019
0e5d8f6
remove test thing
DanoryHub Apr 29, 2019
6a70980
add constants to error handling
DanoryHub Apr 29, 2019
fadddfb
solve multiple operations error
DanoryHub Apr 29, 2019
d5300d1
remove test thing
DanoryHub Apr 29, 2019
115090c
add tests file
DanoryHub Apr 29, 2019
9c69bba
a little change tests
DanoryHub Apr 29, 2019
1b0cdbd
remove imports i dont need
DanoryHub Apr 29, 2019
3e5a015
remove imports i dont need
DanoryHub Apr 29, 2019
f3ab1ec
remove imports i dont need
DanoryHub Apr 29, 2019
058e12d
some refactoring
DanoryHub Apr 29, 2019
0013b8a
remove imports
DanoryHub Apr 29, 2019
4b8584d
refactoring names of variables
DanoryHub Apr 29, 2019
bcd7395
add some comments
DanoryHub Apr 29, 2019
ff2ef4f
add typing library
DanoryHub May 10, 2019
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,6 @@ venv.bak/

# mypy
.mypy_cache/

#pycharm
.idea/
54 changes: 54 additions & 0 deletions final_task/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""

module which contains base constant values which uses for calculating expressions

"""

import re
import operator

operator = {
'+': operator.add,
'-': operator.sub,
'*': operator.mul,
'/': operator.truediv,
'%': operator.mod,
'^': operator.pow,
'//': operator.floordiv,
'==': operator.eq,
'<=': operator.le,
'>=': operator.ge,
'>': operator.gt,
'<': operator.lt,
'!=': operator.ne
}

lowest_priority_operator = '('
low_priority_operators = ('<', '==', '!=', '>=', '>')
mid_priority_operators = ('+', '-')
high_priority_operators = ('*', '/', '%', '//')
highest_priority_operator = '^'

LOWEST_PRIORITY = 0
LOW_PRIORITY = 1
MID_PRIORITY = 2
HIGH_PRIORITY = 3
HIGHEST_PRIORITY = 4

RE_MAIN_PARSE_ARG = re.compile(r'\d+[.]\d+|\d+|[+,\-*^]|[/=!<>]+|\w+|[()]')
#
Copy link
Collaborator

Choose a reason for hiding this comment

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

Для чего тут пустые комментарии?

RE_NEGATIVE_VALUES = re.compile(r'[^\w)]+[\-]\d+[.]\d+|[^\w)]+[\-]\d+')
#
RE_NEGATIVE_VALUES_ON_STR_BEG = re.compile(r'^-\d+[.]\d+|^-\w+')
#
RE_INTS = re.compile(r'\d+')
#
RE_FLOATS = re.compile(r'\d+[.]\d+')
#
RE_FUNCTIONS = re.compile(r'[a-zA-Z]+[0-9]+|[a-zA-Z]+')
#
RE_OPERATIONS = re.compile(r'[(<=!>+\-*/%^]+')
#
RE_NEGATIVE_FUNCS = re.compile(r'[^\w)]-\w+[(].+[)]')

imports = ['math']
309 changes: 309 additions & 0 deletions final_task/logic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
"""

module which contains methods which make up base logic of the calculator

"""
import re
import inspect
import importlib
import constants
import pycodestyle
from typing import Any

pycodestyle.maximum_line_length = 120
Copy link
Collaborator

Choose a reason for hiding this comment

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

в данном случае лучше передавать такие настройки напрямую утилите pycodestyle, без подстройки этой конфигурации в самом калькуляторе



def parse_funcs_params(ex_list: list, methods: dict)-> list:
if not ex_list:
return None
scobes_count = 0
found_func = False
output_list = []
params_list = []
params_buff_list = []
one_param_list = []
for ex in ex_list:
if constants.RE_FUNCTIONS.findall(ex) and callable(methods[ex]):
if not found_func:
found_func = True
output_list.append(ex)
else:
one_param_list.append(ex)
continue
if ex == '(':
if not scobes_count:
output_list.append(ex)
else:
one_param_list.append(ex)
scobes_count += 1
continue
if ex == ')':
scobes_count -= 1
if not scobes_count:
found_func = False
params_list.append(one_param_list)
one_param_list = []
for list in params_list:
Copy link
Collaborator

Choose a reason for hiding this comment

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

list уже есть в built-in
Нужно дать другое имя переменной, чтобы не было перекрытия

params_buff_list.append(parse_funcs_params(list, methods))
params_list = params_buff_list
output_list.append(params_list)
params_list = []
params_buff_list = []
output_list.append(ex)
else:
one_param_list.append(ex)
continue
if ex == ',' and scobes_count == 1:
params_list.append(one_param_list)
one_param_list = []
continue
if scobes_count:
one_param_list.append(ex)
continue
output_list.append(ex)

return output_list


def import_usr_imports(usr_imports: list):
"""import user files

function gets objects of user imports and added it to global namespace

:param usr_imports: bunch of user imports names
:return: nothing
"""
import_objects = dict()

for import_elem in usr_imports:
import_objects[import_elem] = importlib.import_module(import_elem)
Copy link
Collaborator

Choose a reason for hiding this comment

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

import_objects = {import_elem: importlib.import_module(import_elem) for import_elem in usr_imports}


globals().update(import_objects)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Опасный подход с хранением импортированных объектов напрямую в глобальной секции. Намного лучше будет хранить эти объекты в отдельном словаре (либо что-то другое, но точно не стоит хранит это в globals)



def _get_item_by_type(item: str, methods: dict) -> Any:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Одна точка выхода из функции обычно лучше, чем несколько. В данном случае можно сделать только один return в функции без особых усилий и читаемость кода не пострадает.

"""get item by its string according its type

:param item: string of equivalent of some number
:param methods: dictionary which contains objects of methods of imports
:return: number by its type
"""
if not type(item) is str:
Copy link
Collaborator

Choose a reason for hiding this comment

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

if not isinstance(item, str)

return item
if constants.RE_FLOATS.findall(item):
return float(item)
elif constants.RE_INTS.findall(item):
return int(item)
elif constants.RE_FUNCTIONS.findall(item) and not callable(methods[item]):
return methods[item]
else:
return None


def get_imports_attrs(user_imports: list) -> dict:
"""get methods of user imports as dictionary

:param user_imports: list of names of user imports
:return: dictionary which contains names of methods of imports as keys and its object as values
"""
output_dict = dict()
for element in user_imports:
output_dict.update(_get_unit_attrs(element))
return output_dict


def _get_unit_attrs(unit: str) -> dict:
"""get attributes of unit by it name

:param unit: name of unit as string
:return: dictionary which contains name of attribute as key and it object as value
"""
attrs = tuple([i for i in dir(globals()[unit])])
Copy link
Collaborator

Choose a reason for hiding this comment

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

В данном случае сначала создается список, а потом приводится к кортежу :) Созание списка в этом случае абсолютно бесполезно.

attrs = tuple(i for i in dir(globals()[unit]))

И снова, советую совсем избавиться от globals

output_dict = dict()
for attribute in attrs:
if not attribute.count('_'):
output_dict[attribute] = getattr(globals()[unit], attribute)
output_dict['abs'] = abs
output_dict['round'] = round
return output_dict


def str_parse(ex_str: str, methods: dict) -> list:
"""parse mathematical expression string using regular expressions

:param ex_str: expression string
:return: list made of parsed expression string
"""
if not ex_str:
raise Exception('brackets are not balanced')
if ex_str.count('(') != ex_str.count(')'):
raise Exception('brackets are not balanced')

while re.compile(r'[+][-]|[-][+]').findall(ex_str):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Регулярные выражения лучше выносить с отдельные переменные с говорящим названием.

for operators in re.compile(r'[+][-]|[-][+]').findall(ex_str):
ex_str = ex_str.replace(operators, '-')
if not ex_str.count(operators):
break

while re.compile(r'[\-]{2,}').findall(ex_str) or re.compile(r'[+]{2,}').findall(ex_str):
for minuses in re.compile(r'[\-]{2,}').findall(ex_str):
if minuses.count('-') % 2 == 0:
ex_str = ex_str.replace(minuses, '+')
elif minuses.count('-') % 2 != 0:
ex_str = ex_str.replace(minuses, '-')
for pluses in re.compile(r'[+]{2,}').findall(ex_str):
ex_str = ex_str.replace(pluses, '+')

for negative_value in constants.RE_NEGATIVE_VALUES.findall(ex_str):
if re.compile(r'[a-zA-Z]+').findall(negative_value) and callable(methods[re.compile(r'[a-zA-Z]+').findall(negative_value)[0]]):
continue
ex_str = ex_str.replace(negative_value, negative_value[0]+'(0'+negative_value[1:]+')')

while constants.RE_NEGATIVE_FUNCS.findall(ex_str):
for negative_func in constants.RE_NEGATIVE_FUNCS.findall(ex_str):
ex_str = ex_str.replace(negative_func, negative_func[0]+'(0'+negative_func[1:]+')')

if constants.RE_NEGATIVE_VALUES_ON_STR_BEG.findall(ex_str):
ex_str = '0'+ex_str

parse_list = constants.RE_MAIN_PARSE_ARG.findall(ex_str)

packed_expression = parse_funcs_params(parse_list, methods)

return packed_expression


def _priority(expression: str) -> int:
Copy link
Collaborator

Choose a reason for hiding this comment

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

_get_priority

"""evaluate priority of given mathematical operator

:param expression: mathematical operator
:return: priority of given operator as integer
"""
if expression == constants.lowest_priority_operator:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Как вариант, хранение обозначения оператора и его приоритет можно связать в какую-то общую структуру.
либо можно сделать удобный мапинг в виде словаря, который позволит избавиться от цепочки конструкций elif

return constants.LOWEST_PRIORITY
elif expression in constants.low_priority_operators:
return constants.LOW_PRIORITY
elif expression in constants.mid_priority_operators:
return constants.MID_PRIORITY
elif expression in constants.high_priority_operators:
return constants.HIGH_PRIORITY
elif expression == constants.highest_priority_operator:
return constants.HIGHEST_PRIORITY
else:
return -1


def polish_notation(expression_list: list, methods: dict) -> list:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Название функции/метода должно обозначать действие

Copy link
Collaborator

Choose a reason for hiding this comment

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

Я насчитал в этой функции 14 операторов условий, что на самом деле очень много. Если есть возможность "без боли" разбить эту функцию на несколько маленьких, то это бы значительно упростило понимание кода.

"""rewrite mathematical expression list into polish notation

:param expression_list: parsed list of mathematical expression
:param methods: dictionary which contains name of attribute as key and it object as value
:return: rewrited into polish notation list of mathematical expression
"""
output_expression = []
operation_list = []

for expression in expression_list:
if isinstance(expression, list):
output_expression.append(polish_notation(expression, methods))
continue
if constants.RE_FUNCTIONS.findall(expression) and expression not in methods.keys():
raise Exception('unknown function {}'.format(expression))
if constants.RE_FUNCTIONS.findall(expression) and not callable(methods[expression]):
# if we found constant value
output_expression.append(expression)
continue

elif constants.RE_FUNCTIONS.findall(expression) and callable(methods[expression]):
# if we found mathematical operation
operation_list.append(expression)

if constants.RE_FLOATS.findall(expression) or constants.RE_INTS.findall(expression) \
and not constants.RE_FUNCTIONS.findall(expression) :
# if we found number
output_expression.append(expression)
continue

if constants.RE_OPERATIONS.findall(expression)or(constants.RE_FUNCTIONS.findall(expression)
and not callable(methods[expression])):
if (not operation_list or _priority(expression) > _priority(operation_list[-1])) and expression != '(':
# if we found operator or function and it priority higher than priority of operator on the top of
# operators stack add it into operators stack
operation_list.append(expression)

elif operation_list and _priority(operation_list[-1]) >= _priority(expression):
# if we found operator and it priority lower than priority of operator on the top of
# operators stack pop all of operators in operators stack into output stack
while operation_list and _priority(operation_list[-1]) >= _priority(expression):
if expression == '(':
break
output_expression.append(operation_list.pop())

if expression != '(':
operation_list.append(expression)

if expression == '(':
operation_list.append(expression)

if expression == ')':
while operation_list[-1] != '(':
if operation_list:
output_expression.append(operation_list.pop())
operation_list.pop()
if operation_list and constants.RE_FUNCTIONS.findall(operation_list[-1]):
# if after '(' we found operation also push it into output list
output_expression.append(operation_list.pop())

while operation_list:
output_expression.append(operation_list.pop())

return output_expression


def ex_calc(polish_list: list, methods: dict) -> float:
"""calculate mathematical expression writed as polish notation

:param polish_list: list which contains mathematical expression writed as polish notation
:param methods: dictionary which contains manes of methods of user imports as keys and objects of it as values
:return: result of calculated mathematical expression
"""
output_list = []
for ex in polish_list:
if constants.RE_FLOATS.findall(ex) or constants.RE_INTS.findall(ex) and not constants.RE_FUNCTIONS.findall(ex):
output_list.append(ex)
continue

if constants.RE_FUNCTIONS.findall(ex):
values = []

if not callable(methods[ex]): # if word is constant value just write it into output list
output_list.append(methods[ex])
continue

signature = inspect.signature(methods[ex])
parameters_count = len(signature.parameters)
parameters = signature.parameters
for parameter in parameters: # get count of parameters of function
if not parameters[parameter].default:
parameters_count -= 1
while parameters_count > 0:
if output_list:
float_val = float(output_list.pop())
values.append(float_val)
parameters_count -= 1

# perform mathematical function and write result into output List
output_list.append(methods[ex](*values[::-1]))

if constants.RE_OPERATIONS.findall(ex): # if found operation perform it
second_val = _get_item_by_type(output_list.pop(), methods)
first_val = _get_item_by_type(output_list.pop(), methods)
output_list.append(constants.operator[ex](first_val, second_val))

return _get_item_by_type(output_list[0], methods)

# goals for weekend

# argparse (!!!)
# setuptools(!!!)

11 changes: 11 additions & 0 deletions final_task/main_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import argparse
import mainclass
import constants
from math import *

argparse.ArgumentParser()

pycalc = mainclass.calculator()

print(pycalc.calculate(str(input())))
print(sin(-cos(-sin(3.0)-cos(-sin(-3.0*5.0)-sin(cos(log10(43.0))))+cos(sin(sin(34.0-2.0**2.0))))))
Loading