Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 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
10 changes: 5 additions & 5 deletions final_task/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ error explanation **with "ERROR: " prefix** and exit with non-zero exit code:
```shell
$ pycalc '15(25+1'
ERROR: brackets are not balanced
$ pycalc 'sin(-Pi/4)**1.5'
ERROR: negative number cannot be raised to a fractional power
$ pycalc 'func'
ERROR: unknown function 'func'
```

### Mathematical operations calculator must support
* Arithmetic (`+`, `-`, `*`, `/`, `//`, `%`, `^`) (`^` is a power).
* Comparison (`<`, `<=`, `==`, `!=`, `>=`, `>`).
* 3 built-in python functions: `abs`, `pow`, `round`.
* 2 built-in python functions: `abs` and `round`.
* All functions and constants from standard python module `math` (trigonometry, logarithms, etc.).


Expand Down Expand Up @@ -89,9 +89,9 @@ These requirements are not mandatory for implementation, but you can get more po
```shell
$ pycalc 'sin(pi/2)'
1.0
$ pycalc -m my_module 'sin(pi/2)'
$ pycalc 'sin(pi/2)' -m my_module
42
$ pycalc -m time 'time()/3600/24/365'
$ pycalc 'time()/3600/24/365' -m time
48.80147332327218
```

Expand Down
267 changes: 267 additions & 0 deletions final_task/pycalc/UnitTests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
"""This module contains unit tests for methods from all pycalc modules"""

# general import
import unittest

# import of classes to be tested from pycalc modules
from .tokenizer import Tokenizer
from .addmultsigns import Multsignsadder
from .rpn import RPN
from .constsreplacer import Constsreplacer
from .rpncalculator import RPNcalculator


# Tests of 'Tokenizer' class from 'tokenizer' module
class TokenizerTestCase(unittest.TestCase):
"""Tests for Tokenizer class"""

def test_extract_operators_and_pos_int_numbers(self):
"""Are operators and positive int numbers extracted properly?"""
user_expr = '1+2-3*4/5^6**7//8%9'
tokenizer = Tokenizer(user_expr)
tokens, error_msg = tokenizer.extract_tokens()
self.assertEqual(tokens, ['1', '+', '2', '-', '3', '*', '4', '/',
'5', '^', '6', '**', '7', '//', '8', '%', '9'])
self.assertEqual(error_msg, None)

def test_extract_operators_and_neg_int_numbers(self):
"""Are operators and negative int numbers extracted properly?"""
user_expr = '-1+-2--3*-4/-5^-6**-7//-8%-9'
tokenizer = Tokenizer(user_expr)
tokens, error_msg = tokenizer.extract_tokens()
self.assertEqual(tokens, ['-1.0', '-', '2', '+', '3', '*', '-4', '/', '-5',
'^', '-6', '**', '-7', '//', '-8', '%', '-9'])
self.assertEqual(error_msg, None)

def test_extract_pos_float_numbers(self):
"""Are positive float numbers extracted properly?"""
user_expr = '0.1+1.55-112.12'
tokenizer = Tokenizer(user_expr)
tokens, error_msg = tokenizer.extract_tokens()
self.assertEqual(tokens, ['0.1', '+', '1.55', '-', '112.12'])
self.assertEqual(error_msg, None)

def test_extract_neg_float_numbers(self):
"""Are negative float numbers extracted properly?"""
user_expr = '-0.1+-1.55--112.12'
tokenizer = Tokenizer(user_expr)
tokens, error_msg = tokenizer.extract_tokens()
self.assertEqual(tokens, ['-0.1', '-', '1.55', '+', '112.12'])
self.assertEqual(error_msg, None)

def test_extract_comparison_operators(self):
"""Are comparison operators extracted properly?"""
user_expr = '><>=<=!==='
tokenizer = Tokenizer(user_expr)
tokens, error_msg = tokenizer.extract_tokens()
self.assertEqual(tokens, ['>', '<', '>=', '<=', '!=', '=='])
self.assertEqual(error_msg, None)

