数字推演:算出无穷大、非数时说清算不出来,不让接口在 JSON 编码时 500

改了输入之后溢出成无穷大(或算出非数),以前照 ok 交回,Starlette 编 JSON 时拒收这种浮点数,整个请求 500。
现在按计算出错处理(status: error,写明原因);老卡上本来就记着无穷大的值和输入,交回时换成 null(口径卡渲染成「—」)。
This commit is contained in:
ThearchyHelios
2026-10-05 04:14:56 +08:00
parent cb141cea88
commit 24fc21cb56
2 changed files with 25 additions and 2 deletions
+10 -2
View File
@@ -473,6 +473,9 @@ def _compute(plan: _Plan, values: dict[str, Any], notes: dict[str, list[int]],
if inp.row is not None and path in notes and path not in changes.inputs:
return "excluded", None, f"{EXCLUDED}:{path} 所在的第 {inp.row + 1} 行已排除"
value, reason = _evaluate(plan, values)
if reason is None and isinstance(value, float) and not math.isfinite(value):
# 溢出成无穷大、算出非数:口径卡渲染成「—」,这里也说清算不出来(JSON 里也放不下这种值)
reason = "计算结果不是有限的数值(溢出或非数),无法推演"
return ("ok", value, None) if reason is None else ("error", None, reason)
@@ -545,7 +548,7 @@ def _result(plan: _Plan, status: str, new: Any, reason: str | None, values: dict
return {
"id": metric.get("id"), "name": metric.get("name") or metric.get("id"), "unit": metric.get("unit") or "",
"decimals": metric.get("decimals"), "format": metric.get("format") or DEFAULT_FORMAT,
"old": old, "new": new, "delta": delta,
"old": _finite(old), "new": _finite(new), "delta": delta,
"rendered_old": _render(metric, old), "rendered_new": _render(metric, new) if known else None,
"rendered_delta": rendered_delta,
"changed": (not _same(old, new)) if known else None,
@@ -562,6 +565,11 @@ def _render(metric: dict[str, Any], value: Any, conv: str | None = None) -> str:
return MISSING
def _finite(value: Any) -> Any:
"""无穷大、非数换成 None:老卡上可能记着(口径卡渲染成「—」),JSON 里放不下。"""
return None if isinstance(value, float) and not math.isfinite(value) else value
def _same(a: Any, b: Any) -> bool:
if is_number(a) and is_number(b):
return a == b or (isinstance(a, float) and isinstance(b, float) and math.isnan(a) and math.isnan(b))
@@ -620,7 +628,7 @@ def _input_view(inp: _Input, values: dict[str, Any], notes: dict[str, list[int]]
new = _outline(values.get(inp.path), inp.table) if inp.problem is None else None
changed = inp.problem is None and not _same(old, new)
else:
old, new = item.get("value"), values.get(inp.path)
old, new = _finite(item.get("value")), _finite(values.get(inp.path))
changed = not _same(old, new)
view.update(old=old, new=new, changed=changed)
if inp.path in notes:
+15
View File
@@ -218,6 +218,21 @@ def test_division_by_zero_is_explained():
assert wow["status"] == "error" and wow["new"] is None and wow["reason"] == "除以零"
def test_an_overflow_is_an_error_not_a_crash():
"""算出无穷大:说清算不出来;老卡上记着无穷大的也不往外送(JSON 放不下,接口会 500)。"""
import json
result = whatif(CARD, Changes(inputs={"vars.kpi.refunds": 1e308, "vars.kpi.orders": 1e-10}), sources(),
metric="ratio")
[ratio] = result["metrics"]
assert ratio["status"] == "error" and ratio["new"] is None and "有限" in ratio["reason"]
card = {**CARD, "metrics": [metric("big", "vars.kpi.x * 2", "inf * 2", math.inf,
[scalar("vars.kpi.x", math.inf)], rendered="—")]}
big = whatif(card, Changes(), sources())["metrics"][0]
assert big["old"] is None and big["inputs"][0]["old"] is None and big["rendered_old"] == "—"
json.dumps([result, big], allow_nan=False)
def test_a_missing_input_can_be_supplied():
card = {**CARD, "metrics": [metric("cost", "vars.kpi.cost / vars.kpi.orders", "vars.kpi.cost / 1234", None,
[scalar("vars.kpi.cost", None), scalar("vars.kpi.orders", 1234)], decimals=2,