Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
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
22 changes: 22 additions & 0 deletions final_task/pycalc
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/local/bin/python3

"""Pure-python command-line calculator."""

import argparse

from pycalc_src import Calculator


def main():
"""Function parse argument and calculate expression."""

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('EXPRESSION', help='expression string to evaluate')
args = parser.parse_args()

pycalc = Calculator(args.EXPRESSION)
print(pycalc.calculate())


if __name__ == '__main__':
main()
1 change: 1 addition & 0 deletions final_task/pycalc_src/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from pycalc_src.calculator import Calculator
269 changes: 269 additions & 0 deletions final_task/pycalc_src/calculator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
"""Calculator module."""

from pycalc_src.exceptions import CalculatorError

from pycalc_src.operators import OPERATORS
from pycalc_src.operators import CONSTANTS
from pycalc_src.operators import UNARY_OPERATORS
from pycalc_src.operators import COMPARISON_SYMBOLS

from pycalc_src.preprocessor import Preprocessor


class Calculator:
"""Calculator object."""

def __init__(self, expression):
self.expression = expression
self.number = ''
self.operator = ''
self.unary_operator = ''
self.rpn = []
self.stack = []
self._return_code = 1

def _process_digit(self, index, symbol):
"""Process digit from expression."""
if self.expression[index - 1] == ' ' and self.number:
raise CalculatorError('invalid syntax', self._return_code)
self.number += symbol

def _process_number_and_constant(self):
"""Process number and constant."""
if self.unary_operator:
self.unary_operator = self._replace_unary_operator(self.unary_operator)

if self.number:
self.rpn.append(self._convert_to_number('{}{}'.format(self.unary_operator,
self.number)))
self.number = ''

if self.operator in CONSTANTS:

if self.rpn and self.rpn[-1] in CONSTANTS.values() and not self.stack:
self.stack.append('*')

if self.unary_operator == '-':
self.rpn.append(0 - CONSTANTS[self.operator])
else:
self.rpn.append(CONSTANTS[self.operator])
self.operator = ''

self.unary_operator = ''

def _process_operator(self, closing_bracket_index):
"""Process operator."""
if self.unary_operator:
self.stack.append(self.unary_operator)

if self.operator:
if self.operator not in OPERATORS:
raise CalculatorError('operator not supported', self._return_code)

self._process_implicit_multiplication(closing_bracket_index - len(self.operator))

self.stack.append(self.operator)

self.unary_operator = ''
self.operator = ''

def _process_stack(self, symbol):
"""Process stack."""
while self.stack:
if self.stack[-1] == symbol == '^':
break

if OPERATORS[symbol].priority <= OPERATORS[self.stack[-1]].priority:
self.rpn.append(self.stack.pop())
else:
break

self.stack.append(symbol)

def _process_comparison(self, index, symbol):
"""Process comparison."""
self._process_number_and_constant()

if self.stack and self.stack[-1] in COMPARISON_SYMBOLS:
if self.expression[index - 1] == ' ':
raise CalculatorError('unexpected whitespace', self._return_code)
self.stack[-1] += symbol
else:
while self.stack:
self.rpn.append(self.stack.pop())

self.stack.append(symbol)

def _process_brackets_and_comma(self, index, symbol):
"""Process brackets and comma from expression."""
if symbol == ',':
self._process_number_and_constant()
while self.stack:
if OPERATORS[symbol].priority < OPERATORS[self.stack[-1]].priority:
self.rpn.append(self.stack.pop())
else:
break
self.stack.append(symbol)
elif symbol == '(':
if self.number:
self._process_number_and_constant()
self.stack.append('*')
self._process_operator(index)

self._process_implicit_multiplication(index)

self.stack.append(symbol)
elif symbol == ')':
self._process_number_and_constant()
while self.stack:
element = self.stack.pop()
if element == '(':
break
self.rpn.append(element)

if self.stack and OPERATORS[self.stack[-1]].have_brackets:
self.rpn.append(self.stack.pop())

