""" Math Engine — safely evaluates mathematical expressions. Supported operations: + Addition - Subtraction * Multiplication / Division % Modulo ^ Exponentiation (converted to **) () Parentheses for grouping Uses a whitelist-based safe evaluator to avoid arbitrary code execution. """ import re import ast import operator # Allowed AST node types for safe evaluation ALLOWED_NODES = ( ast.Expression, ast.BinOp, ast.UnaryOp, ast.Constant, ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Mod, ast.Pow, ast.USub, # unary minus, e.g. -5 ast.UAdd, # unary plus, e.g. +5 ) # Mapping AST operators to Python functions OPERATORS = { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Mod: operator.mod, ast.Pow: operator.pow, ast.USub: operator.neg, ast.UAdd: operator.pos, } # Prefixes the user might add before the expression STRIP_PREFIXES = ( "calculate", "compute", "evaluate", "solve", "what is", "what's", ) def extract_expression(user_input: str) -> str: """ Strip any natural-language prefixes to get the raw math expression. Also converts '^' to '**' for exponentiation. """ text = user_input.strip() lowered = text.lower() for prefix in STRIP_PREFIXES: if lowered.startswith(prefix): text = text[len(prefix):].strip() break # Replace caret with Python exponentiation operator text = text.replace("^", "**") return text def safe_eval(expression: str) -> float: """ Safely evaluate a mathematical expression string. Raises ValueError if the expression is invalid or contains disallowed operations. """ try: tree = ast.parse(expression, mode="eval") except SyntaxError: raise ValueError(f"Invalid mathematical expression: '{expression}'") # Validate every node in the AST for node in ast.walk(tree): if not isinstance(node, ALLOWED_NODES): raise ValueError( f"Unsafe or unsupported operation detected: {type(node).__name__}" ) return _eval_node(tree.body) def _eval_node(node: ast.AST) -> float: """Recursively evaluate an AST node.""" if isinstance(node, ast.Constant): if isinstance(node.value, (int, float)): return node.value raise ValueError(f"Unsupported constant type: {type(node.value)}") if isinstance(node, ast.BinOp): left = _eval_node(node.left) right = _eval_node(node.right) op_func = OPERATORS.get(type(node.op)) if op_func is None: raise ValueError(f"Unsupported binary operator: {type(node.op).__name__}") if isinstance(node.op, ast.Div) and right == 0: raise ValueError("Division by zero is not allowed.") return op_func(left, right) if isinstance(node, ast.UnaryOp): operand = _eval_node(node.operand) op_func = OPERATORS.get(type(node.op)) if op_func is None: raise ValueError(f"Unsupported unary operator: {type(node.op).__name__}") return op_func(operand) raise ValueError(f"Unsupported AST node: {type(node).__name__}") def evaluate(user_input: str) -> str: """ Main entry point: extract the expression, evaluate it, and return a formatted result string. """ expression = extract_expression(user_input) if not expression: return "Please provide a mathematical expression to evaluate." try: result = safe_eval(expression) # Format: show integer if result is whole, else show decimal if isinstance(result, float) and result.is_integer(): formatted = str(int(result)) else: formatted = f"{result:.6g}" # up to 6 significant figures return f"The result of {expression} = {formatted}" except ValueError as e: return f"Math error: {e}" except Exception as e: return f"Unexpected error evaluating expression: {e}"