"""B09 — 사용자가 글로 넣은 **식**을 안전하게 셈 (PLAN 12장 2차 ③ · STmate `wM_Edit_San` 함수 칸). 받는 것 — 수 · `+ - * /` · 괄호 · 함수 넷 `ROUND(x, n)` · `ROUNDDOWN(x, n)` · `INT(x)` · `SQRT(x)` (STmate 식 편집기 함수 목록과 같은 이름 · 명세 13장 식 언어). 이름·속성·호출 모양이 그 밖이면 거절. ⚠ `eval` 을 쓰지 않음 — 파이썬 문법 나무를 한 마디씩 보고 아는 것만 셈. ⚠ 값은 `Decimal` — 소수를 이진수로 접지 않음(`1/1.175` 는 식 그대로 나눔). ⚠ 「좌→우 순차」 — 같은 우선순위의 곱·나눗셈은 파이썬 문법 나무가 이미 왼쪽부터 묶음(명세 7장 노임 식과 같음). """ from __future__ import annotations import ast from decimal import ROUND_FLOOR, ROUND_HALF_UP, Decimal, InvalidOperation _MAX_LENGTH = 400 class ExpressionError(ValueError): """식을 셀 수 없음 — 까닭을 사람 말로.""" def _places(value: Decimal) -> Decimal: digits = int(value) if digits < 0 or digits > 10: raise ExpressionError("자릿수는 0~10 사이여야 합니다") return Decimal(1).scaleb(-digits) def _call(name: str, args: list[Decimal]) -> Decimal: if name == "ROUND" and len(args) == 2: return args[0].quantize(_places(args[1]), rounding=ROUND_HALF_UP) if name == "ROUNDDOWN" and len(args) == 2: return args[0].quantize(_places(args[1]), rounding=ROUND_FLOOR) if name == "INT" and len(args) == 1: return args[0].to_integral_value(rounding=ROUND_FLOOR) if name == "SQRT" and len(args) == 1: if args[0] < 0: raise ExpressionError("음수의 제곱근은 셀 수 없습니다") return args[0].sqrt() raise ExpressionError(f"모르는 함수이거나 인자 수가 맞지 않습니다: {name}") def _eval(node: ast.AST) -> Decimal: if isinstance(node, ast.Expression): return _eval(node.body) if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)): return Decimal(str(node.value)) if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.USub, ast.UAdd)): value = _eval(node.operand) return -value if isinstance(node.op, ast.USub) else value if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Sub, ast.Mult, ast.Div)): left, right = _eval(node.left), _eval(node.right) if isinstance(node.op, ast.Add): return left + right if isinstance(node.op, ast.Sub): return left - right if isinstance(node.op, ast.Mult): return left * right if right == 0: raise ExpressionError("0 으로 나눌 수 없습니다") return left / right if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and not node.keywords: return _call(node.func.id.upper(), [_eval(arg) for arg in node.args]) raise ExpressionError( "식에 쓸 수 없는 글자가 있습니다 — 수·+ - * /·괄호·ROUND·ROUNDDOWN·INT·SQRT 만" ) def evaluate(text: str) -> Decimal: """식 글 → 값. 셀 수 없으면 `ExpressionError`.""" source = str(text or "").strip().replace("×", "*").replace("÷", "/") if not source: raise ExpressionError("식이 비었습니다") if len(source) > _MAX_LENGTH: raise ExpressionError("식이 너무 깁니다") try: tree = ast.parse(source, mode="eval") except SyntaxError as error: raise ExpressionError(f"식 문법이 맞지 않습니다: {error.msg}") from error try: return _eval(tree) except (InvalidOperation, OverflowError) as error: raise ExpressionError("셀 수 없는 값입니다") from error