File size: 1,889 Bytes
16ab8a2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from __future__ import annotations

import ast
import math
import operator
import re
from typing import Any

_BINARY = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.truediv,
    ast.FloorDiv: operator.floordiv,
    ast.Mod: operator.mod,
    ast.Pow: operator.pow,
}
_UNARY = {ast.UAdd: operator.pos, ast.USub: operator.neg}
_FUNCTIONS = {"abs": abs, "round": round, "sqrt": math.sqrt, "ceil": math.ceil, "floor": math.floor}


def calculate_expression(expression: str) -> dict[str, Any]:
    cleaned = _extract_expression(expression)
    value = _evaluate(ast.parse(cleaned, mode="eval").body)
    return {
        "ok": True,
        "source": "safe_calculator",
        "content": str(value),
        "metadata": {"expression": cleaned},
    }


def _extract_expression(text: str) -> str:
    text = text.strip().replace("×", "*").replace("÷", "/").replace("^", "**")
    match = re.search(r"(?:expression\s*[:=]\s*)?([0-9eE\.\s+\-*/%(),]+)$", text)
    candidate = (match.group(1) if match else text).strip()
    if not candidate:
        raise ValueError("No arithmetic expression found.")
    return candidate


def _evaluate(node: ast.AST) -> int | float:
    if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
        return node.value
    if isinstance(node, ast.BinOp) and type(node.op) in _BINARY:
        return _BINARY[type(node.op)](_evaluate(node.left), _evaluate(node.right))
    if isinstance(node, ast.UnaryOp) and type(node.op) in _UNARY:
        return _UNARY[type(node.op)](_evaluate(node.operand))
    if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in _FUNCTIONS:
        return _FUNCTIONS[node.func.id](*[_evaluate(arg) for arg in node.args])
    raise ValueError(f"Unsupported expression element: {ast.dump(node, include_attributes=False)}")