nexuscalc 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nexuscalc-1.0.0/LICENSE +21 -0
- nexuscalc-1.0.0/PKG-INFO +126 -0
- nexuscalc-1.0.0/README.md +109 -0
- nexuscalc-1.0.0/setup.cfg +4 -0
- nexuscalc-1.0.0/setup.py +29 -0
- nexuscalc-1.0.0/src/nexuscalc/__init__.py +23 -0
- nexuscalc-1.0.0/src/nexuscalc/core/__init__.py +1 -0
- nexuscalc-1.0.0/src/nexuscalc/core/calculator.py +140 -0
- nexuscalc-1.0.0/src/nexuscalc/core/constants.py +30 -0
- nexuscalc-1.0.0/src/nexuscalc/core/operations.py +55 -0
- nexuscalc-1.0.0/src/nexuscalc/evaluator/__init__.py +1 -0
- nexuscalc-1.0.0/src/nexuscalc/evaluator/evaluator.py +81 -0
- nexuscalc-1.0.0/src/nexuscalc/evaluator/functions.py +124 -0
- nexuscalc-1.0.0/src/nexuscalc/exceptions/__init__.py +1 -0
- nexuscalc-1.0.0/src/nexuscalc/exceptions/calculator_errors.py +27 -0
- nexuscalc-1.0.0/src/nexuscalc/parsers/__init__.py +1 -0
- nexuscalc-1.0.0/src/nexuscalc/parsers/ast_nodes.py +63 -0
- nexuscalc-1.0.0/src/nexuscalc/parsers/expression_parser.py +111 -0
- nexuscalc-1.0.0/src/nexuscalc/parsers/tokenizer.py +52 -0
- nexuscalc-1.0.0/src/nexuscalc/utils/__init__.py +1 -0
- nexuscalc-1.0.0/src/nexuscalc/utils/formatters.py +64 -0
- nexuscalc-1.0.0/src/nexuscalc/utils/validators.py +43 -0
- nexuscalc-1.0.0/src/nexuscalc.egg-info/PKG-INFO +126 -0
- nexuscalc-1.0.0/src/nexuscalc.egg-info/SOURCES.txt +25 -0
- nexuscalc-1.0.0/src/nexuscalc.egg-info/dependency_links.txt +1 -0
- nexuscalc-1.0.0/src/nexuscalc.egg-info/entry_points.txt +3 -0
- nexuscalc-1.0.0/src/nexuscalc.egg-info/top_level.txt +1 -0
nexuscalc-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Light Bulb Experiments
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
nexuscalc-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: nexuscalc
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A powerful calculator application with an interactive CLI
|
|
5
|
+
Home-page: https://github.com/yourusername/nexuscalc
|
|
6
|
+
Author: NexusCalc Team
|
|
7
|
+
License: UNKNOWN
|
|
8
|
+
Platform: UNKNOWN
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.6
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
|
|
16
|
+
# NexusCalc
|
|
17
|
+
|
|
18
|
+
A powerful calculator application with an interactive command-line interface.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- ➕ Addition
|
|
23
|
+
- ➖ Subtraction
|
|
24
|
+
- ✖️ Multiplication
|
|
25
|
+
- ➗ Division with zero division error handling
|
|
26
|
+
- 🏠 Floor division
|
|
27
|
+
- 🔢 Mathematical constants (pi, e, tau)
|
|
28
|
+
- 📐 Advanced mathematical functions (sqrt, sin, cos, tan, log)
|
|
29
|
+
- 🛡️ Comprehensive error handling
|
|
30
|
+
- 💎 Clean number formatting
|
|
31
|
+
- 🎨 Colorful and interactive CLI
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python -m pip install nexuscalc
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### Interactive Mode
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from nexuscalc import nexuscalc
|
|
45
|
+
nexuscalc.calculate()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import nexuscalc
|
|
52
|
+
nexuscalc.nexuscalc.calculate()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or after installation:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
nexuscalc
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Programmatic Usage
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from nexuscalc.core.operations import Operations
|
|
65
|
+
|
|
66
|
+
ops = Operations()
|
|
67
|
+
result = ops.add(5, 3) # Returns 8
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Advanced Expression Evaluation
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from nexuscalc.evaluator.evaluator import Evaluator
|
|
74
|
+
|
|
75
|
+
eval = Evaluator()
|
|
76
|
+
result = eval.evaluate_expression("2 + 3 * 4") # Returns 14
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Error Handling
|
|
80
|
+
|
|
81
|
+
NexusCalc includes comprehensive error handling for:
|
|
82
|
+
|
|
83
|
+
- Division by zero (with friendly error messages)
|
|
84
|
+
- Invalid input formats
|
|
85
|
+
- Numeric overflow
|
|
86
|
+
- Expression parsing errors
|
|
87
|
+
- Function evaluation errors
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
Run tests:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python -m pytest tests/ -v
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Run tests with coverage:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
python -m pytest tests/ --cov=src/nexuscalc
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Project Structure
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
nexuscalc/
|
|
107
|
+
├── src/nexuscalc/
|
|
108
|
+
│ ├── core/ # Core calculator logic
|
|
109
|
+
│ ├── parsers/ # Expression parsing
|
|
110
|
+
│ ├── evaluator/ # Expression evaluation
|
|
111
|
+
│ ├── utils/ # Utilities and helpers
|
|
112
|
+
│ └── exceptions/ # Custom exceptions
|
|
113
|
+
├── tests/ # Unit tests
|
|
114
|
+
├── examples/ # Usage examples
|
|
115
|
+
└── setup.py # Package setup
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
|
121
|
+
|
|
122
|
+
## Author
|
|
123
|
+
|
|
124
|
+
Light Bulb Experiments © 2026
|
|
125
|
+
|
|
126
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# NexusCalc
|
|
2
|
+
|
|
3
|
+
A powerful calculator application with an interactive command-line interface.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ➕ Addition
|
|
8
|
+
- ➖ Subtraction
|
|
9
|
+
- ✖️ Multiplication
|
|
10
|
+
- ➗ Division with zero division error handling
|
|
11
|
+
- 🏠 Floor division
|
|
12
|
+
- 🔢 Mathematical constants (pi, e, tau)
|
|
13
|
+
- 📐 Advanced mathematical functions (sqrt, sin, cos, tan, log)
|
|
14
|
+
- 🛡️ Comprehensive error handling
|
|
15
|
+
- 💎 Clean number formatting
|
|
16
|
+
- 🎨 Colorful and interactive CLI
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
python -m pip install nexuscalc
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Interactive Mode
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from nexuscalc import nexuscalc
|
|
30
|
+
nexuscalc.calculate()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import nexuscalc
|
|
37
|
+
nexuscalc.nexuscalc.calculate()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or after installation:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
nexuscalc
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Programmatic Usage
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from nexuscalc.core.operations import Operations
|
|
50
|
+
|
|
51
|
+
ops = Operations()
|
|
52
|
+
result = ops.add(5, 3) # Returns 8
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Advanced Expression Evaluation
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from nexuscalc.evaluator.evaluator import Evaluator
|
|
59
|
+
|
|
60
|
+
eval = Evaluator()
|
|
61
|
+
result = eval.evaluate_expression("2 + 3 * 4") # Returns 14
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Error Handling
|
|
65
|
+
|
|
66
|
+
NexusCalc includes comprehensive error handling for:
|
|
67
|
+
|
|
68
|
+
- Division by zero (with friendly error messages)
|
|
69
|
+
- Invalid input formats
|
|
70
|
+
- Numeric overflow
|
|
71
|
+
- Expression parsing errors
|
|
72
|
+
- Function evaluation errors
|
|
73
|
+
|
|
74
|
+
## Development
|
|
75
|
+
|
|
76
|
+
Run tests:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python -m pytest tests/ -v
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Run tests with coverage:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
python -m pytest tests/ --cov=src/nexuscalc
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Project Structure
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
nexuscalc/
|
|
92
|
+
├── src/nexuscalc/
|
|
93
|
+
│ ├── core/ # Core calculator logic
|
|
94
|
+
│ ├── parsers/ # Expression parsing
|
|
95
|
+
│ ├── evaluator/ # Expression evaluation
|
|
96
|
+
│ ├── utils/ # Utilities and helpers
|
|
97
|
+
│ └── exceptions/ # Custom exceptions
|
|
98
|
+
├── tests/ # Unit tests
|
|
99
|
+
├── examples/ # Usage examples
|
|
100
|
+
└── setup.py # Package setup
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
|
106
|
+
|
|
107
|
+
## Author
|
|
108
|
+
|
|
109
|
+
Light Bulb Experiments © 2026
|
nexuscalc-1.0.0/setup.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Setup configuration for NexusCalc."""
|
|
2
|
+
|
|
3
|
+
from setuptools import setup, find_packages
|
|
4
|
+
|
|
5
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
|
6
|
+
long_description = fh.read()
|
|
7
|
+
|
|
8
|
+
setup(
|
|
9
|
+
name="nexuscalc",
|
|
10
|
+
version="1.0.0",
|
|
11
|
+
author="NexusCalc Team",
|
|
12
|
+
description="A powerful calculator application with an interactive CLI",
|
|
13
|
+
long_description=long_description,
|
|
14
|
+
long_description_content_type="text/markdown",
|
|
15
|
+
url="https://github.com/yourusername/nexuscalc",
|
|
16
|
+
package_dir={"": "src"},
|
|
17
|
+
packages=find_packages(where="src"),
|
|
18
|
+
classifiers=[
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
],
|
|
23
|
+
python_requires=">=3.6",
|
|
24
|
+
entry_points={
|
|
25
|
+
"console_scripts": [
|
|
26
|
+
"nexuscalc=nexuscalc:calculate",
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""NexusCalc - A powerful calculator application."""
|
|
2
|
+
|
|
3
|
+
from .core.calculator import NexusCalc
|
|
4
|
+
|
|
5
|
+
__version__ = "1.0.0"
|
|
6
|
+
__all__ = ["NexusCalc", "calculate", "nexuscalc"]
|
|
7
|
+
|
|
8
|
+
def calculate():
|
|
9
|
+
"""Main entry point for the calculator."""
|
|
10
|
+
calc = NexusCalc()
|
|
11
|
+
calc.run()
|
|
12
|
+
|
|
13
|
+
# Create a class that matches the module name for nexuscalc.nexuscalc.calculate()
|
|
14
|
+
class nexuscalc:
|
|
15
|
+
"""Wrapper class for nexuscalc.nexuscalc.calculate() compatibility."""
|
|
16
|
+
@staticmethod
|
|
17
|
+
def calculate():
|
|
18
|
+
"""Main entry point for the calculator."""
|
|
19
|
+
calc = NexusCalc()
|
|
20
|
+
calc.run()
|
|
21
|
+
|
|
22
|
+
# Also expose NexusCalc as a class
|
|
23
|
+
NexusCalc = NexusCalc
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core calculator module."""
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Main calculator implementation."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import re
|
|
5
|
+
from ..core.operations import Operations
|
|
6
|
+
from ..utils.validators import validate_number
|
|
7
|
+
from ..utils.formatters import format_result
|
|
8
|
+
from ..exceptions.calculator_errors import CalculatorError, DivisionByZeroError
|
|
9
|
+
|
|
10
|
+
class NexusCalc:
|
|
11
|
+
"""Main calculator class with interactive menu."""
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.operations = Operations()
|
|
15
|
+
self.running = True
|
|
16
|
+
self.menu_options = {
|
|
17
|
+
'1': ('Add', self.operations.add),
|
|
18
|
+
'2': ('Subtract', self.operations.subtract),
|
|
19
|
+
'3': ('Multiply', self.operations.multiply),
|
|
20
|
+
'4': ('Divide', self.operations.divide),
|
|
21
|
+
'5': ('Floor', self.operations.floor_divide),
|
|
22
|
+
}
|
|
23
|
+
# Regex patterns for quit commands
|
|
24
|
+
self.quit_patterns = [
|
|
25
|
+
re.compile(r'^[Qq]$'), # q or Q
|
|
26
|
+
re.compile(r'^[Qq][Uu][Ii][Tt]$'), # quit in any case
|
|
27
|
+
re.compile(r'^6$'), # number 6
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
def is_quit_command(self, text):
|
|
31
|
+
"""Check if the input matches any quit pattern."""
|
|
32
|
+
if not text:
|
|
33
|
+
return False
|
|
34
|
+
text = text.strip()
|
|
35
|
+
for pattern in self.quit_patterns:
|
|
36
|
+
if pattern.match(text):
|
|
37
|
+
return True
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
def run(self):
|
|
41
|
+
"""Run the calculator main loop."""
|
|
42
|
+
try:
|
|
43
|
+
while self.running:
|
|
44
|
+
try:
|
|
45
|
+
self.show_menu()
|
|
46
|
+
choice = self.get_input("Use 1-5").strip().lower()
|
|
47
|
+
|
|
48
|
+
# Check if it's a quit command using regex
|
|
49
|
+
if self.is_quit_command(choice):
|
|
50
|
+
print("\n👋 Goodbye! Thanks for using NexusCalc!")
|
|
51
|
+
self.running = False
|
|
52
|
+
break
|
|
53
|
+
|
|
54
|
+
if choice not in self.menu_options:
|
|
55
|
+
print("❌ Invalid choice. Please select 1-5 or 6/q/quit to exit.")
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
operation_name, operation_func = self.menu_options[choice]
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
num1 = self.get_number("Enter first number")
|
|
62
|
+
num2 = self.get_number("Enter second number")
|
|
63
|
+
|
|
64
|
+
result = operation_func(num1, num2)
|
|
65
|
+
print(f"\n✅ Result: {format_result(result)}")
|
|
66
|
+
|
|
67
|
+
except DivisionByZeroError as e:
|
|
68
|
+
print(f"\n❌ Division Error: {e}")
|
|
69
|
+
print("💡 Hint: You cannot divide by zero. Please try a different number.")
|
|
70
|
+
except CalculatorError as e:
|
|
71
|
+
print(f"\n❌ Error: {e}")
|
|
72
|
+
except ValueError:
|
|
73
|
+
print("\n❌ Error: Invalid number format. Please enter a valid number.")
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print(f"\n❌ Unexpected error: {e}")
|
|
76
|
+
|
|
77
|
+
print()
|
|
78
|
+
|
|
79
|
+
except KeyboardInterrupt:
|
|
80
|
+
print("\n\n⚠️ Operation cancelled by user.")
|
|
81
|
+
print("💡 Press Ctrl+C again to exit, or continue using the calculator.")
|
|
82
|
+
continue
|
|
83
|
+
except EOFError:
|
|
84
|
+
print("\n\n⚠️ Input stream closed.")
|
|
85
|
+
print("💡 Exiting calculator...")
|
|
86
|
+
self.running = False
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
except KeyboardInterrupt:
|
|
90
|
+
print("\n\n👋 Goodbye! Calculator exited.")
|
|
91
|
+
sys.exit(0)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"\n❌ Fatal error: {e}")
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
def show_menu(self):
|
|
97
|
+
"""Display the main menu."""
|
|
98
|
+
print("\n" + "="*50)
|
|
99
|
+
print("🔢 NEXUSCALC - Powerful Calculator")
|
|
100
|
+
print("="*50)
|
|
101
|
+
print("\nWhat are you trying to calculate?")
|
|
102
|
+
print("List:")
|
|
103
|
+
print()
|
|
104
|
+
print("1. Add ➕")
|
|
105
|
+
print("2. Subtract ➖")
|
|
106
|
+
print("3. Multiply ✖️")
|
|
107
|
+
print("4. Divide ➗")
|
|
108
|
+
print("5. Floor 🏠")
|
|
109
|
+
print("6. Quit 🚪")
|
|
110
|
+
print()
|
|
111
|
+
print("💡 Press Ctrl+C to cancel operation")
|
|
112
|
+
print("💡 Type '6', 'q', 'Q', 'quit', or 'QUIT' to exit")
|
|
113
|
+
print("-"*50)
|
|
114
|
+
|
|
115
|
+
def get_input(self, prompt):
|
|
116
|
+
"""Get user input with NEXUSCALC > prompt."""
|
|
117
|
+
try:
|
|
118
|
+
return input(f"{prompt}\nNEXUSCALC > ")
|
|
119
|
+
except KeyboardInterrupt:
|
|
120
|
+
print("\n\n⚠️ Input cancelled.")
|
|
121
|
+
raise
|
|
122
|
+
except EOFError:
|
|
123
|
+
print("\n\n⚠️ End of input detected.")
|
|
124
|
+
raise
|
|
125
|
+
|
|
126
|
+
def get_number(self, prompt):
|
|
127
|
+
"""Get and validate a number from user input."""
|
|
128
|
+
while True:
|
|
129
|
+
try:
|
|
130
|
+
user_input = self.get_input(prompt)
|
|
131
|
+
return validate_number(user_input)
|
|
132
|
+
except CalculatorError as e:
|
|
133
|
+
print(f"❌ Error: {e}")
|
|
134
|
+
print("💡 Please try again with a valid number.")
|
|
135
|
+
except KeyboardInterrupt:
|
|
136
|
+
print("\n⚠️ Number input cancelled.")
|
|
137
|
+
raise
|
|
138
|
+
except EOFError:
|
|
139
|
+
print("\n⚠️ End of input detected.")
|
|
140
|
+
raise
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Mathematical constants."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
class Constants:
|
|
6
|
+
"""Collection of mathematical constants."""
|
|
7
|
+
|
|
8
|
+
PI = math.pi
|
|
9
|
+
E = math.e
|
|
10
|
+
TAU = math.tau
|
|
11
|
+
INF = float('inf')
|
|
12
|
+
NAN = float('nan')
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def get_constant(cls, name):
|
|
16
|
+
"""Get a constant by name."""
|
|
17
|
+
constants = {
|
|
18
|
+
'pi': cls.PI,
|
|
19
|
+
'e': cls.E,
|
|
20
|
+
'tau': cls.TAU,
|
|
21
|
+
'inf': cls.INF,
|
|
22
|
+
'infinity': cls.INF,
|
|
23
|
+
'nan': cls.NAN
|
|
24
|
+
}
|
|
25
|
+
return constants.get(name.lower())
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def list_constants(cls):
|
|
29
|
+
"""List all available constants."""
|
|
30
|
+
return ['pi', 'e', 'tau', 'inf', 'nan']
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Mathematical operations with proper error handling."""
|
|
2
|
+
|
|
3
|
+
from ..exceptions.calculator_errors import DivisionByZeroError, CalculatorError
|
|
4
|
+
|
|
5
|
+
class Operations:
|
|
6
|
+
"""Collection of mathematical operations."""
|
|
7
|
+
|
|
8
|
+
@staticmethod
|
|
9
|
+
def add(a, b):
|
|
10
|
+
"""Add two numbers."""
|
|
11
|
+
return a + b
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def subtract(a, b):
|
|
15
|
+
"""Subtract b from a."""
|
|
16
|
+
return a - b
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def multiply(a, b):
|
|
20
|
+
"""Multiply two numbers."""
|
|
21
|
+
return a * b
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def divide(a, b):
|
|
25
|
+
"""Divide a by b with zero division handling."""
|
|
26
|
+
if b == 0:
|
|
27
|
+
raise DivisionByZeroError("Cannot divide by zero!")
|
|
28
|
+
if a == 0:
|
|
29
|
+
return 0.0
|
|
30
|
+
return a / b
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def floor_divide(a, b):
|
|
34
|
+
"""Floor divide a by b with zero division handling."""
|
|
35
|
+
if b == 0:
|
|
36
|
+
raise DivisionByZeroError("Cannot floor divide by zero!")
|
|
37
|
+
if a == 0:
|
|
38
|
+
return 0
|
|
39
|
+
return a // b
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def safe_divide(a, b):
|
|
43
|
+
"""Safe division that returns None on error instead of raising."""
|
|
44
|
+
try:
|
|
45
|
+
return a / b
|
|
46
|
+
except ZeroDivisionError:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def safe_floor_divide(a, b):
|
|
51
|
+
"""Safe floor division that returns None on error instead of raising."""
|
|
52
|
+
try:
|
|
53
|
+
return a // b
|
|
54
|
+
except ZeroDivisionError:
|
|
55
|
+
return None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Evaluator modules."""
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Expression evaluator for advanced calculations."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from ..exceptions.calculator_errors import EvaluationError, DivisionByZeroError
|
|
5
|
+
from ..parsers.ast_nodes import NumberNode, BinOpNode, UnaryOpNode, FunctionNode, ConstantNode
|
|
6
|
+
from ..core.constants import Constants
|
|
7
|
+
|
|
8
|
+
class Evaluator:
|
|
9
|
+
"""Evaluate parsed expressions."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.constants = Constants()
|
|
13
|
+
self.functions = {
|
|
14
|
+
'sqrt': math.sqrt,
|
|
15
|
+
'sin': math.sin,
|
|
16
|
+
'cos': math.cos,
|
|
17
|
+
'tan': math.tan,
|
|
18
|
+
'log': math.log,
|
|
19
|
+
'abs': abs,
|
|
20
|
+
'ceil': math.ceil,
|
|
21
|
+
'floor': math.floor,
|
|
22
|
+
'round': round,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def evaluate(self, ast):
|
|
26
|
+
"""Evaluate an AST and return the result."""
|
|
27
|
+
if isinstance(ast, NumberNode):
|
|
28
|
+
return ast.value
|
|
29
|
+
|
|
30
|
+
elif isinstance(ast, BinOpNode):
|
|
31
|
+
left_val = self.evaluate(ast.left)
|
|
32
|
+
right_val = self.evaluate(ast.right)
|
|
33
|
+
|
|
34
|
+
if ast.operator == '+':
|
|
35
|
+
return left_val + right_val
|
|
36
|
+
elif ast.operator == '-':
|
|
37
|
+
return left_val - right_val
|
|
38
|
+
elif ast.operator == '*':
|
|
39
|
+
return left_val * right_val
|
|
40
|
+
elif ast.operator == '/':
|
|
41
|
+
if right_val == 0:
|
|
42
|
+
raise DivisionByZeroError("Division by zero in expression")
|
|
43
|
+
return left_val / right_val
|
|
44
|
+
elif ast.operator == '//':
|
|
45
|
+
if right_val == 0:
|
|
46
|
+
raise DivisionByZeroError("Floor division by zero in expression")
|
|
47
|
+
return left_val // right_val
|
|
48
|
+
else:
|
|
49
|
+
raise EvaluationError(f"Unknown binary operator: {ast.operator}")
|
|
50
|
+
|
|
51
|
+
elif isinstance(ast, UnaryOpNode):
|
|
52
|
+
operand_val = self.evaluate(ast.operand)
|
|
53
|
+
if ast.operator == '-':
|
|
54
|
+
return -operand_val
|
|
55
|
+
elif ast.operator == '+':
|
|
56
|
+
return +operand_val
|
|
57
|
+
else:
|
|
58
|
+
raise EvaluationError(f"Unknown unary operator: {ast.operator}")
|
|
59
|
+
|
|
60
|
+
elif isinstance(ast, FunctionNode):
|
|
61
|
+
args = [self.evaluate(arg) for arg in ast.args]
|
|
62
|
+
if ast.name in self.functions:
|
|
63
|
+
try:
|
|
64
|
+
return self.functions[ast.name](*args)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
raise EvaluationError(f"Error evaluating function {ast.name}: {e}")
|
|
67
|
+
else:
|
|
68
|
+
raise EvaluationError(f"Unknown function: {ast.name}")
|
|
69
|
+
|
|
70
|
+
elif isinstance(ast, ConstantNode):
|
|
71
|
+
return ast.value
|
|
72
|
+
|
|
73
|
+
else:
|
|
74
|
+
raise EvaluationError(f"Unknown AST node type: {type(ast)}")
|
|
75
|
+
|
|
76
|
+
def evaluate_expression(self, expression):
|
|
77
|
+
"""Convenience method to parse and evaluate an expression."""
|
|
78
|
+
from ..parsers.expression_parser import ExpressionParser
|
|
79
|
+
parser = ExpressionParser()
|
|
80
|
+
ast = parser.parse(expression)
|
|
81
|
+
return self.evaluate(ast)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Custom mathematical functions."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from nexuscalc.exceptions.calculator_errors import CalculatorError
|
|
6
|
+
|
|
7
|
+
class MathFunctions:
|
|
8
|
+
"""Collection of mathematical functions."""
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def sqrt(x):
|
|
12
|
+
"""Square root."""
|
|
13
|
+
if x < 0:
|
|
14
|
+
raise CalculatorError("Cannot take square root of negative number")
|
|
15
|
+
return math.sqrt(x)
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def sin(x):
|
|
19
|
+
"""Sine in radians."""
|
|
20
|
+
return math.sin(x)
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def cos(x):
|
|
24
|
+
"""Cosine in radians."""
|
|
25
|
+
return math.cos(x)
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def tan(x):
|
|
29
|
+
"""Tangent in radians."""
|
|
30
|
+
return math.tan(x)
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def log(x, base=math.e):
|
|
34
|
+
"""Logarithm with specified base."""
|
|
35
|
+
if x <= 0:
|
|
36
|
+
raise CalculatorError("Logarithm only defined for positive numbers")
|
|
37
|
+
if base <= 0 or base == 1:
|
|
38
|
+
raise CalculatorError("Logarithm base must be positive and not equal to 1")
|
|
39
|
+
return math.log(x, base)
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def log10(x):
|
|
43
|
+
"""Base-10 logarithm."""
|
|
44
|
+
return MathFunctions.log(x, 10)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def log2(x):
|
|
48
|
+
"""Base-2 logarithm."""
|
|
49
|
+
return MathFunctions.log(x, 2)
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def exp(x):
|
|
53
|
+
"""Exponential function e^x."""
|
|
54
|
+
return math.exp(x)
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def pow(x, y):
|
|
58
|
+
"""Power function x^y."""
|
|
59
|
+
return x ** y
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def factorial(x):
|
|
63
|
+
"""Factorial function."""
|
|
64
|
+
if x < 0:
|
|
65
|
+
raise CalculatorError("Factorial not defined for negative numbers")
|
|
66
|
+
if not isinstance(x, int) and x.is_integer():
|
|
67
|
+
x = int(x)
|
|
68
|
+
if not isinstance(x, int):
|
|
69
|
+
raise CalculatorError("Factorial only defined for integers")
|
|
70
|
+
return math.factorial(x)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def gcd(a, b):
|
|
74
|
+
"""Greatest common divisor."""
|
|
75
|
+
return math.gcd(int(a), int(b))
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def lcm(a, b):
|
|
79
|
+
"""Least common multiple."""
|
|
80
|
+
return abs(a * b) // math.gcd(int(a), int(b))
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def max(*args):
|
|
84
|
+
"""Maximum value."""
|
|
85
|
+
return max(args)
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def min(*args):
|
|
89
|
+
"""Minimum value."""
|
|
90
|
+
return min(args)
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def average(*args):
|
|
94
|
+
"""Average value."""
|
|
95
|
+
if not args:
|
|
96
|
+
raise CalculatorError("At least one number required for average")
|
|
97
|
+
return sum(args) / len(args)
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def sum(*args):
|
|
101
|
+
"""Sum of numbers."""
|
|
102
|
+
return sum(args)
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def product(*args):
|
|
106
|
+
"""Product of numbers."""
|
|
107
|
+
result = 1
|
|
108
|
+
for arg in args:
|
|
109
|
+
result *= arg
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def is_prime(n):
|
|
114
|
+
"""Check if number is prime."""
|
|
115
|
+
if n < 2:
|
|
116
|
+
return False
|
|
117
|
+
if n == 2:
|
|
118
|
+
return True
|
|
119
|
+
if n % 2 == 0:
|
|
120
|
+
return False
|
|
121
|
+
for i in range(3, int(math.sqrt(n)) + 1, 2):
|
|
122
|
+
if n % i == 0:
|
|
123
|
+
return False
|
|
124
|
+
return True
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Exception modules."""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Custom exceptions for the calculator."""
|
|
2
|
+
|
|
3
|
+
class CalculatorError(Exception):
|
|
4
|
+
"""Base exception for calculator errors."""
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
class DivisionByZeroError(CalculatorError):
|
|
8
|
+
"""Exception raised when attempting to divide by zero."""
|
|
9
|
+
def __init__(self, message="Cannot divide by zero!"):
|
|
10
|
+
self.message = message
|
|
11
|
+
super().__init__(self.message)
|
|
12
|
+
|
|
13
|
+
class ParserError(CalculatorError):
|
|
14
|
+
"""Exception raised for parsing errors."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
class EvaluationError(CalculatorError):
|
|
18
|
+
"""Exception raised for evaluation errors."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
class InvalidInputError(CalculatorError):
|
|
22
|
+
"""Exception raised for invalid input."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
class OverflowError(CalculatorError):
|
|
26
|
+
"""Exception raised for numeric overflow."""
|
|
27
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Parser modules."""
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""AST nodes for parsed expressions."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import List, Any, Optional
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Token:
|
|
8
|
+
"""Token representation."""
|
|
9
|
+
type: str
|
|
10
|
+
value: Any
|
|
11
|
+
position: int = 0
|
|
12
|
+
line: int = 1
|
|
13
|
+
col: int = 1
|
|
14
|
+
|
|
15
|
+
class ASTNode:
|
|
16
|
+
"""Base class for AST nodes."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class NumberNode(ASTNode):
|
|
21
|
+
"""Node for numeric values."""
|
|
22
|
+
value: float
|
|
23
|
+
|
|
24
|
+
def __str__(self):
|
|
25
|
+
return str(self.value)
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class BinOpNode(ASTNode):
|
|
29
|
+
"""Node for binary operations."""
|
|
30
|
+
operator: str
|
|
31
|
+
left: ASTNode
|
|
32
|
+
right: ASTNode
|
|
33
|
+
|
|
34
|
+
def __str__(self):
|
|
35
|
+
return f"({self.left} {self.operator} {self.right})"
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class UnaryOpNode(ASTNode):
|
|
39
|
+
"""Node for unary operations."""
|
|
40
|
+
operator: str
|
|
41
|
+
operand: ASTNode
|
|
42
|
+
|
|
43
|
+
def __str__(self):
|
|
44
|
+
return f"({self.operator}{self.operand})"
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class FunctionNode(ASTNode):
|
|
48
|
+
"""Node for function calls."""
|
|
49
|
+
name: str
|
|
50
|
+
args: List[ASTNode]
|
|
51
|
+
|
|
52
|
+
def __str__(self):
|
|
53
|
+
args_str = ', '.join(str(arg) for arg in self.args)
|
|
54
|
+
return f"{self.name}({args_str})"
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ConstantNode(ASTNode):
|
|
58
|
+
"""Node for constants like pi, e."""
|
|
59
|
+
name: str
|
|
60
|
+
value: float
|
|
61
|
+
|
|
62
|
+
def __str__(self):
|
|
63
|
+
return self.name
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Expression parser for advanced calculations."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from ..exceptions.calculator_errors import ParserError
|
|
5
|
+
from .tokenizer import Tokenizer
|
|
6
|
+
from .ast_nodes import *
|
|
7
|
+
|
|
8
|
+
class ExpressionParser:
|
|
9
|
+
"""Parse mathematical expressions."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.tokenizer = Tokenizer()
|
|
13
|
+
self.current_token = None
|
|
14
|
+
self.tokens = []
|
|
15
|
+
self.position = 0
|
|
16
|
+
|
|
17
|
+
def parse(self, expression):
|
|
18
|
+
"""Parse an expression string into an AST."""
|
|
19
|
+
if not expression or expression.strip() == '':
|
|
20
|
+
raise ParserError("Expression cannot be empty")
|
|
21
|
+
|
|
22
|
+
self.tokens = self.tokenizer.tokenize(expression)
|
|
23
|
+
self.position = 0
|
|
24
|
+
|
|
25
|
+
if not self.tokens:
|
|
26
|
+
raise ParserError("No valid tokens found in expression")
|
|
27
|
+
|
|
28
|
+
self.current_token = self.tokens[0]
|
|
29
|
+
return self.parse_expression()
|
|
30
|
+
|
|
31
|
+
def parse_expression(self):
|
|
32
|
+
"""Parse an expression (addition/subtraction)."""
|
|
33
|
+
node = self.parse_term()
|
|
34
|
+
|
|
35
|
+
while self.current_token and self.current_token.type in ('PLUS', 'MINUS'):
|
|
36
|
+
if self.current_token.type == 'PLUS':
|
|
37
|
+
self.advance()
|
|
38
|
+
right = self.parse_term()
|
|
39
|
+
node = BinOpNode('+', node, right)
|
|
40
|
+
elif self.current_token.type == 'MINUS':
|
|
41
|
+
self.advance()
|
|
42
|
+
right = self.parse_term()
|
|
43
|
+
node = BinOpNode('-', node, right)
|
|
44
|
+
|
|
45
|
+
return node
|
|
46
|
+
|
|
47
|
+
def parse_term(self):
|
|
48
|
+
"""Parse a term (multiplication/division/floor)."""
|
|
49
|
+
node = self.parse_factor()
|
|
50
|
+
|
|
51
|
+
while self.current_token and self.current_token.type in ('MULTIPLY', 'DIVIDE', 'FLOOR_DIVIDE'):
|
|
52
|
+
if self.current_token.type == 'MULTIPLY':
|
|
53
|
+
self.advance()
|
|
54
|
+
right = self.parse_factor()
|
|
55
|
+
node = BinOpNode('*', node, right)
|
|
56
|
+
elif self.current_token.type == 'DIVIDE':
|
|
57
|
+
self.advance()
|
|
58
|
+
right = self.parse_factor()
|
|
59
|
+
node = BinOpNode('/', node, right)
|
|
60
|
+
elif self.current_token.type == 'FLOOR_DIVIDE':
|
|
61
|
+
self.advance()
|
|
62
|
+
right = self.parse_factor()
|
|
63
|
+
node = BinOpNode('//', node, right)
|
|
64
|
+
|
|
65
|
+
return node
|
|
66
|
+
|
|
67
|
+
def parse_factor(self):
|
|
68
|
+
"""Parse a factor (number, parenthesized expression, function call)."""
|
|
69
|
+
if not self.current_token:
|
|
70
|
+
raise ParserError("Unexpected end of expression")
|
|
71
|
+
|
|
72
|
+
if self.current_token.type == 'NUMBER':
|
|
73
|
+
value = self.current_token.value
|
|
74
|
+
self.advance()
|
|
75
|
+
return NumberNode(value)
|
|
76
|
+
|
|
77
|
+
elif self.current_token.type == 'LPAREN':
|
|
78
|
+
self.advance()
|
|
79
|
+
node = self.parse_expression()
|
|
80
|
+
if not self.current_token or self.current_token.type != 'RPAREN':
|
|
81
|
+
raise ParserError("Missing closing parenthesis")
|
|
82
|
+
self.advance()
|
|
83
|
+
return node
|
|
84
|
+
|
|
85
|
+
elif self.current_token.type == 'FUNCTION':
|
|
86
|
+
func_name = self.current_token.value
|
|
87
|
+
self.advance()
|
|
88
|
+
if not self.current_token or self.current_token.type != 'LPAREN':
|
|
89
|
+
raise ParserError(f"Missing parenthesis after function '{func_name}'")
|
|
90
|
+
self.advance()
|
|
91
|
+
args = []
|
|
92
|
+
if self.current_token and self.current_token.type != 'RPAREN':
|
|
93
|
+
args.append(self.parse_expression())
|
|
94
|
+
while self.current_token and self.current_token.type == 'COMMA':
|
|
95
|
+
self.advance()
|
|
96
|
+
args.append(self.parse_expression())
|
|
97
|
+
if not self.current_token or self.current_token.type != 'RPAREN':
|
|
98
|
+
raise ParserError(f"Missing closing parenthesis for function '{func_name}'")
|
|
99
|
+
self.advance()
|
|
100
|
+
return FunctionNode(func_name, args)
|
|
101
|
+
|
|
102
|
+
else:
|
|
103
|
+
raise ParserError(f"Unexpected token: {self.current_token.type}")
|
|
104
|
+
|
|
105
|
+
def advance(self):
|
|
106
|
+
"""Advance to the next token."""
|
|
107
|
+
self.position += 1
|
|
108
|
+
if self.position < len(self.tokens):
|
|
109
|
+
self.current_token = self.tokens[self.position]
|
|
110
|
+
else:
|
|
111
|
+
self.current_token = None
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Tokenizer for mathematical expressions."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from .ast_nodes import Token
|
|
5
|
+
|
|
6
|
+
class Tokenizer:
|
|
7
|
+
"""Convert expression string into tokens."""
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.token_patterns = [
|
|
11
|
+
('NUMBER', r'\d+\.?\d*'),
|
|
12
|
+
('FUNCTION', r'(?:sqrt|sin|cos|tan|log|abs|ceil|floor|round)\b'),
|
|
13
|
+
('PLUS', r'\+'),
|
|
14
|
+
('MINUS', r'-'),
|
|
15
|
+
('MULTIPLY', r'\*'),
|
|
16
|
+
('DIVIDE', r'/'),
|
|
17
|
+
('FLOOR_DIVIDE', r'//'),
|
|
18
|
+
('LPAREN', r'\('),
|
|
19
|
+
('RPAREN', r'\)'),
|
|
20
|
+
('COMMA', r','),
|
|
21
|
+
('WHITESPACE', r'\s+'),
|
|
22
|
+
]
|
|
23
|
+
self.token_regex = re.compile(
|
|
24
|
+
'|'.join(f'(?P<{name}>{pattern})' for name, pattern in self.token_patterns)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def tokenize(self, expression):
|
|
28
|
+
"""Tokenize an expression string."""
|
|
29
|
+
tokens = []
|
|
30
|
+
position = 0
|
|
31
|
+
line = 1
|
|
32
|
+
col = 1
|
|
33
|
+
|
|
34
|
+
for match in self.token_regex.finditer(expression):
|
|
35
|
+
token_type = match.lastgroup
|
|
36
|
+
value = match.group()
|
|
37
|
+
|
|
38
|
+
if token_type == 'WHITESPACE':
|
|
39
|
+
# Count newlines for position tracking
|
|
40
|
+
lines = value.count('\n')
|
|
41
|
+
if lines > 0:
|
|
42
|
+
line += lines
|
|
43
|
+
col = len(value) - value.rfind('\n') - 1
|
|
44
|
+
else:
|
|
45
|
+
col += len(value)
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
tokens.append(Token(token_type, value, position, line, col))
|
|
49
|
+
position += len(value)
|
|
50
|
+
col += len(value)
|
|
51
|
+
|
|
52
|
+
return tokens
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utility modules."""
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Output formatting utilities."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
def format_result(result):
|
|
6
|
+
"""Format the result for display."""
|
|
7
|
+
if isinstance(result, float):
|
|
8
|
+
# Handle special values
|
|
9
|
+
if math.isnan(result):
|
|
10
|
+
return "NaN (Not a Number)"
|
|
11
|
+
if math.isinf(result):
|
|
12
|
+
return "Infinity" if result > 0 else "-Infinity"
|
|
13
|
+
|
|
14
|
+
# Remove trailing zeros for cleaner display
|
|
15
|
+
if result.is_integer():
|
|
16
|
+
return str(int(result))
|
|
17
|
+
# Limit decimal places to 10
|
|
18
|
+
return f"{result:.10f}".rstrip('0').rstrip('.')
|
|
19
|
+
return str(result)
|
|
20
|
+
|
|
21
|
+
def format_expression(operation, num1, num2, result):
|
|
22
|
+
"""Format a complete mathematical expression."""
|
|
23
|
+
symbols = {
|
|
24
|
+
'add': '+',
|
|
25
|
+
'subtract': '-',
|
|
26
|
+
'multiply': '×',
|
|
27
|
+
'divide': '÷',
|
|
28
|
+
'floor': '//'
|
|
29
|
+
}
|
|
30
|
+
symbol = symbols.get(operation, '?')
|
|
31
|
+
return f"{format_result(num1)} {symbol} {format_result(num2)} = {format_result(result)}"
|
|
32
|
+
|
|
33
|
+
def format_table(data, headers=None):
|
|
34
|
+
"""Format data as a table."""
|
|
35
|
+
if not data:
|
|
36
|
+
return ""
|
|
37
|
+
|
|
38
|
+
if headers:
|
|
39
|
+
rows = [headers] + data
|
|
40
|
+
else:
|
|
41
|
+
rows = data
|
|
42
|
+
|
|
43
|
+
# Calculate column widths
|
|
44
|
+
col_widths = []
|
|
45
|
+
for col in range(len(rows[0])):
|
|
46
|
+
max_width = max(len(str(row[col])) for row in rows)
|
|
47
|
+
col_widths.append(max_width + 2)
|
|
48
|
+
|
|
49
|
+
# Build table
|
|
50
|
+
result = []
|
|
51
|
+
for i, row in enumerate(rows):
|
|
52
|
+
line = "|"
|
|
53
|
+
for j, cell in enumerate(row):
|
|
54
|
+
line += f" {str(cell).ljust(col_widths[j] - 1)}|"
|
|
55
|
+
result.append(line)
|
|
56
|
+
|
|
57
|
+
# Add header separator
|
|
58
|
+
if i == 0 and headers:
|
|
59
|
+
separator = "+"
|
|
60
|
+
for width in col_widths:
|
|
61
|
+
separator += "-" * width + "+"
|
|
62
|
+
result.insert(1, separator)
|
|
63
|
+
|
|
64
|
+
return "\n".join(result)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Input validation utilities."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from ..exceptions.calculator_errors import CalculatorError
|
|
5
|
+
|
|
6
|
+
def validate_number(value):
|
|
7
|
+
"""Validate and convert a string to a number (int or float)."""
|
|
8
|
+
if not value or value.strip() == '':
|
|
9
|
+
raise CalculatorError("Input cannot be empty")
|
|
10
|
+
|
|
11
|
+
value = value.strip().lower()
|
|
12
|
+
|
|
13
|
+
# Check for special values
|
|
14
|
+
if value in ['inf', 'infinity']:
|
|
15
|
+
return float('inf')
|
|
16
|
+
if value == '-inf':
|
|
17
|
+
return float('-inf')
|
|
18
|
+
if value in ['nan', 'not a number']:
|
|
19
|
+
return float('nan')
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
# Try int first
|
|
23
|
+
return int(value)
|
|
24
|
+
except ValueError:
|
|
25
|
+
try:
|
|
26
|
+
# Try float
|
|
27
|
+
return float(value)
|
|
28
|
+
except ValueError:
|
|
29
|
+
raise CalculatorError(f"'{value}' is not a valid number")
|
|
30
|
+
|
|
31
|
+
def is_valid_number(value):
|
|
32
|
+
"""Check if a value is a valid number (not NaN or Infinity)."""
|
|
33
|
+
if isinstance(value, (int, float)):
|
|
34
|
+
return not (math.isnan(value) or math.isinf(value))
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
def is_integer(value):
|
|
38
|
+
"""Check if a value is an integer."""
|
|
39
|
+
if isinstance(value, int):
|
|
40
|
+
return True
|
|
41
|
+
if isinstance(value, float):
|
|
42
|
+
return value.is_integer()
|
|
43
|
+
return False
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: nexuscalc
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A powerful calculator application with an interactive CLI
|
|
5
|
+
Home-page: https://github.com/yourusername/nexuscalc
|
|
6
|
+
Author: NexusCalc Team
|
|
7
|
+
License: UNKNOWN
|
|
8
|
+
Platform: UNKNOWN
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.6
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
|
|
16
|
+
# NexusCalc
|
|
17
|
+
|
|
18
|
+
A powerful calculator application with an interactive command-line interface.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- ➕ Addition
|
|
23
|
+
- ➖ Subtraction
|
|
24
|
+
- ✖️ Multiplication
|
|
25
|
+
- ➗ Division with zero division error handling
|
|
26
|
+
- 🏠 Floor division
|
|
27
|
+
- 🔢 Mathematical constants (pi, e, tau)
|
|
28
|
+
- 📐 Advanced mathematical functions (sqrt, sin, cos, tan, log)
|
|
29
|
+
- 🛡️ Comprehensive error handling
|
|
30
|
+
- 💎 Clean number formatting
|
|
31
|
+
- 🎨 Colorful and interactive CLI
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python -m pip install nexuscalc
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### Interactive Mode
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from nexuscalc import nexuscalc
|
|
45
|
+
nexuscalc.calculate()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import nexuscalc
|
|
52
|
+
nexuscalc.nexuscalc.calculate()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or after installation:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
nexuscalc
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Programmatic Usage
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from nexuscalc.core.operations import Operations
|
|
65
|
+
|
|
66
|
+
ops = Operations()
|
|
67
|
+
result = ops.add(5, 3) # Returns 8
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Advanced Expression Evaluation
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from nexuscalc.evaluator.evaluator import Evaluator
|
|
74
|
+
|
|
75
|
+
eval = Evaluator()
|
|
76
|
+
result = eval.evaluate_expression("2 + 3 * 4") # Returns 14
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Error Handling
|
|
80
|
+
|
|
81
|
+
NexusCalc includes comprehensive error handling for:
|
|
82
|
+
|
|
83
|
+
- Division by zero (with friendly error messages)
|
|
84
|
+
- Invalid input formats
|
|
85
|
+
- Numeric overflow
|
|
86
|
+
- Expression parsing errors
|
|
87
|
+
- Function evaluation errors
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
Run tests:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python -m pytest tests/ -v
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Run tests with coverage:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
python -m pytest tests/ --cov=src/nexuscalc
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Project Structure
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
nexuscalc/
|
|
107
|
+
├── src/nexuscalc/
|
|
108
|
+
│ ├── core/ # Core calculator logic
|
|
109
|
+
│ ├── parsers/ # Expression parsing
|
|
110
|
+
│ ├── evaluator/ # Expression evaluation
|
|
111
|
+
│ ├── utils/ # Utilities and helpers
|
|
112
|
+
│ └── exceptions/ # Custom exceptions
|
|
113
|
+
├── tests/ # Unit tests
|
|
114
|
+
├── examples/ # Usage examples
|
|
115
|
+
└── setup.py # Package setup
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
|
121
|
+
|
|
122
|
+
## Author
|
|
123
|
+
|
|
124
|
+
Light Bulb Experiments © 2026
|
|
125
|
+
|
|
126
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
src/nexuscalc/__init__.py
|
|
5
|
+
src/nexuscalc.egg-info/PKG-INFO
|
|
6
|
+
src/nexuscalc.egg-info/SOURCES.txt
|
|
7
|
+
src/nexuscalc.egg-info/dependency_links.txt
|
|
8
|
+
src/nexuscalc.egg-info/entry_points.txt
|
|
9
|
+
src/nexuscalc.egg-info/top_level.txt
|
|
10
|
+
src/nexuscalc/core/__init__.py
|
|
11
|
+
src/nexuscalc/core/calculator.py
|
|
12
|
+
src/nexuscalc/core/constants.py
|
|
13
|
+
src/nexuscalc/core/operations.py
|
|
14
|
+
src/nexuscalc/evaluator/__init__.py
|
|
15
|
+
src/nexuscalc/evaluator/evaluator.py
|
|
16
|
+
src/nexuscalc/evaluator/functions.py
|
|
17
|
+
src/nexuscalc/exceptions/__init__.py
|
|
18
|
+
src/nexuscalc/exceptions/calculator_errors.py
|
|
19
|
+
src/nexuscalc/parsers/__init__.py
|
|
20
|
+
src/nexuscalc/parsers/ast_nodes.py
|
|
21
|
+
src/nexuscalc/parsers/expression_parser.py
|
|
22
|
+
src/nexuscalc/parsers/tokenizer.py
|
|
23
|
+
src/nexuscalc/utils/__init__.py
|
|
24
|
+
src/nexuscalc/utils/formatters.py
|
|
25
|
+
src/nexuscalc/utils/validators.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nexuscalc
|