COBOL 截断 vs 四舍五入:迁移后逐字节对比

参考 OpenAI COBOL 迁移评测中提到的定点运算陷阱,用 Python decimal 模拟一段不带 ROUNDED 的计息逻辑,对比保留截断、改成四舍五入、改用浮点三种迁移写法与参考实现的逐字节输出是否一致。

已验证 Python 3.12.15 耗时 不到 0.1s 运行于 2026-10-20 22:07 UTC
结论

truncate parity=True; round_half_up parity=False (529 rows differ, +529 cents); float parity=False (528 rows differ)

以上内容从实际运行输出中提取,完整输出见下方。

python main.py
truncate       byte_parity=True  diff_rows=   0/1000 total_delta_cents=+0
round_half_up  byte_parity=False diff_rows= 529/1000 total_delta_cents=+529
float          byte_parity=False diff_rows= 528/1000 total_delta_cents=+528
RESULT: truncate parity=True; round_half_up parity=False (529 rows differ, +529 cents); float parity=False (528 rows differ)

目的

OpenAI 在 Advancing coding in low-resource programming languages 中介绍了一个 COBOL 迁移评测:隐藏测试要求迁移后的程序在可观察输出上逐字节一致,全部通过才算成功。文中举例:COBOL 的定点运算默认会截断多余的小数位,除非显式要求四舍五入(ROUNDED);改写成四舍五入的版本能正常运行,却会算出不同的金额。

本实验用 Python 标准库 decimal 复现这个陷阱,直观感受「能跑」和「行为等价」之间的差距,以及逐字节对比为什么能抓住它。

注意:参考实现、数据和报表格式都是本实验自拟的简化版本,不是 OpenAI 评测中的程序或测试。

步骤

  1. 用 ROUND_DOWN 模拟 COBOL 不带 ROUNDED 的 COMPUTE(目标字段两位小数),作为参考实现
  2. 准备三种「迁移后」的写法:保留截断、改为四舍五入(ROUND_HALF_UP)、改用浮点数再格式化
  3. 用确定性伪随机数生成 1000 个账户,把结果输出成定长报表,与参考实现逐字节比较
  4. 检查:截断写法必须完全一致;四舍五入写法必须被发现不一致,且总金额偏高

运行:

python main.py

程序最后输出 RESULT: ...,任一检查失败时以非零状态退出。

预期结果

  • truncate:逐字节一致,差异行数为 0
  • round_half_up、float:程序都能正常跑完,但有相当比例的行相差 1 分钱,单笔差异小、累计起来不可忽略

局限与可以继续尝试

  • 真实 COBOL 还涉及 COMP-3 打包十进制、符号位、溢出截断(ON SIZE ERROR)等,本实验只模拟了小数截断一点
  • 可以把同样的逐字节对比思路用到自己的迁移项目:先用旧系统跑一批真实输入固定「黄金输出」,再让 AI 迁移后的程序对照
  • 可以尝试让手头的编码 Agent 把 cobol_reference 翻译成其他语言,看它是否保留了截断语义

内网复现

  • 无需任何第三方依赖,只用 Python 3.8+ 标准库(decimal),无需联网,可直接在内网运行
  • 把 main.py 拷贝到内网机器执行 python main.py 即可
  • 如需参考 OpenAI 公开的 MultiLangBench(github.com/openai/MultiLangBench),需要外网或内网的 GitHub 镜像;没有镜像时本实验不受影响

代码

main.py88 行
#!/usr/bin/env python3
"""COBOL 定点运算「截断 vs 四舍五入」的迁移等价性小实验。

OpenAI 在介绍 COBOL 迁移评测时举例:COBOL 的定点运算默认截断多余小数位,
除非显式写 ROUNDED;把它改写成「四舍五入」的版本能正常运行,却会算出不同的金额。
本实验用 Python decimal 模拟一段简化的计息逻辑(PIC 9(7)V99),
对比两种迁移写法与「参考实现」的逐字节输出是否一致。

参考实现与数据均为本实验自拟,不是 OpenAI 评测中的程序。
"""
import sys
from decimal import Decimal, ROUND_DOWN, ROUND_HALF_UP

