math-engine 0.5.0__tar.gz → 0.6.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.
Files changed (21) hide show
  1. {math_engine-0.5.0 → math_engine-0.6.0}/PKG-INFO +56 -42
  2. {math_engine-0.5.0 → math_engine-0.6.0}/README.md +55 -41
  3. {math_engine-0.5.0 → math_engine-0.6.0}/math_engine/AST_Node_Types.py +36 -59
  4. math_engine-0.6.0/math_engine/__init__.py +188 -0
  5. {math_engine-0.5.0 → math_engine-0.6.0}/math_engine/calculator.py +337 -324
  6. {math_engine-0.5.0 → math_engine-0.6.0}/math_engine/cli.py +4 -3
  7. {math_engine-0.5.0 → math_engine-0.6.0}/math_engine/config.json +1 -0
  8. {math_engine-0.5.0 → math_engine-0.6.0}/math_engine/config_manager.py +22 -1
  9. {math_engine-0.5.0 → math_engine-0.6.0}/math_engine/error.py +5 -2
  10. {math_engine-0.5.0 → math_engine-0.6.0}/math_engine/non_decimal_utility.py +1 -1
  11. {math_engine-0.5.0 → math_engine-0.6.0}/math_engine.egg-info/PKG-INFO +56 -42
  12. {math_engine-0.5.0 → math_engine-0.6.0}/math_engine.egg-info/entry_points.txt +1 -0
  13. {math_engine-0.5.0 → math_engine-0.6.0}/pyproject.toml +2 -1
  14. math_engine-0.5.0/math_engine/__init__.py +0 -128
  15. {math_engine-0.5.0 → math_engine-0.6.0}/LICENSE +0 -0
  16. {math_engine-0.5.0 → math_engine-0.6.0}/math_engine/ScientificEngine.py +0 -0
  17. {math_engine-0.5.0 → math_engine-0.6.0}/math_engine/utility.py +0 -0
  18. {math_engine-0.5.0 → math_engine-0.6.0}/math_engine.egg-info/SOURCES.txt +0 -0
  19. {math_engine-0.5.0 → math_engine-0.6.0}/math_engine.egg-info/dependency_links.txt +0 -0
  20. {math_engine-0.5.0 → math_engine-0.6.0}/math_engine.egg-info/top_level.txt +0 -0
  21. {math_engine-0.5.0 → math_engine-0.6.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: math-engine
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: A fast and secure mathematical expression evaluator.
5
5
  Author-email: Jan Teske <jan.teske.06@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/JanTeske06/math_engine
@@ -14,7 +14,7 @@ Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
 
16
16
 
17
- # Math Engine 0.5.0
17
+ # Math Engine v0.6.0
18
18
 
19
19
  [![PyPI Version](https://img.shields.io/pypi/v/math-engine.svg)](https://pypi.org/project/math-engine/)
20
20
  [![License: MIT](https://img.shields.io/pypi/l/math-engine.svg)](https://opensource.org/licenses/MIT)
@@ -354,7 +354,6 @@ All bitwise functions:
354
354
  - support overflow/wrap-around behavior
355
355
  - fully support binary, hex, decimal, and octal inputs
356
356
  - participate in the AST just like standard operators
357
- - support underscores in non-decimal literals (`0b1111_0000`)
358
357
 
359
358
  This makes Math Engine behave like a full-featured programmer’s calculator with CPU-like precision control.
360
359
 
@@ -393,6 +392,8 @@ preset = {
393
392
  # New in 0.3.0
394
393
  "word_size": 0, # 0 = unlimited, or 8, 16, 32, 64
395
394
  "signed_mode": True, # True = Two's Complement, False = Unsigned
395
+ # New in 0.6.0
396
+ "readable_error": False
396
397
  }
397
398
 
398
399
  math_engine.load_preset(preset)
@@ -412,70 +413,83 @@ decimal_places = math_engine.load_one_setting("decimal_places")
412
413
 
413
414
  -----
414
415
 
415
- # Error Handling
416
416
 
417
- Every error is a custom exception with:
417
+ # Error Handling (v0.6.0: Visual & Precise)
418
418
 
419
- * Human-readable message
420
- * Machine-readable error code
421
- * Position (if applicable)
422
- * The original expression
419
+ Math Engine 0.6.0 introduces a dual-mode error handling system designed for both interactive use and strict library integration.
423
420
 
424
- Example:
421
+ ## 1\. Visual Feedback (Default Behavior)
422
+
423
+ By default (`readable_error = True`), the engine catches syntax errors internally and prints a visual diagnostic to the console. This is perfect for CLI tools or quick debugging, as it points exactly to the issue without crashing the program.
425
424
 
426
425
  ```python
427
426
  import math_engine
428
- from math_engine import error as E
429
427
 
430
- try:
431
- math_engine.evaluate("1/0")
432
- except E.CalculationError as e:
433
- print(e.code) # 3003
434
- print(e.message) # "Division by zero"
435
- print(e.equation) # "1/0"
428
+ # readable_error is True by default
429
+ math_engine.evaluate("sin(5")
436
430
  ```
437
431
 
438
- ### Example Error Codes
432
+ **Console Output:**
439
433
 
440
- | Code | Meaning |
441
- | ---- | --------------------------- |
442
- | 3003 | Division by zero |
443
- | 3034 | Empty input |
444
- | 3036 | Multiple = signs |
445
- | 3032 | Multiple-character variable |
446
- | 8000 | Conversion to int failed |
447
- | 8006 | Output conversion error |
434
+ ```text
435
+ Errormessage: Unbalanced parenthesis.
436
+ Code: 2010
437
+ Equation: sin(5
438
+ ^ HERE IS THE PROBLEM (Position: 5)
439
+ ```
448
440
 
449
- For a complete list of all error codes and their meanings, please see the **[Error Codes Reference](https://github.com/JanTeske06/math_engine/blob/master/ERRORS.md)**.
441
+ ## 2\. Programmatic Handling (Exceptions)
450
442
 
451
- -----
443
+ If you are building an application or running unit tests, you likely want to catch exceptions instead of printing to stdout. You can disable `readable_error` to raise standard `MathError` exceptions.
452
444
 
453
- # Testing and Reliability
445
+ The exception object carries **precise start and end indices**:
454
446
 
455
- math\_engine is designed with testing in mind:
447
+ * `e.position_start` (int): Index where the error begins.
448
+ * `e.position_end` (int): Index where the error ends.
456
449
 
457
- * Full error-code consistency
458
- * Strict syntax rules
459
- * Unit-test friendly behavior
460
- * No reliance on Python’s runtime execution
450
+ <!-- end list -->
461
451
 
462
- Example with `pytest`:
452
+ ```python
453
+ import math_engine
454
+ from math_engine import error as E
455
+
456
+ # Disable visual printing to catch exceptions
457
+ math_engine.change_setting("readable_error", False)
458
+
459
+ try:
460
+ math_engine.evaluate("10.5 + 4.2.1")
461
+ except E.SyntaxError as e:
462
+ print(f"Error Code: {e.code}")
463
+ print(f"Location: {e.position_start} to {e.position_end}")
464
+
465
+ # You can use these indices to highlight the error in your own UI
466
+ bad_part = e.equation[e.position_start : e.position_end + 1]
467
+ print(f"Invalid segment: '{bad_part}'")
468
+
469
+ ```
470
+
471
+ ## Testing and Reliability
472
+
473
+ To write unit tests with `pytest`, ensure you set `readable_error` to `False` so that exceptions are raised and can be asserted.
463
474
 
464
475
  ```python
465
476
  import pytest
466
477
  import math_engine
467
478
  from math_engine import error as E
468
479
 
469
- def test_division_by_zero_error_code():
480
+ def test_division_by_zero():
481
+ # Ensure exceptions are raised
482
+ math_engine.change_setting("readable_error", False)
483
+
470
484
  with pytest.raises(E.CalculationError) as exc:
471
- math_engine.evaluate("1/0")
485
+ math_engine.evaluate("10 / 0")
486
+
487
+ # Assert the error is Division by Zero (3003)
472
488
  assert exc.value.code == "3003"
489
+ # Assert the error points exactly to the zero/operator
490
+ assert exc.value.position_start == 3
473
491
  ```
474
-
475
- You can also test more advanced behavior (non-decimal, strict modes, bitwise operations, etc.) in the same way.
476
-
477
- -----
478
-
492
+ ---
479
493
  # Performance
480
494
 
481
495
  * No use of Python `eval()`
@@ -1,5 +1,5 @@
1
1
 
2
- # Math Engine 0.5.0
2
+ # Math Engine v0.6.0
3
3
 
4
4
  [![PyPI Version](https://img.shields.io/pypi/v/math-engine.svg)](https://pypi.org/project/math-engine/)
5
5
  [![License: MIT](https://img.shields.io/pypi/l/math-engine.svg)](https://opensource.org/licenses/MIT)
@@ -339,7 +339,6 @@ All bitwise functions:
339
339
  - support overflow/wrap-around behavior
340
340
  - fully support binary, hex, decimal, and octal inputs
341
341
  - participate in the AST just like standard operators
342
- - support underscores in non-decimal literals (`0b1111_0000`)
343
342
 
344
343
  This makes Math Engine behave like a full-featured programmer’s calculator with CPU-like precision control.
345
344
 
@@ -378,6 +377,8 @@ preset = {
378
377
  # New in 0.3.0
379
378
  "word_size": 0, # 0 = unlimited, or 8, 16, 32, 64
380
379
  "signed_mode": True, # True = Two's Complement, False = Unsigned
380
+ # New in 0.6.0
381
+ "readable_error": False
381
382
  }
382
383
 
383
384
  math_engine.load_preset(preset)
@@ -397,70 +398,83 @@ decimal_places = math_engine.load_one_setting("decimal_places")
397
398
 
398
399
  -----
399
400
 
400
- # Error Handling
401
401
 
402
- Every error is a custom exception with:
402
+ # Error Handling (v0.6.0: Visual & Precise)
403
403
 
404
- * Human-readable message
405
- * Machine-readable error code
406
- * Position (if applicable)
407
- * The original expression
404
+ Math Engine 0.6.0 introduces a dual-mode error handling system designed for both interactive use and strict library integration.
408
405
 
409
- Example:
406
+ ## 1\. Visual Feedback (Default Behavior)
407
+
408
+ By default (`readable_error = True`), the engine catches syntax errors internally and prints a visual diagnostic to the console. This is perfect for CLI tools or quick debugging, as it points exactly to the issue without crashing the program.
410
409
 
411
410
  ```python
412
411
  import math_engine
413
- from math_engine import error as E
414
412
 
415
- try:
416
- math_engine.evaluate("1/0")
417
- except E.CalculationError as e:
418
- print(e.code) # 3003
419
- print(e.message) # "Division by zero"
420
- print(e.equation) # "1/0"
413
+ # readable_error is True by default
414
+ math_engine.evaluate("sin(5")
421
415
  ```
422
416
 
423
- ### Example Error Codes
417
+ **Console Output:**
424
418
 
425
- | Code | Meaning |
426
- | ---- | --------------------------- |
427
- | 3003 | Division by zero |
428
- | 3034 | Empty input |
429
- | 3036 | Multiple = signs |
430
- | 3032 | Multiple-character variable |
431
- | 8000 | Conversion to int failed |
432
- | 8006 | Output conversion error |
419
+ ```text
420
+ Errormessage: Unbalanced parenthesis.
421
+ Code: 2010
422
+ Equation: sin(5
423
+ ^ HERE IS THE PROBLEM (Position: 5)
424
+ ```
433
425
 
434
- For a complete list of all error codes and their meanings, please see the **[Error Codes Reference](https://github.com/JanTeske06/math_engine/blob/master/ERRORS.md)**.
426
+ ## 2\. Programmatic Handling (Exceptions)
435
427
 
436
- -----
428
+ If you are building an application or running unit tests, you likely want to catch exceptions instead of printing to stdout. You can disable `readable_error` to raise standard `MathError` exceptions.
437
429
 
438
- # Testing and Reliability
430
+ The exception object carries **precise start and end indices**:
439
431
 
440
- math\_engine is designed with testing in mind:
432
+ * `e.position_start` (int): Index where the error begins.
433
+ * `e.position_end` (int): Index where the error ends.
441
434
 
442
- * Full error-code consistency
443
- * Strict syntax rules
444
- * Unit-test friendly behavior
445
- * No reliance on Python’s runtime execution
435
+ <!-- end list -->
446
436
 
447
- Example with `pytest`:
437
+ ```python
438
+ import math_engine
439
+ from math_engine import error as E
440
+
441
+ # Disable visual printing to catch exceptions
442
+ math_engine.change_setting("readable_error", False)
443
+
444
+ try:
445
+ math_engine.evaluate("10.5 + 4.2.1")
446
+ except E.SyntaxError as e:
447
+ print(f"Error Code: {e.code}")
448
+ print(f"Location: {e.position_start} to {e.position_end}")
449
+
450
+ # You can use these indices to highlight the error in your own UI
451
+ bad_part = e.equation[e.position_start : e.position_end + 1]
452
+ print(f"Invalid segment: '{bad_part}'")
453
+
454
+ ```
455
+
456
+ ## Testing and Reliability
457
+
458
+ To write unit tests with `pytest`, ensure you set `readable_error` to `False` so that exceptions are raised and can be asserted.
448
459
 
449
460
  ```python
450
461
  import pytest
451
462
  import math_engine
452
463
  from math_engine import error as E
453
464
 
454
- def test_division_by_zero_error_code():
465
+ def test_division_by_zero():
466
+ # Ensure exceptions are raised
467
+ math_engine.change_setting("readable_error", False)
468
+
455
469
  with pytest.raises(E.CalculationError) as exc:
456
- math_engine.evaluate("1/0")
470
+ math_engine.evaluate("10 / 0")
471
+
472
+ # Assert the error is Division by Zero (3003)
457
473
  assert exc.value.code == "3003"
474
+ # Assert the error points exactly to the zero/operator
475
+ assert exc.value.position_start == 3
458
476
  ```
459
-
460
- You can also test more advanced behavior (non-decimal, strict modes, bitwise operations, etc.) in the same way.
461
-
462
- -----
463
-
477
+ ---
464
478
  # Performance
465
479
 
466
480
  * No use of Python `eval()`
@@ -8,11 +8,13 @@ from . import error as E
8
8
  class Number:
9
9
  """AST node for numeric literal backed by Decimal."""
10
10
 
11
- def __init__(self, value):
11
+ def __init__(self, value, position_start=-1, position_end=-1):
12
12
  # Always normalize input to Decimal via string to avoid float artifacts
13
13
  if not isinstance(value, Decimal):
14
14
  value = str(value)
15
15
  self.value = Decimal(value)
16
+ self.position_start = position_start
17
+ self.position_end = position_end
16
18
 
17
19
  def evaluate(self):
18
20
  """Return Decimal value for this literal."""
@@ -23,11 +25,9 @@ class Number:
23
25
  return (0, self.value)
24
26
 
25
27
  def __repr__(self):
26
- # Helpful for debugging/printing the AST
27
28
  try:
28
29
  display_value = self.value.to_normal_string()
29
30
  except AttributeError:
30
- # Fallback for older Decimal versions
31
31
  display_value = str(self.value)
32
32
  return f"Number({display_value})"
33
33
 
@@ -35,21 +35,21 @@ class Number:
35
35
  class Variable:
36
36
  """AST node representing a single symbolic variable (e.g. 'var0')."""
37
37
 
38
- def __init__(self, name):
38
+ def __init__(self, name, position_start=-1, position_end=-1):
39
39
  self.name = name
40
+ self.position_start = position_start
41
+ self.position_end = position_end
40
42
 
41
43
  def evaluate(self):
42
44
  """Variables cannot be directly evaluated without solving."""
43
- raise E.SolverError(f"Non linear problem.", code="3005")
45
+ raise E.SolverError(f"Non linear problem.", code="3005", position_start=self.position_start)
44
46
 
45
47
  def collect_term(self, var_name):
46
48
  """Return (1, 0) if this variable matches var_name; else error."""
47
49
  if self.name == var_name:
48
50
  return (1, 0)
49
51
  else:
50
- # Only one variable supported in the linear solver
51
- raise E.SolverError(f"Multiple variables found: {self.name}", code="3002")
52
- return (0, 0)
52
+ raise E.SolverError(f"Multiple variables found: {self.name}", code="3002", position_start=self.position_start)
53
53
 
54
54
  def __repr__(self):
55
55
  return f"Variable('{self.name}')"
@@ -58,15 +58,20 @@ class Variable:
58
58
  class BinOp:
59
59
  """AST node for a binary operation: left <operator> right."""
60
60
 
61
- def __init__(self, left, operator, right):
61
+ def __init__(self, left, operator, right, position_start=-1, position_end=-1):
62
62
  self.left = left
63
63
  self.operator = operator
64
64
  self.right = right
65
+ self.position_start = position_start
66
+ self.position_end = position_end
65
67
 
66
68
  def evaluate(self):
67
69
  """Evaluate numeric subtree and apply the binary operator."""
68
70
  left_value = self.left.evaluate()
69
71
  right_value = self.right.evaluate()
72
+ def check_int(val_l, val_r):
73
+ if val_l % 1 != 0 or val_r % 1 != 0:
74
+ raise E.CalculationError(f"Operator '{self.operator}' requires integers.", code="3042", position_start=self.position_start)
70
75
 
71
76
  if self.operator == '+':
72
77
  return left_value + right_value
@@ -75,28 +80,23 @@ class BinOp:
75
80
  return left_value - right_value
76
81
 
77
82
  elif self.operator == '&':
78
- if left_value % 1 != 0 or right_value % 1 != 0:
79
- raise E.CalculationError("Bitwise AND requires integers.", code="3042")
83
+ check_int(left_value, right_value)
80
84
  return Decimal(int(left_value) & int(right_value))
81
85
 
82
86
  elif self.operator == '|':
83
- if left_value % 1 != 0 or right_value % 1 != 0:
84
- raise E.CalculationError("Bitwise OR requires integers.", code="3042")
87
+ check_int(left_value, right_value)
85
88
  return Decimal(int(left_value) | int(right_value))
86
89
 
87
90
  elif self.operator == '^':
88
- if left_value % 1 != 0 or right_value % 1 != 0:
89
- raise E.CalculationError("XOR requires integers.", code="3042")
91
+ check_int(left_value, right_value)
90
92
  return Decimal(int(left_value) ^ int(right_value))
91
93
 
92
94
  elif self.operator == '<<':
93
- if left_value % 1 != 0 or right_value % 1 != 0:
94
- raise E.CalculationError("Bitshift requires integers.", code="3041")
95
+ check_int(left_value, right_value)
95
96
  return Decimal(int(left_value) << int(right_value))
96
97
 
97
98
  elif self.operator == '>>':
98
- if left_value % 1 != 0 or right_value % 1 != 0:
99
- raise E.CalculationError("Bitshift requires integers.", code="3041")
99
+ check_int(left_value, right_value)
100
100
  return Decimal(int(left_value) >> int(right_value))
101
101
 
102
102
  elif self.operator == '*':
@@ -107,78 +107,55 @@ class BinOp:
107
107
 
108
108
  elif self.operator == '/':
109
109
  if right_value == 0:
110
- raise E.CalculationError("Division by zero", code="3003")
110
+ raise E.CalculationError("Division by zero", code="3003", position_start=self.position_start)
111
111
  return left_value / right_value
112
112
 
113
113
  elif self.operator == '=':
114
- # Equality is evaluated to a boolean (used for "= True/False" responses)
115
114
  return left_value == right_value
116
115
  else:
117
- raise E.CalculationError(f"Unknown operator: {self.operator}", code="3004")
116
+ raise E.CalculationError(f"Unknown operator: {self.operator}", code="3004", position_start=self.position_start)
118
117
 
119
118
  def collect_term(self, var_name):
120
- """Collect linear terms on this subtree into (factor_of_var, constant).
121
-
122
- Only linear combinations are allowed; non-linear forms raise Solver/Syntax errors.
123
- """
119
+ """Collect linear terms on this subtree into (factor_of_var, constant)."""
124
120
  (left_factor, left_constant) = self.left.collect_term(var_name)
125
121
  (right_factor, right_constant) = self.right.collect_term(var_name)
126
122
 
127
123
  if self.operator == '+':
128
- result_factor = left_factor + right_factor
129
- result_constant = left_constant + right_constant
130
- return (result_factor, result_constant)
124
+ return (left_factor + right_factor, left_constant + right_constant)
131
125
 
132
126
  elif self.operator == '-':
133
- result_factor = left_factor - right_factor
134
- result_constant = left_constant - right_constant
135
- return (result_factor, result_constant)
127
+ return (left_factor - right_factor, left_constant - right_constant)
136
128
 
137
129
  elif self.operator == '*':
138
- # Only constant * (A*x + B) is allowed. (A*x + B)*(C*x + D) would be non-linear.
130
+ # Only constant * (A*x + B) is allowed.
139
131
  if left_factor != 0 and right_factor != 0:
140
- raise E.SyntaxError("x^x Error.", code="3005")
132
+ raise E.SyntaxError("x^x Error (Non-linear).", code="3005", position_start=self.position_start)
141
133
 
142
134
  elif left_factor == 0:
143
- # B * (C*x + D) = (B*C)*x + (B*D)
144
- result_factor = left_constant * right_factor
145
- result_constant = left_constant * right_constant
146
- return (result_factor, result_constant)
135
+ return (left_constant * right_factor, left_constant * right_constant)
147
136
 
148
137
  elif right_factor == 0:
149
- # (A*x + B) * D = (A*D)*x + (B*D)
150
- result_factor = right_constant * left_factor
151
- result_constant = right_constant * left_constant
152
- return (result_factor, result_constant)
138
+ return (right_constant * left_factor, right_constant * left_constant)
153
139
 
154
140
  elif left_factor == 0 and right_factor == 0:
155
- # Pure constant multiplication
156
- result_factor = 0
157
- result_constant = right_constant * left_constant
158
- return (result_factor, result_constant)
141
+ return (0, right_constant * left_constant)
159
142
 
160
143
  elif self.operator == '/':
161
- # (A*x + B) / D is allowed; division by (C*x + D) is non-linear
162
144
  if right_factor != 0:
163
- raise E.SolverError("Non-linear equation. (Division by x)", code="3006")
145
+ raise E.SolverError("Non-linear equation (Division by variable).", code="3006", position_start=self.position_start)
164
146
  elif right_constant == 0:
165
- raise E.SolverError("Solver: Division by zero", code="3003")
147
+ raise E.SolverError("Solver: Division by zero", code="3003", position_start=self.position_start)
166
148
  else:
167
- # (A*x + B) / D = (A/D)*x + (B/D)
168
- result_factor = left_factor / right_constant
169
- result_constant = left_constant / right_constant
170
- return (result_factor, result_constant)
149
+ return (left_factor / right_constant, left_constant / right_constant)
171
150
 
172
151
  elif self.operator == '**':
173
- # Powers generate non-linear terms (e.g., x^2)
174
- raise E.SolverError("Powers are not supported by the linear solver.", code="3007")
152
+ raise E.SolverError("Powers are not supported by the linear solver.", code="3007", position_start=self.position_start)
175
153
 
176
154
  elif self.operator == '=':
177
- # '=' only belongs at the root for solving; not inside collection
178
- raise E.SolverError("Should not happen: '=' inside collect_terms", code="3720")
155
+ raise E.SolverError("Should not happen: '=' inside collect_terms", code="3720", position_start=self.position_start)
179
156
 
180
157
  else:
181
- raise E.CalculationError(f"Unknown operator: {self.operator}", code="3004")
158
+ raise E.CalculationError(f"Unknown operator: {self.operator}", code="3004", position_start=self.position_start)
182
159
 
183
160
  def __repr__(self):
184
- return f"BinOp({self.operator!r}, left={self.left}, right={self.right})"
161
+ return f"BinOp({self.operator!r}, left={self.left}, right={self.right})"