复式记账要求一笔交易所有分录的权重之和为零,但账本里的数字带有各自的小数位,成本与价格换算又会产生长尾小数,"严格等于零"在实际数据上不成立。interpolate.py
定义三件事:残差怎么算(compute_residual)、多大的残差算平(infer_tolerances,从用户写的数字小数位推断,按币种给出容差)、自动补出的数字要不要按容差取整(quantize_with_tolerance)。另外提供把残差记到指定账户的工具(get_residual_postings/fill_residual_posting)和两个余额统计函数(compute_entries_balance/compute_entry_context)。
模块名是历史遗留:真正"填补缺失数字"的插值逻辑曾经在这里(balance_incomplete_postings,CHANGES:3269-3272),2016-10-14
commit 8f14afb6 把它移到
parser/booking_simple.py,2018-03-13 commit
2be15243 连同 SIMPLE 记账法一起删除;现行插值在
beancount/parser/booking_full.py:798 (interpolate_group)
起的函数里,本模块只提供它和最终校验共用的数值规则。
| 名称 | 位置 | 作用 |
|---|---|---|
MAXIMUM_TOLERANCE = D("0.5") |
interpolate.py:28-30 |
由成本/价格推出的容差上限 |
MAX_TOLERANCE_DIGITS = 5 |
:33-35 |
判定"用户手写容差"的系数位数阈值 |
class BalanceError(NamedTuple) |
:38-43 |
source: Meta、message: str、entry: Balance;本模块不实例化,只被
ops/balance.py 使用 |
is_tolerance_user_specified |
:46-58 |
系数位数 < 5 即视为用户指定 |
has_nontrivial_balance |
:61-69 |
返回 posting.cost or posting.price,不是布尔值 |
compute_residual |
:72-94 |
权重求和为 Inventory,跳过 __residual__
分录 |
infer_tolerances |
:97-231 |
按币种推断容差,返回 ImmutableDictWithDefault |
AUTOMATIC_META / AUTOMATIC_RESIDUAL /
AUTOMATIC_TOLERANCES |
:234-242 |
三个 meta
键:__automatic__、__residual__、__tolerances__ |
get_residual_postings /
fill_residual_posting |
:245-286 |
把残差做成打到 account_rounding 的分录 |
compute_entries_balance |
:289-312 |
按账户前缀、截止日期汇总所有分录 |
compute_entry_context |
:315-361 |
某条目涉及账户在它之前/之后的余额 |
quantize_with_tolerance |
:364-394 |
容差像用户手写时才 quantize |
# Meta-data field appended to automatically inserted postings.
# (Note: A better name might have been '__interpolated__'.)
AUTOMATIC_META = "__automatic__"
# Meta-data field appended to postings inserted to absorb rounding error.
AUTOMATIC_RESIDUAL = "__residual__"
# Meta-data field added for the tolerances inferred for this entry.
AUTOMATIC_TOLERANCES = "__tolerances__"
interpolate.py:234-242。写入方与读取方分散在三个文件:__automatic__
由 booking_full.py:1013-1016 打在插值补出的 posting
上,infer_tolerances(:177)据此跳过,plugins/check_commodity.py:50
把它列入不扫描币种的 meta 键;__residual__ 由
get_residual_postings(:255)与
__automatic__
一起写入,compute_residual(:90)据此跳过;__tolerances__
由 booking_full.py:238-240 写进交易 meta,值是
tolerances_max,interpolate_test.py:540-555 与
booking_full_test.py:3499,3520
直接断言它。__automatic__ 标记随 2015-05-17
的容差改版引入(CHANGES:3916-3917);__residual__
来自 2015-05-20 commit
ca3d3b28;__tolerances__ 来自 2016-10-14
commit cc82c533。
compute_residual 与 #48inventory = Inventory()
for posting in postings:
# Skip auto-postings inserted to absorb the residual (rounding error).
if posting.meta and posting.meta.get(AUTOMATIC_RESIDUAL, False):
continue
# Add to total residual balance.
inventory.add_amount(convert.get_weight(posting))
return inventory
interpolate.py:87-94。权重规则在
convert.py:63-106 (get_weight):有 Cost 且
cost.number 是 Decimal 时取
cost.number × units.number(币种为成本币种);否则有价格时取
price.number × units.number(币种为价格币种);否则就是
units。残差以 add_amount
累加,不带成本,所以残差 Inventory 里的 position 永远
cost is None。
跳过 __residual__ 分录的原因是 issue
#48(ca3d3b28,2015-05-20;CHANGES:3836-3838):account_rounding
自动插入吸收尾差的分录后,交易在数值上已经严格为零,平账校验被"顺带"关掉了;把这类分录排除出残差计算,校验看到的仍是用户原始分录的残差。interpolate_test.py:125-129
锁定:插入 Equity:Rounding 0.0000001 USD
后再算残差,结果仍是 -0.0000001 USD。
units = posting.units
if not (isinstance(units, Amount) and isinstance(units.number, Decimal)):
continue
# Compute bounds on the number.
currency = units.currency
expo = units.number.as_tuple().exponent
if expo < 0:
# Note: the exponent is a negative value.
tolerance = ONE.scaleb(expo) * tolerance_multiplier
interpolate.py:179-188。容差 = 最小小数位 ×
tolerance_multiplier:5.00 USD 的 exponent 为
-2,ONE.scaleb(-2) 得
0.01,乘默认 0.5 得
0.005。乘数默认 0.5 的理由写在
options.py:523-525:"We normally assume that the
institution we're reproducing this posting from applies rounding, and so
the default value for the multiplier is 0.5, that is, half of the
smallest digit encountered"。整数金额(exponent 为 0
或正数)自身不推断容差——if expo < 0:(:186)为假时连同其内嵌套的成本、价格容差计算(:199-222,见
§3.7)一起跳过。该币种最终容差取决于
inferred_tolerance_default(见
§3.6):只有既没有该币种的显式默认值、也没有 * 默认值时才是
ZERO,此时 Inventory.is_small
要求分毫不差(inventory.py:161-163)。interpolate_test.py:288-297
锁定三条整数分录、且未配置默认容差时得到 {}。
成本与价格数字的小数位默认不参与:5 VHT @ 102.2340 USD
与 -511.11 USD 只得到
{"USD": 0.005}(interpolate_test.py:318-346
三个 ignore_* 测试)。
# Note that we take the max() and not the min() here because the
# tolerance has a dual purpose: it's used to infer the resolution
# for interpolation (where we might want the min()) and also for
# balance checks (where we favor the looser/larger tolerance).
if currency in tolerances:
tolerances[currency] = agg(tolerance, tolerances[currency])
else:
tolerances[currency] = tolerance
interpolate.py:190-197;agg 由
mode
决定(:149-150,assert mode in ("max", "min"))。注释是
2024-05-27 commit bc776058 补的,当时代码只有
max;mode 参数由 2026-04-28 commit
ec057e11 加入。interpolate_test.py:299-316
锁定 5.0000/5.000/5.00/5.0/5 USD 混合时取
0.05;:590-606 锁定 4.8 EUR 与
2.97 EUR 在 max 下得
0.05、min 下得 0.005。
__automatic__
分录interpolate.py:176-178:"Skip the precision on
automatically inferred
postings"。插值补出的数字由程序算出,其小数位不代表用户的记录精度。真实案例在
CHANGES:3252-3257(2015-08-30):split_expenses
插件生成的分录漏打这个标记,"their automatically calculated values would
end up being used for inferring the tolerances"。
判断方式是键存在性
AUTOMATIC_META in posting.meta(:177),不看值;compute_residual
对 __residual__ 用的是
.get(AUTOMATIC_RESIDUAL, False)(:90),只有真值才跳过。两处标记语义不对称:{"__automatic__": False}
仍会被 infer_tolerances 跳过,同样写法不会让
compute_residual 跳过对应分录。
inferred_tolerance_default 只保留本交易币种tolerances = {
currency: tol
for currency, tol in default_tolerances.items()
if currency == "*" or currency in seen_currencies
}
...
default = tolerances.pop("*", ZERO)
return defdict.ImmutableDictWithDefault(tolerances, default=default)
interpolate.py:168-172,230-231。seen_currencies
收集 units、cost、price 三处币种(:160-167)。2025-05-23
commit a3a6c644 之前是整份
default_tolerances.copy(),提交信息说 "This was
unnecessary"。*
键弹出后成为返回字典的默认值;ImmutableDictWithDefault.get
忽略调用方传入的第二个参数,转调
__getitem__:键存在时返回存值,缺键时才回落到
self.default(defdict.py:24-37),所以
inventory.py:162 里
tolerances.get(currency, ZERO) 的 ZERO
实际由字典自己的默认值决定,未配置 * 时为
ZERO。选项语法
<currency>:<tolerance>、*:0.5 见
options.py:484-503。
这份初始 tolerances
不是"推断失败时才用"的兜底值:它在循环之前写入(:168-172),此后
units 推出的容差与它按 agg
合并(:194-197),成本/价格容差也与合并结果再按
agg
合并一次(:224-228)。因此显式配置的默认容差始终参与
max/min 比较——mode="max"
下可能被推断值取代或维持,mode="min"
下同样可能被更小的推断值压低。
infer_tolerance_from_cost:成本与价格对容差的贡献cost = posting.cost
if cost is not None:
cost_currency = cost.currency
if isinstance(cost, Cost):
cost_tolerance = min(tolerance * cost.number, MAXIMUM_TOLERANCE)
else:
assert isinstance(cost, CostSpec)
cost_tolerance = MAXIMUM_TOLERANCE
for cost_number in cost.number_total, cost.number_per:
if cost_number is None or cost_number is MISSING:
continue
cost_tolerance = min(tolerance * cost_number, cost_tolerance)
cost_tolerances[cost_currency] += cost_tolerance
interpolate.py:202-215,价格分支 :217-222
同形;整段(:199-222)嵌套在 if expo < 0:
内(见 §3.3),units
是整数的分录即使开启本选项也不贡献成本或价格容差。use_cost
为 None 时读
options_map["infer_tolerance_from_cost"](:152-153),选项默认
False(options.py:577),说明文字承诺
"Enabling this flag only makes the tolerances potentially
wider"(:575)——这只在默认的 mode="max"
下成立。booking_full.py:174-177 在
use_precise_interpolation 开启时以 mode="min"
调用同一函数(agg 变为
min,interpolate.py:149-150,224-228),成本/价格候选若比已有容差更小会把它压低,是收紧而非放宽(详见
§3.9)。规则:units 的容差乘以成本或价格数值,封顶
MAXIMUM_TOLERANCE = 0.5;CostSpec 的
number_total、number_per 都是
None/MISSING
时循环整体跳过,cost_tolerance 保留初始值
MAXIMUM_TOLERANCE(:208-215)——成本数字完全缺失反而贡献封顶容差。同一成本币种的贡献用
defaultdict(D) 累加(:174,215,222),再与
units 推出的容差按 agg
合并(:224-228)。累加而非取最大来自
2015-06-04(CHANGES:3766-3771):"We assume no more
rounding events than the number of postings held at cost"。docstring
:111-131 给出算例:两条
18.572 VWELX {30.96 USD} 各贡献
0.001 × 0.5 × 30.96 = 0.01548,USD 容差
max(0.005, 0.03096) = 0.03096;interpolate_test.py:491-556 (test_tolerances__bug53b)
按 2、3、16 条持仓分录分别断言
0.03096、0.04644、0.247680。
这段有过反复:2015-05-03 dbe1fcc1 删除("causes too many
problems"),同日 9c055c66 又加回带注释的
use_cost 参数,2015-05-30 8ca3abe7
恢复,2015-06-06 3a767c99 引入乘数选项与
MAXIMUM_TOLERANCE,2016-10-23 36da2cd3 增加
CostSpec 分支,2017-04-20 f7facc1f(#164)在
CostSpec 循环里跳过 MISSING,2017-06-25
a2d2f6a1 把 units/price 的判断改成
isinstance(..., Amount) and isinstance(.number, Decimal),修复
Assets:Checking USD @ 1.32 CAD
这种只缺数字的分录(interpolate_test.py:579-588)。
tolerance = tolerances.get(currency)
if tolerance:
# TODO(blais): "2" is used here but really it ought to be the reciprocal
# of the "tolerance_multiplier" value. The better fix would be to apply
# the multiplier late elsewhere, and to just not apply the multiplier
# here.
quantum = (tolerance * 2).normalize()
...
if is_tolerance_user_specified(quantum):
number = number.quantize(quantum)
return number
interpolate.py:375-394;is_tolerance_user_specified
在 :46-58,判据是
len(tolerance.as_tuple().digits) < MAX_TOLERANCE_DIGITS。四个要点:
quantum(容差乘 2 再
normalize()),不是容差本身。normalize()
去掉尾零:Decimal("0.005") * 2 是
0.010(exponent −3),normalize() 后是
0.01(exponent −2);Decimal.quantize
只取右操作数的 exponent,所以没有 normalize()
会多量化一位。0.5 的倒数,硬编码;乘数改成 1.1
时容差 0.011 的 quantum 是
0.022,系数两位仍判为"用户指定",量化到三位小数。TODO
注释承认应取 tolerance_multiplier 的倒数。:383-391 引用 Decimal.quantize 文档说明
InvalidOperation
的触发条件(量化后系数长度超过上下文精度)。但 guard 只看 quantum
的位数,不看被量化数字的量级,quantize_with_tolerance({"USD": D("0.005")}, "USD", D("1E+30"))
仍抛 InvalidOperation(本机直接调用函数验证)。:393 的 number.quantize(quantum) 既没有传
rounding=,也没有建立自己的
localcontext,用的是调用者当时的 Decimal
上下文舍入模式。全仓库非测试代码没有设置舍入模式(grep ROUND_
无命中;用 localcontext 的只有
display_context.py:321 与
scripts/example.py:441,465,三处改的都是 prec
而非 rounding),所以新进程下走的是 Decimal
默认的
ROUND_HALF_EVEN;但这不是函数自身的保证——本机把当前上下文的
rounding 改为 ROUND_HALF_UP
后,Decimal("7.85").quantize(Decimal("0.1")) 从默认的
7.8 变为 7.9。interpolate_test.py:609-631
锁定:{"USD": 0.01} 加默认 0.000005 时
100.123123123 分别量化为 100.12(USD)和
100.12312(CAD,走默认值);默认为 ZERO 时
if tolerance: 为假,CAD 原样返回。
# Get the list of tolerances.
tolerances_max = interpolate.infer_tolerances(
entry.postings, options_map, mode="max"
)
if options_map["use_precise_interpolation"]:
tolerances_interp = interpolate.infer_tolerances(
entry.postings, options_map, mode="min"
)
else:
tolerances_interp = tolerances_max
booking_full.py:170-179;tolerances_interp
传给
interpolate_group(:230),tolerances_max
写入
__tolerances__(:239)。来龙去脉:ec057e11(2026-04-28)随提交附带的
rounding_fix_explanation.md(次日 573f04bc
删除,git show ec057e11:rounding_fix_explanation.md
可取回)描述了问题——4.8 EUR + 2.97 EUR 的缺失腿应为
-7.77,但单一的 max 容差 0.05 让 quantum 变成
0.1,补出 -7.8,账户里留下
0.03 EUR 的不平;修复是插值用 min 容差、校验和 meta 仍用
max 以保持 "backward compatibility with permissive balancing
rules"。ec057e11 最初无条件启用;2026-05-02
5704a861 加入 use_precise_interpolation
选项并默认关闭(options.py:700-717),提交信息:"This
change makes that behavior optional and disabled by default to maintain
backward compatibility"(其中引用的 8e4bdd99
是合并该修复分支的 merge
commit)。booking_full_test.py:3481-3520
两个对照测试锁定默认得 -7.8、开启得
-7.77,两者 __tolerances__["EUR"] 都是
0.05。
量化只发生在缺 units 的情形:booking_full.py:945-947 是
quantize_with_tolerance
的唯一生产调用点。COST_PER(:964)与
PRICE(:1003)是除法,COST_TOTAL(:979,number_total = weight - cost.number_per * units.number)是乘减,三者结果都不经过
quantize_with_tolerance,直接采用调用当时的
Decimal 上下文精度:新进程默认是 28
位(getcontext().prec),但可被调用者改变(本机在
localcontext 内把 prec 设为 6 后
Decimal(1)/Decimal(7) 得
0.142857),且精确结果不会被补足到 28
位有效数字(默认上下文下 Decimal(1)/Decimal(2) 就是
0.5)。
account_rounding 的接线状态meta = {AUTOMATIC_META: True, AUTOMATIC_RESIDUAL: True}
return [
Posting(account_rounding, -position.units, position.cost, None, None, meta.copy())
for position in residual.get_positions()
]
interpolate.py:255-259;fill_residual_posting(:262-286)在残差非空时追加这些分录。docstring
:266-269 写明它是为导出 Ledger 而做,"A better method would
be to enable the feature that automatically inserts these rounding
postings on all transactions"。options.py:424-439 的
account_rounding 选项说明仍承诺 "setting this value to an
account name will automatically enable the addition of postings on all
transactions that have a residual amount"。全仓库搜索
account_rounding,除
interpolate.py(参数名)、options.py(选项声明)与测试外,还命中
CHANGES:1850,3229,3233,3237,3837,3880,3883
这几处历史记录;排除测试与 CHANGES 后,生产 Python
代码没有读取 options_map["account_rounding"]
的调用路径,fill_residual_posting
没有非测试调用点;get_residual_postings
的唯一非测试调用者是同模块的
fill_residual_posting(:284),因此整条链在生产路径上都不可达。自动插入曾经存在:2015-05-16
ef6e0a2d 实现于解析阶段,2016-10-14 8f14afb6
随 balance_incomplete_postings 移到
booking_simple.py:176(该文件被删除前,这处已移到
:276),2018-03-13 2be15243 删除
booking_simple
后失去最后一个生产读取点;fill_residual_posting
的最后调用者 reports/convert_reports.py:169,299 随
2020-07-05 a7c4f14f 删除 bean-report 一起消失。
compute_entries_balance 与
compute_entry_contextinterpolate.py:289-312:遍历条目,date
为排他截止日,遇到第一条 entry.date >= date 即
break(:306-307,依赖条目已排序);prefix
按账户名前缀过滤(:310);用
add_position(posting)
累加,保留成本(interpolate_test.py:179-200 断言
10 HOOL {40 USD} 与
-400 USD),价格被忽略(:202-220 断言
2000.00 EUR 与 -3560.00 GBP)。生产调用点是
summarize.py:542,用来算换算差额。
interpolate.py:315-361:先取 context_entry
涉及的账户集合,additional_accounts
可补充(:337-339,2021-01-30 06127f16 为
bean-doctor context 加入,让 booking
报错、分录被丢弃的交易也能显示账户余额);顺序累加直到
entry is context_entry(:344-346),再深拷贝并叠加该交易自身的分录(:355-359)。:357
用的是循环变量 entry 而非
context_entry,正确性依赖 break
命中。context_entry 是 Transaction 但不在
entries 里时,循环耗尽后 entry
停在列表最后一项:末条恰好是 Transaction
则错误叠加它的分录;末条不是 Transaction(没有
postings 属性)则抛
AttributeError;entries 为空时
entry 从未被赋值,抛
UnboundLocalError(本机对三种情形分别调用验证)。
| 函数 | 签名 | 返回 | 生产调用点 |
|---|---|---|---|
compute_residual(postings) |
:72 |
Inventory |
booking_full.py:893(其它分录的残差决定缺失腿权重)、validation.py:375、context.py:168 |
infer_tolerances(postings, options_map, use_cost=None, mode="max") |
:97 |
ImmutableDictWithDefault |
booking_full.py:171,175、validation.py:376、context.py:174、sellgains.py:133 |
quantize_with_tolerance(tolerances, currency, number) |
:364 |
Decimal |
booking_full.py:945 |
get_residual_postings(residual, account_rounding) /
fill_residual_posting(entry, account_rounding) |
:245,262 |
list[Posting] / Transaction |
无 |
compute_entries_balance(entries, prefix=None, date=None) |
:289 |
Inventory |
summarize.py:542 |
compute_entry_context(entries, context_entry, additional_accounts=None) |
:315 |
(dict, dict) |
context.py:110 |
has_nontrivial_balance(posting) |
:61 |
cost or price |
printer.py:239(决定是否渲染权重列) |
is_tolerance_user_specified(tolerance) |
:46 |
bool |
仅 :392 |
最终裁决在
validation.py:350-386 (validate_check_transaction_balances):对每笔交易
compute_residual + infer_tolerances(默认
mode="max")+
residual.is_small(tolerances),不平则报
"Transaction does not balance: {residual}"。:365-372
的注释以 IMPORTANT 标注:"This must come after the user
routines, because unbalancing input is legal, as those types of
transactions may be 'fixed up' by a user-plugin"。加载顺序在
loader.py:605-627:booking →
run_transformations(插件)→
validation.validate;该校验登记在
validation.py:390-398 (BASIC_VALIDATIONS)。
| 决策 | 理由 | 证据 |
|---|---|---|
| 容差从 units 小数位推断而非全局固定值 | 2015-05-17 改版:"inferred tolerances may be smaller than the fixed value we used previously";不同币种、账户的记录精度不同 | CHANGES:3847-3893;interpolate.py:184-188 |
| 乘数默认 0.5 | 假设数据来源机构已做舍入,误差至多半个最小位 | options.py:523-525;CHANGES:3746-3748 |
| 多分录同币种取 max | 容差同时服务插值与校验,校验偏向宽松 | interpolate.py:190-193;bc776058 |
| 插值另算一份 min 容差,默认不启用 | 单一 max 容差会把补出的数字过度取整;默认关闭为兼容旧账本 | booking_full.py:170-179;ec057e11、5704a861;options.py:701-708 |
跳过 __automatic__ 分录 |
程序算出的小数位不代表记录精度 | interpolate.py:176-178;CHANGES:3252-3257 |
残差排除 __residual__ 分录 |
吸收尾差的分录会让校验失效(#48) | interpolate.py:89-91;ca3d3b28 |
| 成本/价格的贡献累加并封顶 0.5 | 每条持仓分录各有一次舍入;上限防止大额成本放大容差 | CHANGES:3766-3771;interpolate.py:28-30,207,210,221 |
| 只有"像用户手写"的容差才量化 | 推断出的长小数容差量化会得到无意义的位数或
InvalidOperation |
interpolate.py:46-58,383-392;c8ec7c0b |
| 默认容差只复制本交易币种 | 复制整份字典无必要 | interpolate.py:157-172;a3a6c644 |
| Balance/Pad 容差取乘数的两倍 | 用户手填的核对数字误差来源更杂,"Be generous" | ops/balance.py:36-41 |
| 平账校验放在所有插件之后 | 允许用户输入不平的交易由插件修正 | validation.py:365-372;loader.py:605-627 |
| 现象 | 后果 | 证据 |
|---|---|---|
| 整数金额自身不推断容差 | 若该币种无显式默认容差、无 * 默认值,容差才是
ZERO;is_small 用严格 >
比较,此时任何非零残差都报错 |
interpolate.py:157-172,186-222;inventory.py:161-163;interpolate_test.py:288-297 |
| 边界是"严格大于才不平" | 乘数 1.1 时差 0.011 EUR 通过、差 0.012 EUR
报错 |
inventory.py:164;interpolate_test.py:442-459 |
quantize 只取 quantum 的 exponent |
容差 0.01 的 quantum 0.02
量化到两位小数,不是 0.02 的倍数 |
interpolate.py:381,393;interpolate_test.py:614-617 |
| 乘数不是 0.5 时 quantum 与容差错位 | 0.011 → quantum 0.022,量化到三位小数 |
interpolate.py:377-381 |
guard 不防 InvalidOperation |
大数量化仍抛异常 | interpolate.py:383-393;本机验证 |
| 舍入模式取自调用者当前 context,非函数自身保证 | 新进程默认 ROUND_HALF_EVEN 时 7.85
量化到一位得 7.8;context 改为 ROUND_HALF_UP
时得 7.9 |
interpolate.py:393;非测试代码无
ROUND_;本机验证 |
| 两个调用点看到的成本类型不同 | booking_full.py:171 在 booking 前调用,成本是
CostSpec;validation.py:376 在 booking
后调用,成本是 Cost。开启
infer_tolerance_from_cost 且写 {{总价}}
时,语法层把 number_per 置为
ZERO(grammar.py:602-606),min(tolerance × 0, ...)
为 0,成本对 __tolerances__ 无贡献;校验阶段
Cost.number
是总价除以数量,贡献不为零(本机直接调用验证:-1.729 CAAPX {{521.67787 USD}} @ 49.65 USD
两阶段 USD 容差分别为 0.024825 与
0.1756…) |
interpolate.py:206-214;grammar.py:590-606 |
| 只有缺 units 的插值结果被量化 | COST_PER/PRICE 的除法与
COST_TOTAL 的乘减都不经过
quantize_with_tolerance,采用调用时的 Decimal
上下文精度(新进程默认 28 位,可被调用者改变) |
booking_full.py:945-947,964,979,1003 |
has_nontrivial_balance 返回对象而非布尔 |
printer.py:239 用 any(map(...))
消费,真值语义足够 |
interpolate.py:69 |
compute_entry_context 的 :357
使用循环变量 |
context_entry 不在列表中时:末条是
Transaction 则叠加其分录,否则抛
AttributeError;空列表抛
UnboundLocalError |
interpolate.py:344-359;本机验证 |
| 残差 position 无成本 | get_residual_postings 的 position.cost
实际总是 None |
interpolate.py:93,257;convert.py:63-106 |
BalanceError 定义在此但只被 ops/balance.py
用 |
entry 字段类型标注为
Balance,与本模块"postings 平账"的 docstring 不对应 |
interpolate.py:38-43;ops/balance.py:13,113,127,161 |
【文档漂移】compute_residual docstring 说返回 "the
per-currency precision" |
实际只返回 Inventory;2015-04-13 d67cb7e6
加的是 "an unimplemented 'precision' return
argument",参数后来删去、文字留下 |
interpolate.py:73-80,94 |
【文档漂移】quantize_with_tolerance docstring 参数顺序
number 在 currency 前,且写作
"Decimalvalues" |
签名是 (tolerances, currency, number) |
interpolate.py:364-370 |
【文档漂移】is_tolerance_user_specified docstring
夹有孤立的 # |
例子 "0.1234 but not 0.123456" 本身与代码一致(4 位通过、6
位不通过;5 位如 0.12345 也不通过) |
interpolate.py:47-51,58 |
【文档漂移】account_rounding 选项说明承诺自动插入 |
生产路径无读取点,见 3.10 | options.py:425-430 |
【文档漂移】测试名
test_tolerances__minimum_on_costs |
断言的是三条 VHT 分录取 max 后的 0.000005 |
interpolate_test.py:406-416 |
interpolate_test.py 共 27
个测试,四个类;:24-29 定义的 OPTIONS_MAP
在文件内没有被引用。
| 测试 | 行号 | 锁定的行为 |
|---|---|---|
test_has_nontrivial_balance |
40-56 | 无 cost/price 为假;有价格、有成本、两者皆有为真 |
test_compute_residual |
58-81 | 两条与四条分录的残差;用 reduce(convert.get_units)
比较 |
test_fill_residual_posting |
83-146 | 已平衡不插分录;-100.0000001 插
0.0000001;-112.69 CAD @ 0.8875 插
0.012375 USD;插入后残差计算忽略新分录 |
test_compute_entries_balance_* |
150-220 | 全部分录汇总为空;带成本保留成本;带价格只累加 units |
test_compute_entry_context |
222-284 | 交易前后余额;非 Transaction 条目前后相同 |
test_tolerances__no_precision /
dubious_precision |
288-316 | 整数得 {};混合精度取 0.05 |
test_tolerances__ignore_* /
cost_and_number_ignored |
318-356 | 默认不看 cost/price;整数 units 加成本仍 {} |
test_tolerances__number_on_cost_used /
_overrides |
358-380 | use_cost=True 得 USD: 0.051117;与
-511.0 的 0.05 取 max |
test_tolerances__number_on_cost_fail_to_succ |
382-404 | 同一交易:选项关闭报 0.20000 USD 不平,开启无错 |
test_tolerances__minimum_on_costs |
406-416 | 三种精度取 0.000005 |
test_tolerances__with_inference /
capped_inference |
418-440 | 缺失腿存在时的推断;0.05 × 102.2340 封顶为
0.5 |
test_tolerances__multiplier |
442-459 | 乘数 1.1:差 0.011 通过、0.012 报错 |
test_tolerances__bug / bug53a /
bug53b / bug53_price |
461-577 | 成本与价格累加后的 __tolerances__ 精确值;无报错 |
test_tolerances__missing_units_only |
579-588 | USD @ 1.32 CAD 可加载(无显式断言,靠
load_doc
默认要求零错误,loader.py:790-794) |
test_infer_tolerances_modes |
590-606 | max 得 0.05、min 得
0.005 |
test_quantize_with_tolerance |
610-631 | 显式容差与默认容差各自量化;默认 ZERO 不量化 |
interpolate_test.py
内没有直接用例的行为:is_tolerance_user_specified
的直接调用与 5 位边界;get_residual_postings
的直接调用;mode="min" 与 use_cost
组合;quantize_with_tolerance 的
InvalidOperation;compute_entries_balance 的
prefix 参数;compute_entry_context 的
additional_accounts;__automatic__
分录被跳过(本文件内无用例,booking_full
路径间接依赖)。
以下两项本文件内虽无用例,但由其它模块的测试间接覆盖:compute_entries_balance
的 date 参数——summarize_test.py:945-1009
多处以 date=date
调用并断言结果为空;inferred_tolerance_default 的
* 默认值经 infer_tolerances
的传递——validation_test.py:401-410 (test_tolerance_implicit_fractional_global)
用全局 *:0.005 加载一笔残差
0.002237 CAD(7.9599 × 125.63 − 1000)的交易并要求零错误,若
* 默认值未被传递到该交易的
tolerances,此残差在 ZERO 容差下会报错。
| 日期 | 提交 / 记录 | 变化 |
|---|---|---|
| 2014-11-03 | ad01c96c |
在当时的 core/complete.py 加入
fill_residual_posting,为 Ledger 导出补
Equity:Rounding 分录 |
| 2014-11-09 | 51130f08 |
complete.py 更名 interpolate.py |
| 2015-04-13 | d67cb7e6 |
compute_residual 加入未实现的 precision
返回参数(docstring 残留至今) |
| 2015-05-03 / 05-30 | dbe1fcc1、9c055c66、8ca3abe7 |
成本容差推断删除、加回 use_cost 注释、恢复 |
| 2015-05-16 | ef6e0a2d |
实现 account_rounding 自动插入残差分录 |
| 2015-05-17 | CHANGES:3845-3917 |
容差机制整体改版:自动推断、default_tolerance、account_rounding、__automatic__ |
| 2015-05-20 | ca3d3b28 |
#48:残差计算跳过 __residual__ 分录 |
| 2015-05-31 | c8ec7c0b |
is_tolerance_user_specified、MAX_TOLERANCE_DIGITS |
| 2015-06-04 | CHANGES:3766-3771 |
成本容差改为累加 |
| 2015-06-06 | 3a767c99 |
inferred_tolerance_multiplier 选项与
MAXIMUM_TOLERANCE;infer_tolerance_from_cost
脱离实验状态(CHANGES:3755-3758) |
| 2015-08-30 | CHANGES:3252-3257 |
split_expenses 漏打 __automatic__
导致容差误推断 |
| 2016-03-06 | CHANGES:2398-2399 |
#83:default_tolerance 更名
inferred_tolerance_default |
| 2016-10-14 | cc82c533、bbdb744b、8f14afb6 |
FULL 记账法写入 __tolerances__、实现
quantize_with_tolerance;balance_incomplete_postings
移出本模块 |
| 2016-10-23 | 36da2cd3 |
成本容差增加 CostSpec 分支 |
| 2016-10-30 / 12-17 | 99ee920a、dfc75f3b;CHANGES:1836-1851 |
删除 use_legacy_fixed_tolerances 与
LEGACY_DEFAULT_TOLERANCES;删除
default_tolerance、tolerance、experiment_explicit_tolerances
等废弃选项及 account_rounding 的前缀警告代码 |
| 2017-01-14 | 43c5630e、f30c2e46 |
废弃
compute_cost_basis、get_posting_weight(改用
convert.get_weight) |
| 2017-04-20 | f7facc1f |
#164:CostSpec 循环跳过 MISSING |
| 2017-06-25 | a2d2f6a1 |
接受只缺数字的分录 USD @ 1.32 CAD |
| 2018-03-13 | 2be15243 |
删除 booking_simple,account_rounding
失去生产读取点 |
| 2018-03-23 | b04cafdd |
删除已废弃方法 |
| 2020-07-05 | a7c4f14f |
删除 bean-report,fill_residual_posting 失去调用者 |
| 2021-01-30 | 06127f16 |
compute_entry_context 增加
additional_accounts |
| 2024-05-27 | bc776058 |
补注释说明取 max() 的取舍 |
| 2024-12-22 | 85950542 |
BalanceError 改为带类型注解的 NamedTuple
类 |
| 2025-05-23 | a3a6c644、ae5d5f14 |
默认容差只复制本交易币种;inferred_tolerance_multiplier
更名 tolerance_multiplier(旧名以 alias
保留并标记废弃,options.py:544-560) |
| 2026-04-28 | ec057e11(merge 8e4bdd99) |
mode="max"/"min";插值用 min 容差;附
rounding_fix_explanation.md(次日 573f04bc
删除) |
| 2026-05-02 | 5704a861 |
use_precise_interpolation 选项,默认关闭 |
convert.get_weight
定义(convert.py:63-106),Inventory.add_amount/add_position/is_small/is_empty
提供聚合与判定(inventory.py:132,152-166,402,459);Cost/CostSpec
的区分决定成本容差走哪个分支;返回值是 dict 子类
ImmutableDictWithDefault(defdict.py:14-51),只覆写
__setitem__ 阻止下标赋值并提供 *
缺省值语义,继承自 dict 的
update/pop/setdefault/clear
仍可直接修改内容(本机验证四者均成功),并非强不可变语义。booking_full.py:170-179
算两份容差,:230 把 tolerances_interp 传入
interpolate_group,:893 用
compute_residual
求其它分录的权重和作为缺失腿的目标值,:945 量化补出的
units,:1016 打
__automatic__,:239 写
__tolerances__。validation.py:350-386
是"Transaction does not
balance"的唯一来源,在插件之后运行(loader.py:605-627)。ops/balance.py:20-45 (get_balance_tolerance)
不经过 infer_tolerances,只共用
tolerance_multiplier 选项且乘 2;~
显式容差优先(:29-31)。BalanceError
从本模块导入(:13)。context.py:168,174 为
bean-doctor context
重算残差与容差;sellgains.py:133 取容差后再乘
EXTRA_TOLERANCE_MULTIPLIER;summarize.py:542
用 compute_entries_balance
求换算差额;printer.py:239 用
has_nontrivial_balance 决定权重列。options.py:424-439(account_rounding)、:483-512(inferred_tolerance_default)、:513-543(tolerance_multiplier)、:544-560(废弃别名)、:561-578(infer_tolerance_from_cost)、:700-717(use_precise_interpolation)。beancount/core/interpolate.py:1 模块
docstring;28-30 MAXIMUM_TOLERANCE;33-35
MAX_TOLERANCE_DIGITS;38-43
BalanceError;46-58
is_tolerance_user_specified;61-69
has_nontrivial_balance;72-94
compute_residual(89-91 跳过);97-231
infer_tolerances(105-131 算例;149-150
agg;152-155 选项;157-172 默认容差;174-181
循环入口;184-188 容差;190-197 聚合与注释;199-222 成本与价格;224-231
合并与返回);234-242 meta 键;245-259
get_residual_postings;262-286
fill_residual_posting;289-312
compute_entries_balance;315-361
compute_entry_context;364-394
quantize_with_tolerance(377-380 TODO;381 quantum;383-393
guard)。
beancount/core/interpolate_test.py:24-29、40-56、58-81、83-146、150-177、179-200、202-220、222-284、288-297、299-316、318-326、328-336、338-346、348-356、358-368、370-380、382-404、406-416、418-428、430-440、442-459、461-474、476-489、491-556、558-577、579-588、590-606、610-631。
其它:beancount/parser/booking_full.py:170-179,230,238-240,893-895,944-947,949,964,979,1003,1013-1016;beancount/parser/booking_full_test.py:3481-3520;beancount/ops/validation.py:350-386,390-398;beancount/ops/balance.py:13,20-45;beancount/core/inventory.py:132,152-166,402,459;beancount/core/convert.py:63-106;beancount/utils/defdict.py:14-51;beancount/parser/grammar.py:590-606;beancount/parser/options.py:424-439,483-512,513-543,523-525,544-560,561-578,700-717;beancount/parser/context.py:110,168,174;beancount/plugins/sellgains.py:133;beancount/plugins/check_commodity.py:50;beancount/ops/summarize.py:542;beancount/parser/printer.py:239;beancount/loader.py:605-627,790-794;CHANGES:1836-1851,2398-2399,3229,3233,3237,3252-3257,3269-3272,3746-3748,3755-3758,3766-3771,3836-3838,3845-3917,3880,3883。
commit:ad01c96c(2014-11-03)、51130f08(2014-11-09)、d67cb7e6(2015-04-13)、dbe1fcc1/9c055c66(2015-05-03)、ef6e0a2d(2015-05-16)、ca3d3b28(2015-05-20)、8ca3abe7(2015-05-30)、c8ec7c0b(2015-05-31)、3a767c99(2015-06-06)、cc82c533/bbdb744b/8f14afb6(2016-10-14)、36da2cd3(2016-10-23)、99ee920a(2016-10-30)、dfc75f3b(2016-12-17)、43c5630e/f30c2e46(2017-01-14)、f7facc1f(2017-04-20)、a2d2f6a1(2017-06-25)、2be15243(2018-03-13)、b04cafdd(2018-03-23)、a7c4f14f(2020-07-05)、06127f16(2021-01-30)、bc776058(2024-05-27)、85950542(2024-12-22)、a3a6c644/ae5d5f14(2025-05-23)、ec057e11/8e4bdd99(2026-04-28)、573f04bc(2026-04-29)、5704a861(2026-05-02)。