Files
Aislo/B08_wf5_Quantity/B08_wf5_Quantity_Engine_Formula.py
T

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)