def test_extract_pos_constants(self):
"""Are positive constants extracted properly?"""
user_expr = 'e+pi-tau/inf*nan'
tokenizer = Tokenizer(user_expr)
tokens, error_msg = tokenizer.extract_tokens()
self.assertEqual(tokens, ['e', '+', 'pi', '-', 'tau', '/', 'inf', '*', 'nan'])
self.assertEqual(error_msg, None)

def test_extract_neg_constants(self):
"""Are negative constants extracted properly?"""
user_expr = '-e+-pi--tau/-inf*-nan'
tokenizer = Tokenizer(user_expr)
tokens, error_msg = tokenizer.extract_tokens()
self.assertEqual(tokens, ['-e', '-', 'pi', '+', 'tau', '/', '-inf', '*', '-nan'])
self.assertEqual(error_msg, None)

def test_extract_brackets(self):
"""Are brackets extracted properly?"""
user_expr = '()'
tokenizer = Tokenizer(user_expr)
tokens, error_msg = tokenizer.extract_tokens()
self.assertEqual(tokens, ['(', ')'])
self.assertEqual(error_msg, None)

def test_extract_comma(self):
"""Is comma extracted?"""
user_expr = 'pow(2,3)'
tokenizer = Tokenizer(user_expr)
tokens, error_msg = tokenizer.extract_tokens()
self.assertEqual(tokens, ['pow', '(', '2', ',', '3', ')'])
self.assertEqual(error_msg, None)

def test_extract_functions(self):
"""Are functions extracted properly?"""
user_expr = "round(sin(2)-asin(1))-abs(exp(3))"
tokenizer = Tokenizer(user_expr)
tokens, error_msg = tokenizer.extract_tokens()
self.assertEqual(tokens, ['round', '(', 'sin', '(', '2', ')', '-', 'asin', '(', '1', ')', ')',
'-', 'abs', '(', 'exp', '(', '3', ')', ')'])
self.assertEqual(error_msg, None)

def test_consider_sub_signs_method(self):
"""Are several subtraction and addition signs replaced by one integrated sign?"""
user_expr = '-1---2+-3+++4+-2'
tokenizer = Tokenizer(user_expr)
tokens, error_msg = tokenizer.extract_tokens()
self.assertEqual(tokens, ['-1.0', '-', '2', '-', '3', '+', '4', '-', '2'])
self.assertEqual(error_msg, None)

def test_is_number_method(self):
"""Does 'is_number' method distinguish tokens which are numbers from ones which are not?"""
tokens = ['.3', '-0.3', '7', 'tan']
tokenizer = Tokenizer(user_expr='')
is_numbers = []
for token in tokens:
is_numbers.append(tokenizer.is_number(token))
self.assertEqual(is_numbers, [True, True, True, False])

def test_extract_tokens_error_msg(self):
"""Is error_message created?"""
user_expr = "2+shikaka(3)"
tokenizer = Tokenizer(user_expr)
tokens, error_msg = tokenizer.extract_tokens()
self.assertEqual(error_msg, 'ERROR: invalid syntax')


# Tests of 'Multsignsadder' class from 'addmultsigns' module
class MultsignsadderTestCase(unittest.TestCase):
"""Tests for Multsignsadder class"""

def test_is_number_method(self):
"""Does 'is_number' method distinguish tokens which are numbers from ones which are not?"""
tokens = ['2.3', '-0.6', '5', 'sin', 'exp']
mult_signs_adder = Multsignsadder(tokens)
is_numbers = []
for token in tokens:
is_numbers.append(mult_signs_adder.is_number(token))
self.assertEqual(is_numbers, [True, True, True, False, False])

def test_addmultsigns_add_mult_signs(self):
"""Are multiplication signs added to where they implicit were to be in expression?"""
tokens = ['5', 'tau', '-', '4', 'sin', '(', '7', ')', '-', '9', '(', '1', '+', '10', ')']
mult_signs_adder = Multsignsadder(tokens)
extd_tokens = mult_signs_adder.addmultsigns()
self.assertEqual(extd_tokens, ['5', '*', 'tau', '-', '4', '*', 'sin', '(', '7', ')', '-',
'9', '*', '(', '1', '+', '10', ')'])