def _is_unary_operator(self, index, symbol):
"""Define that operator is unary."""
if symbol not in UNARY_OPERATORS:
return False
if index == 0:
return True
if index <= len(self.expression):
prev_symbol = self._get_previous_symbol(index)
if (prev_symbol in OPERATORS and prev_symbol != ')'
or prev_symbol in COMPARISON_SYMBOLS):
return True
return False

def _is_floordiv(self, index, symbol):
"""Define that operator is flordiv."""
if index <= len(self.expression):
return symbol == self.expression[index - 1] == '/'
return False

def _prepare_rpn(self):
"""Process expression to reverse polish notation."""
for index, symbol in enumerate(self.expression):

if self.operator in CONSTANTS:
self._process_number_and_constant()

if symbol in COMPARISON_SYMBOLS:
self._process_comparison(index, symbol)
continue

if symbol.isdigit() and self.operator:
self.operator += symbol
elif symbol.isdigit() or symbol == '.':
self._process_digit(index, symbol)
elif symbol in ('(', ',', ')'):
self._process_brackets_and_comma(index, symbol)
elif symbol in OPERATORS:
if self.stack and self._is_floordiv(index, symbol):
self.stack[-1] += symbol
continue

if self._is_unary_operator(index, symbol):
self.unary_operator = UNARY_OPERATORS[symbol]
continue

self._process_number_and_constant()
self._process_stack(symbol)
elif symbol.isalpha() or symbol == '=':
self.operator += symbol

if symbol != ')':
self._process_implicit_multiplication(index)

self._process_number_and_constant()
self.rpn.extend(reversed(self.stack))

if not self.rpn:
raise CalculatorError('not enough data to calculate', self._return_code)

del self.stack[:]

def _process_implicit_multiplication(self, index):
"""Сheck for implicit multiplication."""
prev_symbol = self._get_previous_symbol(index)
if prev_symbol == ')':
self.stack.append('*')

def _calculate_operator(self, operator):
"""Prepare operator to calculate."""
operator_params = OPERATORS[operator]

real_params_count = operator_params.params_quantity
if real_params_count == 3:
if self.stack and self.stack[-1] == ',':
self.stack.pop()
real_params_count = 2
else:
real_params_count = 1

if len(self.stack) < real_params_count:
raise CalculatorError("not enough operand's for function {}".format(operator), self._return_code)
elif self.stack and not isinstance(self.stack[-1], (int, float)):
raise CalculatorError("incorrect operand's for function {}".format(operator), self._return_code)

if real_params_count == 1:
operand = self.stack.pop()
self._calculate_result(operator_params.function, operand)
elif real_params_count == 2:
second_operand = self.stack.pop()
first_operand = self.stack.pop()
self._calculate_result(operator_params.function, first_operand, second_operand)

def _calculate_result(self, function, first_operand, second_operand=None):
"""Calculate function(operator)."""
try:
if second_operand is None:
result = function(first_operand)
else:
result = function(first_operand, second_operand)
except (ZeroDivisionError, ArithmeticError, Exception) as e:
raise CalculatorError(e, self._return_code)
else:
self.stack.append(result)

def _calculate_rpn(self):
"""Calculate reverse polish notation."""
for item in self.rpn:
if item == ',':
self.stack.append(item)
elif item in UNARY_OPERATORS.values():
unary_operator = self._replace_unary_operator(item)
self.stack.append(self._convert_to_number('{}1'.format(unary_operator)))
self._calculate_operator('*')
elif item in OPERATORS:
self._calculate_operator(item)
else:
self.stack.append(item)

def _replace_unary_operator(self, unary_operator):
"""Replace unary operator from raw expression."""
for key, value in UNARY_OPERATORS.items():
if value == unary_operator:
return key

def _convert_to_number(self, number):
"""Convert number characters to number."""
return float(number) if '.' in number else int(number)

def _get_previous_symbol(self, index):
"""Return previous symbol excluding whitespace's."""
for prev_symbol in reversed(self.expression[:index]):
if prev_symbol == ' ':
continue
return prev_symbol