CENT = Decimal("0.01")

# 模拟 COBOL: COMPUTE WS-INTEREST = WS-BALANCE * WS-RATE / 12
# 目标字段 PIC 9(7)V99 —— 不带 ROUNDED,多余位截断
def cobol_reference(balance: Decimal, annual_rate: Decimal) -> Decimal:
    return (balance * annual_rate / Decimal(12)).quantize(CENT, rounding=ROUND_DOWN)


# 迁移写法 A:忠实保留截断语义
def migrated_truncate(balance: Decimal, annual_rate: Decimal) -> Decimal:
    return (balance * annual_rate / Decimal(12)).quantize(CENT, rounding=ROUND_DOWN)


# 迁移写法 B:「看起来更合理」的四舍五入(常见的错误改写)
def migrated_round(balance: Decimal, annual_rate: Decimal) -> Decimal:
    return (balance * annual_rate / Decimal(12)).quantize(CENT, rounding=ROUND_HALF_UP)


# 迁移写法 C:用浮点数再格式化(另一种常见改写)
def migrated_float(balance: Decimal, annual_rate: Decimal) -> Decimal:
    return Decimal(f"{float(balance) * float(annual_rate) / 12:.2f}")


def accounts():
    """确定性生成 1000 个账户:余额 100.00~99999.99,年利率 0.0125~0.0800。"""
    rates = [Decimal("0.0125"), Decimal("0.0199"), Decimal("0.0350"), Decimal("0.0475"), Decimal("0.0800")]
    seed = 20261021
    for i in range(1000):
        seed = (seed * 1103515245 + 12345) % (2 ** 31)
        cents = 10000 + seed % 9990000
        yield Decimal(cents) / 100, rates[i % len(rates)]


def render(fn) -> bytes:
    """输出成 COBOL 风格的定长报表行,用于逐字节比较。"""
    lines = []
    for i, (bal, rate) in enumerate(accounts()):
        interest = fn(bal, rate)
        lines.append(f"{i:06d}{int(bal * 100):09d}{int(interest * 100):09d}")
    return ("\n".join(lines) + "\n").encode("ascii")


def main() -> int:
    ref = render(cobol_reference)
    ok = True
    results = {}
    for name, fn in [("truncate", migrated_truncate), ("round_half_up", migrated_round), ("float", migrated_float)]:
        out = render(fn)
        ref_lines, out_lines = ref.splitlines(), out.splitlines()
        diff = sum(1 for a, b in zip(ref_lines, out_lines) if a != b)
        cents = sum(int(b[-9:]) - int(a[-9:]) for a, b in zip(ref_lines, out_lines))
        results[name] = (out == ref, diff, cents)
        print(f"{name:14s} byte_parity={out == ref!s:5s} diff_rows={diff:4d}/1000 total_delta_cents={cents:+d}")

    # 检查:忠实迁移必须逐字节一致;四舍五入版本应能被逐字节对比发现
    if not results["truncate"][0]:
        print("FAIL: 保留截断语义的迁移应当与参考实现逐字节一致")
        ok = False
    if results["round_half_up"][0]:
        print("FAIL: 四舍五入版本竟与参考一致,样本不足以暴露差异")
        ok = False
    if results["round_half_up"][2] <= 0:
        print("FAIL: 四舍五入版本的总金额应高于截断版本")
        ok = False

    t, r, f = results["truncate"], results["round_half_up"], results["float"]
    print(
        f"RESULT: truncate parity={t[0]}; round_half_up parity={r[0]} "
        f"({r[1]} rows differ, +{r[2]} cents); float parity={f[0]} ({f[1]} rows differ)"
    )
    return 0 if ok else 1


if __name__ == "__main__":
    sys.exit(main())
requirements.txt1 行
# 无第三方依赖,只使用 Python 标准库
Esc
↑ ↓ 选择Enter 打开

键盘快捷键

j / k
下一条 / 上一条
o
展开或收起当前条目
Enter
打开当前条目详情
c
复制当前条目链接
s
收藏 / 取消收藏当前条目
/ 或 ⌘K
搜索
1 2 3
切换密度:标题 / 摘要 / 完整
t
切换主题
?
显示本帮助