parsimathious 0.1.1__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.
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: parsimathious
3
+ Version: 0.1.1
4
+ Summary: A mathematical expression parser supporting arithmetic, functions, and complex numbers
5
+ Keywords: math,parser,expression,arithmetic,complex numbers
6
+ Author: Simone Sturniolo
7
+ Author-email: Simone Sturniolo <simonesturniolo@gmail.com>
8
+ License-Expression: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
17
+ Requires-Dist: parsimonious>=0.11.0
18
+ Requires-Python: >=3.11
19
+ Project-URL: Homepage, https://github.com/stur86/parsimathious
20
+ Project-URL: Repository, https://github.com/stur86/parsimathious
21
+ Project-URL: Issues, https://github.com/stur86/parsimathious/issues
22
+ Description-Content-Type: text/markdown
23
+
24
+ # parsimathious
25
+
26
+ [![PyPI](https://img.shields.io/pypi/v/parsimathious)](https://pypi.org/project/parsimathious/)
27
+ [![Python](https://img.shields.io/pypi/pyversions/parsimathious)](https://pypi.org/project/parsimathious/)
28
+ [![Tests](https://github.com/stur86/parsimathious/actions/workflows/test.yml/badge.svg)](https://github.com/stur86/parsimathious/actions/workflows/test.yml)
29
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
30
+
31
+ `parsimathious` is a simple mathematical expression parser implemented with [`parsimonious`](https://github.com/erikrose/parsimonious). It supports basic arithmetic operations, parentheses, unary functions, constants, and complex numbers.
32
+
33
+ ## Installation
34
+
35
+ You can install `parsimathious` using pip:
36
+
37
+ ```bash
38
+ pip install parsimathious
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ Import the `ExpressionParser` and create an instance:
44
+
45
+ ```python
46
+ from parsimathious import ExpressionParser
47
+
48
+ parser = ExpressionParser()
49
+ ```
50
+
51
+ Then you can parse and evaluate expressions:
52
+
53
+ ```python
54
+ result = parser("sin(pi / 2) + 1")
55
+ print(result) # Output: 2.0
56
+ ```
57
+
58
+ ## Supported functions and constants
59
+
60
+
61
+ On top of basic arithmetic operations, `parsimathious` supports the following unary functions and constants by default:
62
+
63
+ | Name | Python Implementation | Description |
64
+ |----------|--------------------------------|------------------------------------|
65
+ | `sin` | math.sin | Sine |
66
+ | `cos` | math.cos | Cosine |
67
+ | `tan` | math.tan | Tangent |
68
+ | `log` | math.log | Natural logarithm (base e) |
69
+ | `sqrt` | math.sqrt | Square root |
70
+ | `exp` | math.exp | Exponential (e^x) |
71
+ | `log10` | math.log10 | Logarithm base 10 |
72
+ | `abs` | abs | Absolute value |
73
+ | `floor` | math.floor | Floor (round down) |
74
+ | `ceil` | math.ceil | Ceiling (round up) |
75
+ | `round` | round | Round to nearest integer |
76
+ | `sinh` | math.sinh | Hyperbolic sine |
77
+ | `cosh` | math.cosh | Hyperbolic cosine |
78
+ | `tanh` | math.tanh | Hyperbolic tangent |
79
+ | `asin` | math.asin | Arc sine |
80
+ | `acos` | math.acos | Arc cosine |
81
+ | `atan` | math.atan | Arc tangent |
82
+ | `asinh` | math.asinh | Inverse hyperbolic sine |
83
+ | `acosh` | math.acosh | Inverse hyperbolic cosine |
84
+ | `atanh` | math.atanh | Inverse hyperbolic tangent |
85
+ | `sec` | lambda x: 1 / math.cos(x) | Secant |
86
+ | `csc` | lambda x: 1 / math.sin(x) | Cosecant |
87
+ | `cot` | lambda x: 1 / math.tan(x) | Cotangent |
88
+
89
+ ### Constants
90
+
91
+ | Name | Value | Description |
92
+ |------|----------------------|----------------------------|
93
+ | `pi` | math.pi | The mathematical constant π |
94
+ | `e` | math.e | The mathematical constant e |
95
+ | `i` | 1j | The imaginary unit |
96
+
97
+ ## Custom Unary Functions
98
+
99
+ It's also possible to support custom unary functions by passing a dictionary of function names to their implementations when creating the `ExpressionParser`:
100
+
101
+ ```python
102
+ import math
103
+ from parsimathious import ExpressionParser, UnaryFunctionMap
104
+
105
+ custom_functions: UnaryFunctionMap = {
106
+ "log2": math.log2, # Logarithm base 2
107
+ "cube": lambda x: x ** 3, # Cube function
108
+ }
109
+
110
+ parser = ExpressionParser(unary_functions=custom_functions)
111
+ result = parser("log2(8) + cube(3)")
112
+ print(result) # Output: 35.0
113
+ ```
@@ -0,0 +1,90 @@
1
+ # parsimathious
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/parsimathious)](https://pypi.org/project/parsimathious/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/parsimathious)](https://pypi.org/project/parsimathious/)
5
+ [![Tests](https://github.com/stur86/parsimathious/actions/workflows/test.yml/badge.svg)](https://github.com/stur86/parsimathious/actions/workflows/test.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ `parsimathious` is a simple mathematical expression parser implemented with [`parsimonious`](https://github.com/erikrose/parsimonious). It supports basic arithmetic operations, parentheses, unary functions, constants, and complex numbers.
9
+
10
+ ## Installation
11
+
12
+ You can install `parsimathious` using pip:
13
+
14
+ ```bash
15
+ pip install parsimathious
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ Import the `ExpressionParser` and create an instance:
21
+
22
+ ```python
23
+ from parsimathious import ExpressionParser
24
+
25
+ parser = ExpressionParser()
26
+ ```
27
+
28
+ Then you can parse and evaluate expressions:
29
+
30
+ ```python
31
+ result = parser("sin(pi / 2) + 1")
32
+ print(result) # Output: 2.0
33
+ ```
34
+
35
+ ## Supported functions and constants
36
+
37
+
38
+ On top of basic arithmetic operations, `parsimathious` supports the following unary functions and constants by default:
39
+
40
+ | Name | Python Implementation | Description |
41
+ |----------|--------------------------------|------------------------------------|
42
+ | `sin` | math.sin | Sine |
43
+ | `cos` | math.cos | Cosine |
44
+ | `tan` | math.tan | Tangent |
45
+ | `log` | math.log | Natural logarithm (base e) |
46
+ | `sqrt` | math.sqrt | Square root |
47
+ | `exp` | math.exp | Exponential (e^x) |
48
+ | `log10` | math.log10 | Logarithm base 10 |
49
+ | `abs` | abs | Absolute value |
50
+ | `floor` | math.floor | Floor (round down) |
51
+ | `ceil` | math.ceil | Ceiling (round up) |
52
+ | `round` | round | Round to nearest integer |
53
+ | `sinh` | math.sinh | Hyperbolic sine |
54
+ | `cosh` | math.cosh | Hyperbolic cosine |
55
+ | `tanh` | math.tanh | Hyperbolic tangent |
56
+ | `asin` | math.asin | Arc sine |
57
+ | `acos` | math.acos | Arc cosine |
58
+ | `atan` | math.atan | Arc tangent |
59
+ | `asinh` | math.asinh | Inverse hyperbolic sine |
60
+ | `acosh` | math.acosh | Inverse hyperbolic cosine |
61
+ | `atanh` | math.atanh | Inverse hyperbolic tangent |
62
+ | `sec` | lambda x: 1 / math.cos(x) | Secant |
63
+ | `csc` | lambda x: 1 / math.sin(x) | Cosecant |
64
+ | `cot` | lambda x: 1 / math.tan(x) | Cotangent |
65
+
66
+ ### Constants
67
+
68
+ | Name | Value | Description |
69
+ |------|----------------------|----------------------------|
70
+ | `pi` | math.pi | The mathematical constant π |
71
+ | `e` | math.e | The mathematical constant e |
72
+ | `i` | 1j | The imaginary unit |
73
+
74
+ ## Custom Unary Functions
75
+
76
+ It's also possible to support custom unary functions by passing a dictionary of function names to their implementations when creating the `ExpressionParser`:
77
+
78
+ ```python
79
+ import math
80
+ from parsimathious import ExpressionParser, UnaryFunctionMap
81
+
82
+ custom_functions: UnaryFunctionMap = {
83
+ "log2": math.log2, # Logarithm base 2
84
+ "cube": lambda x: x ** 3, # Cube function
85
+ }
86
+
87
+ parser = ExpressionParser(unary_functions=custom_functions)
88
+ result = parser("log2(8) + cube(3)")
89
+ print(result) # Output: 35.0
90
+ ```
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "parsimathious"
3
+ version = "0.1.1"
4
+ description = "A mathematical expression parser supporting arithmetic, functions, and complex numbers"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "Simone Sturniolo", email = "simonesturniolo@gmail.com" }
9
+ ]
10
+ requires-python = ">=3.11"
11
+ keywords = ["math", "parser", "expression", "arithmetic", "complex numbers"]
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.11",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Programming Language :: Python :: 3.13",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Intended Audience :: Developers",
19
+ "Intended Audience :: Science/Research",
20
+ "Topic :: Scientific/Engineering :: Mathematics"
21
+ ]
22
+ dependencies = [
23
+ "parsimonious>=0.11.0",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/stur86/parsimathious"
28
+ Repository = "https://github.com/stur86/parsimathious"
29
+ Issues = "https://github.com/stur86/parsimathious/issues"
30
+
31
+ [build-system]
32
+ requires = ["uv_build>=0.10.8,<0.11.0"]
33
+ build-backend = "uv_build"
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "pytest>=9.0.3",
38
+ "taskipy>=1.14.1",
39
+ ]
40
+
41
+ [tool.taskipy.tasks]
42
+ test = "pytest tests"
43
+ build = "uv build"
44
+ publish = "uv publish"
@@ -0,0 +1,4 @@
1
+ from .grammar import ExpressionGrammar, UnaryFunctionMap
2
+ from .parser import ExpressionParser
3
+
4
+ __all__ = ["ExpressionGrammar", "UnaryFunctionMap", "ExpressionParser"]
@@ -0,0 +1,90 @@
1
+ import math
2
+ from parsimonious import Grammar, ParseError
3
+ from parsimonious.nodes import Node
4
+ from typing import Dict, Callable
5
+
6
+ UnaryFunction = Callable[[float], float]
7
+ UnaryFunctionMap = Dict[str, UnaryFunction]
8
+
9
+ _DEFAULT_UNARY_FUNCTIONS: UnaryFunctionMap = {
10
+ "sin": math.sin,
11
+ "cos": math.cos,
12
+ "tan": math.tan,
13
+ "log": math.log,
14
+ "sqrt": math.sqrt,
15
+ "exp": math.exp,
16
+ "log10": math.log10,
17
+ "abs": abs,
18
+ "floor": math.floor,
19
+ "ceil": math.ceil,
20
+ "round": round,
21
+ "sinh": math.sinh,
22
+ "cosh": math.cosh,
23
+ "tanh": math.tanh,
24
+ "asin": math.asin,
25
+ "acos": math.acos,
26
+ "atan": math.atan,
27
+ "asinh": math.asinh,
28
+ "acosh": math.acosh,
29
+ "atanh": math.atanh,
30
+ "sec": lambda x: 1 / math.cos(x),
31
+ "csc": lambda x: 1 / math.sin(x),
32
+ "cot": lambda x: 1 / math.tan(x),
33
+ }
34
+
35
+ class ExpressionGrammar:
36
+ _grammar: Grammar
37
+
38
+ def __init__(self, unary_functions: UnaryFunctionMap = _DEFAULT_UNARY_FUNCTIONS):
39
+ grammar_definition = """
40
+ expression = sum / unary_number
41
+ sum = term (add_op term)*
42
+ term = factor (mul_op factor)*
43
+ factor = exp_factor (exp_op exp_factor)*
44
+ exp_factor = atom
45
+ unary_number = unary_op number
46
+ parenthesized_expression = "(" expression ")"
47
+ add_op = "+" / "-"
48
+ mul_op = "*" / "/"
49
+ exp_op = "^"
50
+ unary_op = "+" / "-"
51
+ number = ~r"\\d+(\\.\\d+)?"
52
+ constant = "pi" / "e"
53
+ imaginary_unit = "i"
54
+ imaginary_number = number imaginary_unit
55
+ complex_number = imaginary_number / number / imaginary_unit / constant
56
+ """
57
+ if len(unary_functions) > 0:
58
+ function_keys = list(unary_functions.keys())
59
+ # Sort them from longest to shortest to ensure correct parsing (e.g., "log10" before "log")
60
+ function_keys.sort(key=len, reverse=True)
61
+ function_names = " / ".join(f'"{name}"' for name in function_keys)
62
+ grammar_definition += f"""atom = function_call / parenthesized_expression / complex_number
63
+ function_call = function_name parenthesized_expression
64
+ function_name = {function_names}"""
65
+ else:
66
+ grammar_definition += "atom = parenthesized_expression / complex_number"
67
+
68
+ self._grammar = Grammar(grammar_definition)
69
+
70
+ def __call__(self, expression: str) -> Node:
71
+ # Strip out all spaces
72
+ expression = expression.replace(" ", "")
73
+ try:
74
+ return self._grammar.parse(expression)
75
+ except ParseError as e:
76
+ raise Exception(f"Syntax error at position {e.pos}: {e.text[e.pos:e.pos+20]}") from e
77
+ except Exception as e:
78
+ raise Exception(f"Error parsing expression: {e}") from e
79
+
80
+
81
+ if __name__ == "__main__":
82
+ while True:
83
+ grammar = ExpressionGrammar()
84
+ try:
85
+ expr = input("Enter an expression (or 'exit' to quit): ")
86
+ if expr.lower() == "exit":
87
+ break
88
+ ast = grammar(expr)
89
+ except Exception as e:
90
+ print(f"Error: {e}")
@@ -0,0 +1,102 @@
1
+ import math
2
+ from typing import Any, Sequence
3
+
4
+ from parsimonious import NodeVisitor
5
+ from parsimonious.nodes import Node
6
+ from .grammar import ExpressionGrammar, UnaryFunctionMap, _DEFAULT_UNARY_FUNCTIONS
7
+
8
+
9
+ class ExpressionVisitor(NodeVisitor):
10
+ def __init__(self, unary_functions: UnaryFunctionMap):
11
+ super().__init__()
12
+ self._unary_functions = unary_functions
13
+
14
+ def visit_complex_number(self, node, visited_children):
15
+ return visited_children[0]
16
+
17
+ def visit_imaginary_number(self, node, visited_children):
18
+ number_node, _ = visited_children
19
+ if number_node is None:
20
+ return complex(0, 1) # Just "i" means 0 + 1i
21
+ else:
22
+ return complex(0, float(number_node)) # e.g., "2i" means 0 + 2i
23
+
24
+ def visit_number(self, node, visited_children):
25
+ return float(node.text)
26
+
27
+ def visit_imaginary_unit(self, node, visited_children):
28
+ return 1.0j
29
+
30
+ def visit_constant(self, node, visited_children):
31
+ constant_map = {
32
+ "pi": math.pi,
33
+ "e": math.e,
34
+ }
35
+ try:
36
+ return constant_map[node.text]
37
+ except KeyError:
38
+ raise ValueError(f"Unknown constant: {node.text}")
39
+
40
+ def visit_atom(self, node, visited_children):
41
+ return visited_children[0]
42
+
43
+ def visit_exp_factor(self, node, visited_children):
44
+ return visited_children[0]
45
+
46
+ def visit_factor(self, node, visited_children):
47
+ base = visited_children[0]
48
+ for op, exponent in visited_children[1]:
49
+ if op.text != "^":
50
+ raise ValueError(f"Unexpected operator in factor: {op.text}")
51
+ base = base ** exponent
52
+ return base
53
+
54
+ def visit_term(self, node, visited_children):
55
+ result = visited_children[0]
56
+ for [op], factor in visited_children[1]:
57
+ if op.text == "*":
58
+ result *= factor
59
+ elif op.text == "/":
60
+ result /= factor
61
+ else:
62
+ raise ValueError(f"Unexpected operator in term: {op.text}")
63
+ return result
64
+
65
+ def visit_sum(self, node, visited_children):
66
+ result = visited_children[0]
67
+ for [op], term in visited_children[1]:
68
+ if op.text == "+":
69
+ result += term
70
+ elif op.text == "-":
71
+ result -= term
72
+ else:
73
+ raise ValueError(f"Unexpected operator in expression: {op.text}")
74
+ return result
75
+
76
+ def visit_parenthesized_expression(self, node, visited_children):
77
+ _, expr, _ = visited_children
78
+ return expr[0]
79
+
80
+ def visit_function_name(self, node, visited_children):
81
+ return node
82
+
83
+ def visit_function_call(self, node, visited_children):
84
+ function_name_node, arg_node = visited_children
85
+ function_name = function_name_node.text
86
+ if function_name not in self._unary_functions:
87
+ raise ValueError(f"Unknown function: {function_name}")
88
+ func = self._unary_functions[function_name]
89
+ arg_value = arg_node # The argument is the first child of the parenthesized expression
90
+ return func(arg_value)
91
+
92
+ def generic_visit(self, node: Node, visited_children: Sequence[Any]):
93
+ return visited_children or node
94
+
95
+ class ExpressionParser:
96
+ def __init__(self, unary_functions: UnaryFunctionMap = _DEFAULT_UNARY_FUNCTIONS):
97
+ self._grammar = ExpressionGrammar(unary_functions)
98
+ self._visitor = ExpressionVisitor(unary_functions)
99
+
100
+ def __call__(self, expression: str) -> float | complex:
101
+ ast = self._grammar(expression)
102
+ return self._visitor.visit(ast)