35 lines
2.0 KiB
Python
35 lines
2.0 KiB
Python
from __future__ import annotations
|
|
import ast
|
|
from decimal import Decimal, ROUND_CEILING, ROUND_FLOOR, ROUND_HALF_UP
|
|
|
|
_ALLOWED_BINARY = {ast.Add: lambda a,b:a+b, ast.Sub:lambda a,b:a-b,
|
|
ast.Mult:lambda a,b:a*b, ast.Div:lambda a,b:a/b}
|
|
_ALLOWED_UNARY = {ast.UAdd:lambda a:a, ast.USub:lambda a:-a}
|
|
|
|
def evaluate(expression: str, variables: dict[str, Decimal | int]) -> Decimal:
|
|
values = {key: Decimal(str(value)) for key,value in variables.items()}
|
|
tree = ast.parse(expression, mode="eval")
|
|
def visit(node):
|
|
if isinstance(node, ast.Expression): return visit(node.body)
|
|
if isinstance(node, ast.Constant) and isinstance(node.value,(int,float)): return Decimal(str(node.value))
|
|
if isinstance(node, ast.Name):
|
|
if node.id not in values: raise ValueError(f"수식 변수 미입력: {node.id}")
|
|
return values[node.id]
|
|
if isinstance(node, ast.BinOp) and type(node.op) in _ALLOWED_BINARY:
|
|
right=visit(node.right)
|
|
if isinstance(node.op,ast.Div) and right==0: raise ValueError("0으로 나눌 수 없습니다.")
|
|
return _ALLOWED_BINARY[type(node.op)](visit(node.left),right)
|
|
if isinstance(node,ast.UnaryOp) and type(node.op) in _ALLOWED_UNARY:
|
|
return _ALLOWED_UNARY[type(node.op)](visit(node.operand))
|
|
if isinstance(node,ast.Call) and isinstance(node.func,ast.Name) and node.func.id in {"min","max"}:
|
|
args=[visit(arg) for arg in node.args]
|
|
return (min if node.func.id=="min" else max)(args)
|
|
raise ValueError(f"허용되지 않은 수식 요소: {type(node).__name__}")
|
|
return visit(tree)
|
|
|
|
def round_amount(value: Decimal, mode: str="ROUND", unit: int=1) -> int:
|
|
if unit < 1: raise ValueError("절사 단위는 1 이상이어야 합니다.")
|
|
rounding={"ROUND":ROUND_HALF_UP,"FLOOR":ROUND_FLOOR,"CEILING":ROUND_CEILING}.get(mode)
|
|
if rounding is None: raise ValueError(f"지원하지 않는 반올림 방식: {mode}")
|
|
step=Decimal(unit)
|
|
return int((value/step).quantize(Decimal("1"),rounding=rounding)*step) |