feat: placeholder evaluation with bounds (min, max)

This commit is contained in:
Felix Zett 2025-10-27 21:45:35 +01:00
parent 85bec5c4e2
commit 37fc063bc7

View file

@ -29,7 +29,18 @@ def _safe_eval(expr: str, ctx: dict) -> str:
return str(math.ceil(val))
return str(val)
_placeholder_re = re.compile(r"\{([^{}]+)\}")
_placeholder_re = re.compile(r"\{([^{}|]+)(?:\|([^{}]+))?\}")
def _safe_eval_value(expr: str, ctx: dict):
node = ast.parse(expr, mode="eval")
for n in ast.walk(node):
if not isinstance(n, ALLOWED_NODES):
raise ValueError("disallowed expression")
if isinstance(n, ast.Name) and n.id not in ALLOWED_NAMES:
raise ValueError("unknown name")
if isinstance(n, ast.Call):
raise ValueError("calls not allowed")
return eval(compile(node, "<expr>", "eval"), {"__builtins__": {}}, ctx)
def render_name(name_template: str, start: Optional[date], end: Optional[date]) -> str:
if not name_template:
@ -40,12 +51,41 @@ def render_name(name_template: str, start: Optional[date], end: Optional[date])
nights = max(days - 1, 0)
ctx = {"days": days, "nights": nights}
def parse_opts(optstr: str):
opts = {}
if not optstr:
return opts
for part in optstr.split(","):
if "=" in part:
k, v = part.split("=", 1)
opts[k.strip()] = v.strip()
return opts
def repl(m):
expr = m.group(1).strip()
opts_str = m.group(2)
opts = parse_opts(opts_str)
try:
return _safe_eval(expr, ctx)
val = _safe_eval_value(expr, ctx)
# if numeric apply bounds
if isinstance(val, (int, float)):
# apply min / max if provided
if "min" in opts:
try:
val = max(val, float(opts["min"]))
except Exception:
pass
if "max" in opts:
try:
val = min(val, float(opts["max"]))
except Exception:
pass
# round up numeric results (ceiling)
return str(math.ceil(val))
# non-numeric: string as before
return str(val)
except Exception:
# if not evaluable, leave placeholder as-is
# leave placeholder unchanged on error
return m.group(0)
return _placeholder_re.sub(repl, name_template)