def test_addmultsigns_dont_add_mult_signs(self):
"""Aren't multiplication signs added if it's not needed?"""
tokens = ['2', '+', '3', '*', '5']
mult_signs_adder = Multsignsadder(tokens)
extd_tokens = mult_signs_adder.addmultsigns()
self.assertEqual(extd_tokens, ['2', '+', '3', '*', '5'])

def test_consider_neg_funcs_method(self):
"""Are negative functions tokens replaced by '-1*function' tokens?"""
tokens = ['2', '*', '-sin', '(', '2', ')']
mult_signs_adder = Multsignsadder(tokens)
mult_signs_adder.consider_neg_functions(mult_signs_adder.tokens)
self.assertEqual(mult_signs_adder.tokens, ['2', '*', '-1', '*', 'sin', '(', '2', ')'])

def test_consider_log_args_method(self):
"""Is 'e' added as a base for log functions if last was entered with one argument?"""
tokens = ['log', '(', '33', ')']
mult_signs_adder = Multsignsadder(tokens)
mult_signs_adder.consider_log_args(mult_signs_adder.tokens)
self.assertEqual(mult_signs_adder.tokens, ['log', '(', '33', ',', 'e', ')'])


# Tests of 'RPN class' from 'rpn' module
class RPNTestCase(unittest.TestCase):
"""Tests for RPN class"""

def test_is_left_associative_method(self):
"""Are left associative operators recognized?"""
tokens = ['^', '**', '+', '/']
rpn = RPN(tokens)
is_left_associative = []
for token in tokens:
is_left_associative.append(rpn.is_left_associative(token))
self.assertEqual(is_left_associative, [False, False, True, True])

def test_is_number_method(self):
"""Does 'is_number' method distinguish tokens which are numbers from ones which are not?"""
tokens = ['1.3', '-0.5', '/', '%', '9']
rpn = RPN(tokens)
is_numbers = []
for token in tokens:
is_numbers.append(rpn.is_number(token))
self.assertEqual(is_numbers, [True, True, False, False, True])

def test_convert2rpn_method(self):
"""Does 'convert2rpn' method work correctly?"""
tokens = ['-pi', '*', 'round', '(', '2.23', ')', '//', '5', '*', 'pow', '(', '2', '3', ')']
rpn = RPN(tokens)
result, error_msg = rpn.convert2rpn()
self.assertEqual(result, ['-pi', '2.23', 'round', '*', '5', '//', '2', '3', 'pow', '*'])
self.assertEqual(error_msg, None)

def test_convert2rpn_method_error_msg(self):
"""Is error_message created?"""
tokens = ['(', '2', '+', '3', ')', ')']
rpn = RPN(tokens)
result, error_msg = rpn.convert2rpn()
self.assertEqual(error_msg, 'ERROR: brackets are not balanced')


# Tests of 'Constsreplacer' class from 'constsreplacer' module
class ConstsreplacerTestCase(unittest.TestCase):
"""Tests for Constsreplacer class"""

def test_replace_constants_method(self):
"""Are constants replaced and not constants aren't replaced?"""
tokens = ['e', '-e', 'pi', '-pi', 'tau', '-tau', '2', 'cos', 'inf', '-nan', '+']
constsreplacer = Constsreplacer(tokens)
replaced_tokens = constsreplacer.replace_constants()
self.assertEqual(replaced_tokens, ['2.718281828459045', '-2.718281828459045',
'3.141592653589793', '-3.141592653589793',
'6.283185307179586', '-6.283185307179586',
'2', 'cos', 'inf', '-nan', '+'])


# Tests of 'RPNcalculator' class from 'rpncalculator' module
class RPNcalculatorTestCase(unittest.TestCase):
"""Tests for RPNcalculator class"""

def test_evaluate_method_result(self):
"""Does 'evaluate' method actually evaluate RPN math expression and give out correct result?"""
rpn_tokens = ['2', 'sqrt', '3', '/', '3.14', '*', 'tan']
rpncalculator = RPNcalculator(rpn_tokens)
result, error_msg = rpncalculator.evaluate()
self.assertEqual(result, 11.009005500434151)

Choose a reason for hiding this comment

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

