Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
266 changes: 266 additions & 0 deletions final_task/pycalc/UnitTests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
"""This module contains unit tests for methods and functions from all pycalc modules"""

# import

Choose a reason for hiding this comment

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

Такие комментарии повторяют информацию, которую нам может дать код, к которому эти комментарии прикреплены. Эти комментарии могут быть опущены либо оформлены как docstring для функций или классов.

Похожие комментарии есть в этом файле в строках 13, 17, 120, 154, 183 и в других файлах.

import unittest
from .tokenizer import Tokenizer
from .addmultsigns import Multsignsadder
from .rpn import RPN
from .constsreplacer import Constsreplacer
from .rpncalculator import RPNcalculator
from .utils import is_number
from .pycalclib import Pycalclib

# create pycalclib
pycalclib = Pycalclib(user_module='test_user_module')


# 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, pycalclib)
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, pycalclib)
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, pycalclib)
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, pycalclib)
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, pycalclib)
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, pycalclib)
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, pycalclib)
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, pycalclib)
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, pycalclib)
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, pycalclib)
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, pycalclib)
tokens, error_msg = tokenizer.extract_tokens()
self.assertEqual(tokens, ['-1.0', '-', '2', '-', '3', '+', '4', '-', '2'])
self.assertEqual(error_msg, None)

def test_extract_tokens_error_msg(self):
"""Is error_message created?"""
user_expr = "2+shikaka(3)"
tokenizer = Tokenizer(user_expr, pycalclib)
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_addmultsigns_add_mult_signs(self):
"""Are multiplication signs added to where they implicit were to be in expression?"""
tokens = ['5', 'tau', '-', '4', 'sin', '(', '7', ')', 'sin', '(', '3', ')', '-', '9', '(', '1', '+', '10', ')']
mult_signs_adder = Multsignsadder(tokens, pycalclib)
extd_tokens = mult_signs_adder.addmultsigns()
self.assertEqual(extd_tokens, ['5', '*', 'tau', '-', '4', '*', 'sin', '(', '7', ')', '*', 'sin', '(', '3', ')',
'-', '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, pycalclib)
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, pycalclib)
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 function if last was entered with one argument?"""
tokens = ['log', '(', '33', ')']
mult_signs_adder = Multsignsadder(tokens, pycalclib)
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, pycalclib)
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_convert2rpn_method(self):
"""Does 'convert2rpn' method work correctly?"""
tokens = ['-pi', '*', 'round', '(', '2.23', ')', '//', '5', '*', 'pow', '(', '2', '3', ')']
rpn = RPN(tokens, pycalclib)
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, pycalclib)
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, pycalclib)
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, pycalclib)
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, pycalclib)
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, pycalclib)
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, pycalclib)
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, pycalclib)
result, error_msg = rpncalculator.evaluate()
self.assertEqual(error_msg, "ERROR: invalid syntax")


# Test of 'Pycalclib' class from 'pycalclib' module
class PycalclibTestCase(unittest.TestCase):
"""Tests for Pycalclib class"""

def test_consider_user_module(self):
"""Are user functions added to pycalclib?"""
self.assertIn('five', pycalclib.functions)
self.assertIn('squaressum', pycalclib.functions)


# Tests of 'is_number' function from 'utils' module
class IsNumberTestCase(unittest.TestCase):
"""Test for 'is_number' function"""

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


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

# import
import argparse
import sys
from .tokenizer import Tokenizer
from .addmultsigns import Multsignsadder
from .rpn import RPN
from .constsreplacer import Constsreplacer
from .rpncalculator import RPNcalculator
from .pycalclib import Pycalclib


def create_parser():
"""Creates parser to parse user 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""")
parser.add_argument('--user_module', '-m', default='', help='additional module with user defined functions')

return parser


def main():
"""Calculates user math expression"""
parser = create_parser()
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
user_module = namespace.user_module

# create pycalclib
pycalclib = Pycalclib(user_module)

# calculation chain
# tokenize user's expression string
tokenizer = Tokenizer(user_expr, pycalclib)
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, pycalclib)
tokens = mult_signs_adder.addmultsigns()
# transform extracted tokens into RPN
rpn = RPN(tokens, pycalclib)
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, pycalclib)
rpn_tokens = constsreplacer.replace_constants()
# evaluate user's expression
rpncalculator = RPNcalculator(rpn_tokens, pycalclib)
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