合计差三元,可每一行都没错
目标
把各货币不同的最小单位做成表,用整数最小单位重新加载手续费账本,把舍入模式钉成策略,并把连多出的那 1 也能对上的分配,以及折算顺序造成的差别,作为产出物留下来。
为什么重要
如果用实数处理金额,每一行就会差 1,而这个程度,单元测试抓不到。即使收到合计对不上的咨询,也指不出是哪一行错了,于是就形成了“重新跑一遍就对了”的运维流程。 这里要定的有三件事。存储格式(整数最小单位)、截取到哪一位和舍入模式(策略)、余数的分配规则(最大余数法)。这三件事技术都不会替你定,需要由人来定,并写进文档。 评分器不会拿你写下的数字互相比较。它会从第 1 步做出的原始数据重新计算基准值来对照。所以原样遵守规则很重要。
步骤
- 创建并运行 /root/money/gen_money.py,生成 /root/money/currencies.json、/root/money/fees.csv(600 行)和 /root/money/splits.csv(30 件)。
- 把加成率 0.125 分别用实数路径和整数路径计算,把出现偏差的行数和差额写入 /root/money/float_gap.json。
- 在 /root/money/money.db 中以 fee(整数最小单位)和 fee_real(REAL)两张表加载,并把各货币的两个合计写入 /root/money/store.json。
- 把解析和显示收拢到 /root/money/money_fmt.py,对说明中的 12 个输入作出判定,生成 /root/money/format_check.json。
- 把同样的乘法分别用 ROUND_HALF_UP 和 ROUND_HALF_EVEN 截取,把出现分歧的行数和两个合计写入 /root/money/rounding.json。
- 用最大余数法分配 splits.csv 的 30 件,写入 /root/money/alloc.json。合计必须与总额恰好相等。
- 用说明中的汇率表,把 5 种货币折算成韩元,把按行和按总额两种值写入 /root/money/fx.json。
- 把前面各步骤的结果汇总到一个 /root/money/report.json 中,并一起写上 fees.csv 的 sha256。
参考
- 第 1 步的生成规则:货币按 KRW、JPY、USD、KWD、BHD、CLP 的顺序循环使用(row_id 除以 6 的余数),row_id 从 0 到 599,最小单位金额是
1000 + (row_id * 37) % 9001,amount_text 是把该金额按货币的小数位数显示出来的文字。 - 第 1 步的分配规则:case_id 从 0 到 29,货币按 KRW、USD、KWD 循环使用,total_minor 是
100000 + case_id * 7777,权重是1 + (case_id*3) % 9、1 + (case_id*5) % 7、1 + (case_id*7) % 5。 - 第 2 步的两条路径:实数一侧是
int(round(round(float(amount_text) * 0.125, 소수자리) * 10 ** 소수자리))(占位符为小数位数),整数一侧是把Decimal(amount_text) * Decimal("0.125")在小数位上用 ROUND_HALF_UP 做 quantize 之后,再换成最小单位的值。 - 第 4 步的 12 个输入:
KRW 1234、JPY 0、USD 0.05、KWD 0.001、BHD 12.345、CLP 99、USD 1234567.89、USD 1.234、KRW 12.5、XAU 1.0、ZZZ 5、USD 1,234.56。拒绝的原因只使用too_many_minor_digits、unknown_minor_units、unknown_currency、not_a_number四种。 - 第 7 步的汇率表(以韩元为基准,小数点后四位):JPY 9.0421 · USD 1381.5027 · KWD 4512.3007 · BHD 3665.1420 · CLP 1.4508。这些数字是本实验的假设——不是真实的公布汇率,而是为了展示“小数点后四位”这项契约而编造的值。只有各货币的小数位数(最小单位)是 ISO 4217 的真实值。
- 常见错误:忘记 Python 内置的
round()是银行家舍入,在日元上加小数点,分配时丢掉余数,把折算顺序混在一起。 - 确认:
python3 -c "import json;print(json.load(open('/root/money/float_gap.json'))['mismatch_rows'])"
生成货币表和手续费账本
创建并运行 /root/money/gen_money.py,生成 /root/money/currencies.json、/root/money/fees.csv(600 行)和 /root/money/splits.csv(30 件)。
货币表的小数位数,直接使用阅读材料中写的 List One 的值。小数位数为 N.A. 的货币不是数字,所以是 null。amount_text 是把最小单位整数做成的文字,所以 0 位货币没有小数点。
用实数计算的加成金额有多少行出现偏差
把加成率 0.125 按说明中的两条路径计算,并把 rate、rows、mismatch_rows、gap_minor_total 和 by_ccy 写入 /root/money/float_gap.json。
两条路径的差别来自两处。0.125 在二进制中是精确的,但 12.345 这样的金额就不是,而且 Python 内置的 round() 会把 0.5 靠向偶数一侧。by_ccy 是为每种货币放入 mismatch_rows 和 gap_minor 两个值的对象。
用整数最小单位重新加载
在 /root/money/money.db 中创建 fee(row_id, ccy, minor, exponent)和 fee_real(row_id, ccy, amount REAL)两张表,各放入 600 行,并把各货币的 sum_minor、real_round_minor 和 equal 写入 /root/money/store.json。
SQLite 的数据类型附着在值上,而不是列上。即使声明为 INTEGER,放入实数也会以 REAL 存入,请用 typeof() 确认一下。real_round_minor 是把 REAL 的合计乘以该货币的小数位数对应的倍数后舍入得到的整数。equal 也可能全部是 true——在这个规模下,REAL 之和碰巧会舍入成同一个值,所以这类缺陷才会长期存活。请照实写。
把解析和显示收拢到一道门
在 /root/money/money_fmt.py 中做出 parse 和 fmt,对说明中的 12 个输入作出判定,并在 /root/money/format_check.json 中写成 ok 和 rejected。
ok 项中放入 in、ccy、minor 和 round_trip。round_trip 是把最小单位重新做成文字后的值,必须与原来的写法相同。拒绝的原因只有四种,完全不认识货币,与不知道小数位数,是不同的原因。
把舍入模式钉成策略
把同样的乘法分别用 ROUND_HALF_UP 和 ROUND_HALF_EVEN 截取,并把 policy、rate、diff_rows、half_up_total_minor 和 half_even_total_minor 写入 /root/money/rounding.json。本实验的策略是 ROUND_HALF_UP。
乘法只做一次,quantize 做两次就行。0.125 在二进制中是精确的值,所以经常会出现恰好一半的位,两种模式就是在那里产生分歧的。如果两个合计相同,说明其中一边实际上没有被应用。
连多出的那 1 也能对上的分配
用最大余数法把 splits.csv 的 30 件分配到 3 处,并把 cases、sum_ok 和 remainder_total 写入 /root/money/alloc.json。
商按向下取整给出,只把剩下的部分每处多给 1。由谁得到,按余数(总额乘以权重再除以权重之和所得的余数)从大到小的顺序,相同则编号靠前的先拿。remainder_to 按升序写出多拿了 1 的位置从 0 开始数的编号。
折算顺序会改变结果
用说明中的汇率表把 5 种货币折算成韩元,并把 rate_scale、round_mode 和 by_ccy(rate, rows, row_then_sum_krw, sum_then_row_krw, gap_krw)写入 /root/money/fx.json。
逐行折算、按韩元单位截取再相加的值,与先全部相加之后只折算一次的值,是不同的。两个都要计算,并把差别原样写出来。韩元的小数位数是 0,所以截取的位是 1 韩元。
把六个步骤汇总成一份报告
在 /root/money/report.json 中写入 fees_sha256、rows、currencies、exact_total_minor、float_gap_rows、float_gap_minor、rounding_policy、half_even_diff_rows、allocation_cases、allocation_remainder_total、fx_gap_krw 和 verdict。verdict 是 integer-minor-units。
读取前面步骤的产出物来汇总,但哈希要现在从 fees.csv 重新计算。如果报告指不出它依据的是哪份数据,日后就无法解释这些数字。exact_total_minor 是各货币的最小单位合计。