def calculate(self):
"""Prepare and calculate expression."""
preprocessor = Preprocessor(self.expression)
self.expression = preprocessor.prepare_expression()
self._prepare_rpn()
self._calculate_rpn()

return self.stack[-1]
31 changes: 31 additions & 0 deletions final_task/pycalc_src/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Exceptions module."""


class BaseCalculatorException(Exception):
"""Base calculator exception."""

def __init__(self, message=None, return_code=1):
""""Init."""
if message is None:
message = 'an error occured while working pycalc'
self.message = 'ERROR: {}'.format(message)

if return_code == 1:
print(self.message)
exit(return_code)


class CalculatorError(BaseCalculatorException):
"""Exception for calculator."""

def __init__(self, message=None, return_code=1):
""""Init."""
super().__init__(message, return_code)


class PreprocessingError(BaseCalculatorException):
"""Exception for preprocessing."""

def __init__(self, message=None, return_code=1):
""""Init."""
super().__init__(message, return_code)
66 changes: 66 additions & 0 deletions final_task/pycalc_src/operators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Operators, constants, unary operators and comparison symbol's for pycalc."""

import operator
import builtins
import math

from collections import namedtuple


UNARY_OPERATORS = {'-': '-@', '+': '+@'}


COMPARISON_SYMBOLS = ('!', '<', '>', '=')


OPERATOR = namedtuple('OPERATOR', 'priority function params_quantity have_brackets')


OPERATORS = {
'+': OPERATOR(1, operator.add, 2, False),
'-': OPERATOR(1, operator.sub, 2, False),
'*': OPERATOR(2, operator.mul, 2, False),
'/': OPERATOR(2, operator.truediv, 2, False),
'//': OPERATOR(2, operator.floordiv, 2, False),
'%': OPERATOR(2, operator.mod, 2, False),
'^': OPERATOR(3, operator.pow, 2, False),
'pow': OPERATOR(3, operator.pow, 3, True),

'sin': OPERATOR(4, math.sin, 1, True),
'cos': OPERATOR(4, math.cos, 1, True),
'asin': OPERATOR(4, math.asin, 1, True),
'acos': OPERATOR(4, math.acos, 1, True),
'sinh': OPERATOR(4, math.sinh, 1, True),
'cosh': OPERATOR(4, math.cosh, 1, True),
'asinh': OPERATOR(4, math.asinh, 1, True),
'acosh': OPERATOR(4, math.acosh, 1, True),
'tanh': OPERATOR(4, math.tanh, 1, True),
'atanh': OPERATOR(4, math.atanh, 1, True),
'tan': OPERATOR(4, math.tan, 1, True),
'atan': OPERATOR(4, math.atan, 1, True),
'hypot': OPERATOR(4, math.hypot, 3, True),
'atan2': OPERATOR(4, math.atan2, 3, True),
'exp': OPERATOR(4, math.exp, 1, True),
'expm1': OPERATOR(4, math.expm1, 1, True),
'log10': OPERATOR(4, math.log10, 1, True),
'log2': OPERATOR(4, math.log2, 1, True),
'log1p': OPERATOR(4, math.log1p, 1, True),
'sqrt': OPERATOR(4, math.sqrt, 1, True),
'abs': OPERATOR(4, builtins.abs, 1, True),
'round': OPERATOR(4, builtins.round, 3, True),
'log': OPERATOR(4, math.log, 3, True),

'<': OPERATOR(0, operator.lt, 2, False),
'<=': OPERATOR(0, operator.le, 2, False),
'==': OPERATOR(0, operator.eq, 2, False),
'!=': OPERATOR(0, operator.ne, 2, False),
'>=': OPERATOR(0, operator.ge, 2, False),
'>': OPERATOR(0, operator.gt, 2, False),
',': OPERATOR(0, None, 0, False),
'(': OPERATOR(0, None, 0, False),
')': OPERATOR(5, None, 0, False),
'-@': OPERATOR(2, None, 0, False),
'+@': OPERATOR(2, None, 0, False)
}

CONSTANTS = {a: getattr(math, a) for a in dir(math) if isinstance(getattr(math, a), float)}
Loading