Skip to content

Commit 650aa02

Browse files
pycalc init
1 parent 1c1529e commit 650aa02

File tree

7 files changed

+396
-0
lines changed

7 files changed

+396
-0
lines changed

.travis.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
language: python
2+
python:
3+
- "3.6"
4+
install:
5+
- pip install -r requirements.txt
6+
script:
7+
- cd final_task
8+
- pip install .
9+
- nosetests --cover-branches --with-coverage .
10+
- pycodestyle --max-line-length=120 .
11+
- python ./../pycalc_checker.py
12+
- cd -

final_task/README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Python Programming Language Foundation Hometask
2+
You are proposed to implement pure-python command-line calculator
3+
using **python 3.6**.
4+
5+
## Minimal requirements
6+
Calculator should be a command-line utility which receives mathematical
7+
expression string as an argument and prints evaluated result:
8+
```shell
9+
$ pycalc '2+2*2'
10+
6
11+
```
12+
13+
It should provide the following interface:
14+
```shell
15+
$ pycalc --help
16+
usage: pycalc [-h] EXPRESSION
17+
18+
Pure-python command-line calculator.
19+
20+
positional arguments:
21+
EXPRESSION expression string to evaluate
22+
23+
```
24+
25+
In case of any mistakes in the expression utility should print human-readable
26+
error explanation **with "ERROR: " prefix** and exit with non-zero exit code:
27+
```shell
28+
$ pycalc '15(25+1'
29+
ERROR: brackets are not balanced
30+
$ pycalc 'sin(-Pi/4)**1.5'
31+
ERROR: negative number cannot be raised to a fractional power
32+
```
33+
34+
### Mathematical operations calculator must support
35+
* Arithmetic (`+`, `-`, `*`, `/`, `//`, `%`, `^`) (`^` is a power).
36+
* Comparison (`<`, `<=`, `==`, `!=`, `>=`, `>`).
37+
* 3 built-in python functions: `abs`, `pow`, `round`.
38+
* All functions and constants from standard python module `math` (trigonometry, logarithms, etc.).
39+
40+
41+
### Non-functional requirements
42+
* It is mandatory to use `argparse` module.
43+
* Codebase must be covered with unittests with at least 70% coverage.
44+
* Usage of **eval** and **exec** is prohibited.
45+
* Usage of module **ast** is prohibited.
46+
* Usage of external modules is prohibited (python standard library only).
47+
48+
### Distribution
49+
* Utility should be wrapped into distribution package with `setuptools`.
50+
* This package should export CLI utility named `pycalc`.
51+
52+
### Codestyle
53+
* Docstrings are mandatory for all methods, classes, functions and modules.
54+
* Code must correspond to pep8 (use `pycodestyle` utility for self-check).
55+
* You can set line length up to 120 symbols.
56+
57+
58+
## Optional requirements
59+
These requirements are not mandatory for implementation, but you can get more points for them.
60+
61+
* Support of functions and constants from the modules provided with `-m` or `--use-modules` command-line option.
62+
This option should accept **names** of the modules which are accessible via
63+
[python standard module search paths](https://docs.python.org/3/tutorial/modules.html#the-module-search-path).
64+
Functions and constants from user defined modules have higher priority in case of name conflict then stuff from `math` module or built-in functions.
65+
66+
In this case `pycalc` utility should provide the following interface:
67+
```shell
68+
$ pycalc --help
69+
usage: pycalc [-h] [-m MODULE [MODULE ...]] EXPRESSION
70+
71+
Pure-python command-line calculator.
72+
73+
positional arguments:
74+
EXPRESSION expression string to evaluate
75+
76+
optional arguments:
77+
-h, --help show this help message and exit
78+
-m MODULE [MODULE ...], --use-modules MODULE [MODULE ...]
79+
additional modules to use
80+
```
81+
82+
Usage example:
83+
```python
84+
# my_module.py
85+
def sin(number):
86+
return 42
87+
```
88+
89+
```shell
90+
$ pycalc 'sin(pi/2)'
91+
1.0
92+
$ pycalc -m my_module 'sin(pi/2)'
93+
42
94+
$ pycalc -m time 'time()/3600/24/365'
95+
48.80147332327218
96+
```
97+
98+
* Support of implicit multiplication:
99+
```shell
100+
$ pycalc '2(5+2)'
101+
14
102+
$ pycalc '(1 + 2)(3 + 4)'
103+
21
104+
$ pycalc 'log10(10)5'
105+
5
106+
```
107+
108+
---
109+
Implementations will be checked with the latest cPython interpreter of 3.6 branch.
110+
---

final_task/__init__.py

Whitespace-only changes.

final_task/setup.py

Whitespace-only changes.

final_task/test_cases.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Test cases for Home task
2+
3+
4+
### Unary operators
5+
6+
* "-13"
7+
* "6-(-13)"
8+
* "1---1"
9+
* "-+---+-1"
10+
11+
### Operation priority
12+
* "1+2\*2"
13+
* "1+(2+3\*2)\*3"
14+
* "10\*(2+1)"
15+
* "10^(2+1)"
16+
* "100/3^2"
17+
* "100/3%2^2"
18+
19+
### Functions and constants
20+
* "pi+e"
21+
* "log(e)"
22+
* "sin(pi/2)"
23+
* "log10(100)"
24+
* "sin(pi/2)\*111\*6"
25+
* "2\*sin(pi/2)"
26+
27+
### Associative
28+
* "102%12%7"
29+
* "100/4/3"
30+
* "2^3^4"
31+
32+
33+
### Comparison operators
34+
* "1+2\*3==1+2\*3"
35+
* "e^5>=e^5+1"
36+
* "1+2\*4/3+1!=1+2\*4/3+2"
37+
38+
39+
### Common tests
40+
* "(100)"
41+
* "666"
42+
* "10(2+1)"
43+
* "-.1"
44+
* "1/3"
45+
* "1.0/3.0"
46+
* ".1 \* 2.0^56.0"
47+
* "e^34"
48+
* "(2.0^(pi/pi+e/e+2.0^0.0))"
49+
* "(2.0^(pi/pi+e/e+2.0^0.0))^(1.0/3.0)"
50+
* "sin(pi/2^1) + log(1\*4+2^2+1, 3^2)"
51+
* "10\*e^0\*log10(.4 -5/ -0.1-10) - -abs(-53/10) + -5"
52+
* "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))))--cos(1.0)--cos(0.0)^3.0)"
53+
* "2.0^(2.0^2.0\*2.0^2.0)"
54+
* "sin(e^log(e^e^sin(23.0),45.0) + cos(3.0+log10(e^-e)))"
55+
56+
### Error cases
57+
* ""
58+
* "+"
59+
* "1-"
60+
* "1 2"
61+
* "ee"
62+
* "==7"
63+
* "1 + 2(3 \* 4))"
64+
* "((1+2)"
65+
* "1 + 1 2 3 4 5 6 "
66+
* "log100(100)"
67+
* "------"
68+
* "5 > = 6"
69+
* "5 / / 6"
70+
* "6 < = 6"
71+
* "6 \* \* 6"
72+
* "((((("
73+
74+
---

pycalc_checker.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import sys
2+
import subprocess
3+
from math import *
4+
from distutils.util import strtobool
5+
from termcolor import colored
6+
7+
8+
PYCALC_UTIL_NAME = "pycalc"
9+
RETURN_CODE = 0
10+
11+
12+
UNARY_OPERATORS = {
13+
"-13": -13,
14+
"6-(-13)": 6-(-13),
15+
"1---1": 1---1,
16+
"-+---+-1": -+---+-1,
17+
}
18+
19+
OPERATION_PRIORITY = {
20+
"1+2*2": 1+2*2,
21+
"1+(2+3*2)*3": 1+(2+3*2)*3,
22+
"10*(2+1)": 10*(2+1),
23+
"10^(2+1)": 10**(2+1),
24+
"100/3^2": 100/3**2,
25+
"100/3%2^2": 100/3%2**2,
26+
}
27+
28+
29+
FUNCTIONS_AND_CONSTANTS = {
30+
"pi+e": pi+e,
31+
"log(e)": log(e),
32+
"sin(pi/2)": sin(pi/2),
33+
"log10(100)": log10(100),
34+
"sin(pi/2)*111*6": sin(pi/2)*111*6,
35+
"2*sin(pi/2)": 2*sin(pi/2),
36+
}
37+
38+
ASSOCIATIVE = {
39+
"102%12%7": 102%12%7,
40+
"100/4/3": 100/4/3,
41+
"2^3^4": 2**3**4,
42+
}
43+
44+
45+
COMPARISON_OPERATORS = {
46+
"1+2*3==1+2*3": eval("1+2*3==1+2*3"),
47+
"e^5>=e^5+1": eval("e**5>=e**5+1"),
48+
"1+2*4/3+1!=1+2*4/3+2": eval("1+2*4/3+1!=1+2*4/3+2"),
49+
}
50+
51+
52+
COMMON_TESTS = {
53+
"(100)": (100),
54+
"666": 666,
55+
"-.1": -.1,
56+
"1/3": 1/3,
57+
"1.0/3.0": 1.0/3.0,
58+
".1 * 2.0^56.0": .1 * 2.0**56.0,
59+
"e^34": e**34,
60+
"(2.0^(pi/pi+e/e+2.0^0.0))": (2.0**(pi/pi+e/e+2.0**0.0)),
61+
"(2.0^(pi/pi+e/e+2.0^0.0))^(1.0/3.0)": (2.0**(pi/pi+e/e+2.0**0.0))**(1.0/3.0),
62+
"sin(pi/2^1) + log(1*4+2^2+1, 3^2)": sin(pi/2**1) + log(1*4+2**2+1, 3**2),
63+
"10*e^0*log10(.4 -5/ -0.1-10) - -abs(-53/10) + -5": 10*e**0*log10(.4 -5/ -0.1-10) - -abs(-53/10) + -5,
64+
"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))))--cos(1.0)--cos(0.0)^3.0)": 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))))--cos(1.0)--cos(0.0)**3.0),
65+
"2.0^(2.0^2.0*2.0^2.0)": 2.0**(2.0**2.0*2.0**2.0),
66+
"sin(e^log(e^e^sin(23.0),45.0) + cos(3.0+log10(e^-e)))": sin(e**log(e**e**sin(23.0),45.0) + cos(3.0+log10(e**-e))),
67+
}
68+
69+
70+
ERROR_CASES = [
71+
"",
72+
"+",
73+
"1-",
74+
"1 2",
75+
"ee",
76+
"==7",
77+
"1 + 2(3 * 4))",
78+
"((1+2)",
79+
"1 + 1 2 3 4 5 6 ",
80+
"log100(100)",
81+
"------",
82+
"5 > = 6",
83+
"5 / / 6",
84+
"6 < = 6",
85+
"6 * * 6",
86+
"(((((",
87+
"abs",
88+
"pow(2, 3, 4)",
89+
]
90+
91+
# OPTIONAL REQUIREMENTS
92+
IMPLICIT_MULTIPLICATION = {
93+
"10(2+1)": 10*(2+1),
94+
"(1 + 2)(3 + 4)": (1 + 2) * (3 + 4),
95+
"epi": e * pi,
96+
"2sin(pi/2)": 2 * sin(pi/2),
97+
"2 sin(pi/2)": 2 * sin(pi/2),
98+
"sin(pi)sin(pi) + cos(pi)cos(pi)": sin(pi) * sin(pi) + cos(pi) * cos(pi),
99+
}
100+
101+
102+
PASS_TEMPLATE = "Test"
103+
FAIL_TEMPLATE = ""
104+
105+
106+
def trunc_string(string):
107+
return (string[:40] + '..') if len(string) > 40 else string
108+
109+
110+
def call_command(command, positional_params, optional_params=""):
111+
params = [command, "--", positional_params] if not optional_params else [command, optional_params, " -- ", positional_params]
112+
result = subprocess.run(params, stdout=subprocess.PIPE)
113+
return result.stdout.decode('utf-8')
114+
115+
116+
def check_results(keys: dict, required=True, user_module=""):
117+
global RETURN_CODE
118+
for command, expected_result in keys.items():
119+
result = call_command(PYCALC_UTIL_NAME, command, optional_params=user_module)
120+
try:
121+
converted_result = float(result)
122+
except Exception:
123+
try:
124+
converted_result = bool(strtobool(result[:-1]))
125+
except Exception:
126+
print("{: <45} | Result: {}".format(
127+
command,
128+
"{}: Invalid output: {} | Expected: {}".format(colored("FAIL", "red"), result, expected_result))
129+
)
130+
if required:
131+
RETURN_CODE = 1
132+
continue
133+
134+
if round(expected_result, 2) == round(converted_result, 2):
135+
print("{: <45} | Result: {}".format(trunc_string(command), colored("PASS", "green")))
136+
else:
137+
print("{: <45} | Result: {}".format(
138+
command,
139+
"{}: Invalid output: {} | Expected: {}".format(colored("FAIL", "red"), result, expected_result))
140+
)
141+
if required:
142+
RETURN_CODE = 1
143+
144+
145+
def check_error_results(keys: list, required=True):
146+
global RETURN_CODE
147+
for command in keys:
148+
result = call_command(PYCALC_UTIL_NAME, command)
149+
if result.startswith("ERROR:"):
150+
print("{: <45} | Result: {}".format(trunc_string(command), colored("PASS", "green")))
151+
else:
152+
print("{: <45} | Result: {}".format(trunc_string(command),
153+
"{}, expected correct error handling".format(colored("FAIL", "red"))))
154+
if required:
155+
RETURN_CODE = 1
156+
157+
158+
def main():
159+
print(colored("************** Checking minimal requirements **************\n", "green"))
160+
print("Checking unary operators...")
161+
check_results(UNARY_OPERATORS)
162+
print()
163+
164+
print("Checking operation priority...")
165+
check_results(OPERATION_PRIORITY)
166+
print()
167+
168+
print("Checking functions and constants...")
169+
check_results(FUNCTIONS_AND_CONSTANTS)
170+
print()
171+
172+
print("Checking associative...")
173+
check_results(ASSOCIATIVE)
174+
print()
175+
176+
print("Checking comparison operators...")
177+
check_results(COMPARISON_OPERATORS)
178+
print()
179+
180+
print("Checking common tests...")
181+
check_results(COMMON_TESTS)
182+
print()
183+
184+
print("Checking error cases...")
185+
check_error_results(ERROR_CASES)
186+
print()
187+
188+
print(colored("************** Checking optional requirements **************\n", "green"))
189+
check_results(IMPLICIT_MULTIPLICATION, required=False)
190+
print()
191+
192+
sys.exit(RETURN_CODE)
193+
194+
195+
if __name__ == '__main__':
196+
main()

0 commit comments

Comments
 (0)