Возможно есть смысл заменить магическую константу в этой строке на выражение на языке Python, чтобы было четко видно, что эта функциональность должна повторять функциональность языка Python. Также в таких случаях можно использовать метод для проверки приблизительного равенства чисел assertAlmostEqual https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertAlmostEqual

self.assertEqual(error_msg, None)

def test_evaluate_method_error_msg_zero_division(self):
"""Is 'division by zero' error message created?"""
rpn_tokens = ['2', '0', '/']
rpncalculator = RPNcalculator(rpn_tokens)
result, error_msg = rpncalculator.evaluate()
self.assertEqual(error_msg, 'ERROR: float division by zero')

def test_evaluate_method_error_msg_neg_num_in_fract_pow(self):
"""Is 'negative number cannot be raised to a fractional power' error message created?"""
rpn_tokens = [['-2', '0.5', '**'], ['-2', '0.5', '^']]
error_msgs = []
for rpn_tokens_list in rpn_tokens:
rpncalculator = RPNcalculator(rpn_tokens_list)
error_msgs.append(rpncalculator.evaluate()[1])
for error_msg in error_msgs:
self.assertEqual(error_msg, 'ERROR: negative number cannot be raised to a fractional power')

def test_evaluate_method_error_msg_neg_num_sqrt(self):
"""Is 'root can't be extracted from a negative number' error message created?"""
rpn_tokens = ['-2', 'sqrt']
rpncalculator = RPNcalculator(rpn_tokens)
result, error_msg = rpncalculator.evaluate()
self.assertEqual(error_msg, "ERROR: a root can't be extracted from a negative number")

def test_evaluate_method_error_msg_invalid_syntax(self):
"""Is 'invalid syntax' error message created?"""
rpn_tokens = ['2', '+']
rpncalculator = RPNcalculator(rpn_tokens)
result, error_msg = rpncalculator.evaluate()
self.assertEqual(error_msg, "ERROR: invalid syntax")


if __name__ == '__main__':
unittest.main()
Empty file added final_task/pycalc/__init__.py
Empty file.
67 changes: 67 additions & 0 deletions final_task/pycalc/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#! /usr/bin/python3
# -*- coding: UTF-8 -*-

# general import
import argparse
import sys
# import from pycalc self library
from .tokenizer import Tokenizer
from .addmultsigns import Multsignsadder
from .rpn import RPN
from .constsreplacer import Constsreplacer
from .rpncalculator import RPNcalculator


def createparser():
"""Creates parser with one positional argument 'expression' to parse user's mathematical expression"""
parser = argparse.ArgumentParser(prog='pycalc', description='pure Python command line calculator',
epilog="""Anton Charnichenka for EPAM: Introduction to Python
and Golang programming, 2018.""")
parser.add_argument('expression', help="""mathematical expression string to evaluate;
implicit multiplication is supported""")

return parser


def main():
"""Calculate user's expression"""
parser = createparser()
namespace = parser.parse_args(sys.argv[1:])

Choose a reason for hiding this comment

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

В данном случае необязательно явно отправлять sys.argv[1:]
можно опустить отправку этого аргумента в эту функцию

user_expr = namespace.expression

# calculation chain
# tokenize user's expression string
tokenizer = Tokenizer(user_expr)
tokens, error_msg = tokenizer.extract_tokens()
if error_msg:
print(error_msg)
sys.exit(1)
elif not tokens:
print('ERROR: no expression was entered')
sys.exit(1)
# add implicit multiplication signs to the list of extracted tokens
mult_signs_adder = Multsignsadder(tokens)
tokens = mult_signs_adder.addmultsigns()
# transform extracted tokens into RPN
rpn = RPN(tokens)
rpn_tokens, error_msg = rpn.convert2rpn()
if error_msg:
print(error_msg)
sys.exit(1)
# replace constants with their numeric equivalents
constsreplacer = Constsreplacer(rpn_tokens)
rpn_tokens = constsreplacer.replace_constants()
# evaluate user's expression
rpncalculator = RPNcalculator(rpn_tokens)
result, error_msg = rpncalculator.evaluate()
if error_msg:
print(error_msg)
sys.exit(1)
else:
print(result)
sys.exit(0)


# main
if __name__ == "__main__":
main()
Loading