目录

06 · display_context.py:显示精度的统计与渲染

核对基线 · 范围 · 依赖

核对基线:beancount 仓库 commit 97472138(2026-08-22)。路径相对仓库根目录,引用格式 文件:起-止行 (名称),行号已逐条核对。 本篇范围beancount/core/display_context.py(500 行)、beancount/core/display_context_test.py(418 行)、beancount/core/distribution.py(78 行)、beancount/core/distribution_test.py(57 行)。 上游依赖distribution.py 是本模块唯一的仓库内导入(display_context.py:75);Currency 只在 TYPE_CHECKING 块里从 data.py:30 导入(:77-78);其余是标准库 decimalenumcollectionsio下游使用者DEFAULT_FORMATTERamount.py:21,63position.py:22,155,209inventory.py:52,110to_string 的默认参数;解析器 parser/grammar.py:172-178,206-213,232 采集统计并挂到 options_map["dcontext"]loader.py:550 跨文件合并;parser/printer.py:116-118 构建两份 formatter;scripts/doctor.py:422,542-548 用于对齐输出与诊断。

1. 模块解决什么问题

账本里同一币种的数字小数位数并不一致(10.510.5010.5000),而 Decimal 保留每个数字自己的 exponent,原样输出得到参差不齐的列。解析器在构造 amount 或 compound_amount 时把结果数值传入 DisplayContext.update(),按币种累计小数位数的频率分布与整数部分最大位数;符号空间不依赖样本学习,_ContextBase.__init__ 起无条件预留(第 4.1 节)。随后 build() 按调用方指定的精度策略(众数或最大值)与对齐方式(自然、小数点对齐、右对齐),根据累计状态为每个币种预计算一条 Python 格式串,交给 DisplayFormatter 逐个渲染。模块 docstring display_context.py:1-59 列出五个维度:PRECISION、ALIGNMENT、SIGN、COMMAS、RESERVED。它只影响输出文本,不参与记账计算:渲染是舍入,不是校验(第 6.4 节)。

2. 结构一览

名称 位置 作用
Distribution distribution.py:9-78 整数值直方图:update / update_from / empty / min / max / mode
Precision display_context.py:81-85 MOST_COMMON = 1MAXIMUM = 2
Align :88-93 NATURAL = 1DOT = 2RIGHT = 3
_ContextBase :96-176 单币种统计基类:has_signinteger_maxupdateupdate_from__str__,三个抽象查询方法
_CurrencyContext :179-224 加挂 fractional_dist: Distribution,小数位数从统计里学
_FixedPrecisionContext :227-249 小数位数写死,integer_max 仍学习
DisplayContext :252-458 币种 → 上下文的字典;updateupdate_fromset_fixed_precisionquantizebuild 与三个 _build_*
DisplayContext.DEFAULT_UNINITIALIZED_PRECISION :412 值 8,仅 _build_dot 在没有任何小数位统计时使用
DisplayFormatter :461-495 持有预烤格式串及其 str.format 绑定方法;format__call__)与 quantize
DEFAULT_DISPLAY_CONTEXT / DEFAULT_FORMATTER :499-500 模块导入时用空上下文构建的默认实例

3. Distribution:直方图与众数

def mode(self):
    if not self.hist:
        return None
    max_value = 0
    max_count = 0
    for value, count in sorted(self.hist.items()):
        if count >= max_count:
            max_count = count
            max_value = value
    return max_value

distribution.py:64-78histdefaultdict(int):15),update 对样本计数加一(:25-31),update_from 按值把计数相加(:33-40)。mode 从小到大遍历并用 >= 比较,频率相同时后遇到的较大值覆盖前者,因此平局取较大的小数位数,与插入顺序无关(本机验证 {2:2, 4:2, 3:1} 正序、反序插入都得 4)。min / max 每次调用都对全表 sorted() 再取首尾(:42-62)。distribution_test.py:21-32 锁定 mode=2, min=1, max=4:35-53 锁定合并后众数从 2 变 3。类 docstring 说它用于 "compute a length that will cover at least some decent fraction of the samples"(:10-12)。2015-05-17 commit 9ea24584 把它从 utils.misc_utils 挪到 core.distribution,提交说明的理由是解开 core 对 utils 的依赖。

4. 三层币种上下文

4.1 _ContextBase:符号位硬编码、整数位学习、update 是热点

def __init__(self) -> None:
    # Note: has_sign should always be assumed when formatting numbers; you
    # never know if a new number may require a sign even though one was
    # never witnessed. So we now hardcode to True. (Note to self: remove
    # this later.)
    self.has_sign = True
    self.integer_max = 1

display_context.py:105-111。2025-05-29 commit 10b6f5efhas_sign 初始值从 False 改成 True:余额、插值结果都可能产生输入文件里从未出现过的负数,不预留符号位会让后续渲染的列错位。update:148-163)仍在见到负数时置 True:157-159),已无实际效果;__str__:113-146)输出的示例串因此总带前导 -(测试 display_context_test.py:395-414 的正则含 sign=1"-0.00")。整数位数用 len(digits) + exponent 计算(:162),Decimal("0.00000125") 得 −5,被 max(..., 1) 兜住。:149-151 注释要求 "Please do care for the performance of this routine" 并留下 "Consider reimplementing this in C, after profiling";调用方 grammar.py:473,491 注明 number.as_tuple() 约增加 70ms,CHANGES:4355-4359(2014-11-09)记录初次引入时解析开销约 4%。:153-154None 直接返回,但子类 _CurrencyContext.update:201-205)在 super().update() 之后无条件调用 number.as_tuple()None 在这里抛 AttributeError(本机验证);该守卫只对 _FixedPrecisionContext 有效(测试 :344-348)。三个 get_fractional* 在基类只 raise NotImplementedError:169-176,2025-05-31 commit eaeaa645)。

4.2 _CurrencyContext:小数位数从分布里学

def update(self, number: Decimal) -> None:
    super().update(number)
    # Update the precision.
    num_tuple = number.as_tuple()
    self.fractional_dist.update(-num_tuple.exponent)

def get_fractional(self, precision: Precision) -> int | None:
    if self.fractional_dist.empty():
        return None
    if precision == Precision.MOST_COMMON:
        return self.fractional_dist.mode()
    elif precision == Precision.MAXIMUM:
        return self.fractional_dist.max()
    else:
        raise ValueError("Unknown precision: {}".format(precision))

display_context.py:201-205,212-224。样本是 -exponent,即 Decimal 自身记录的小数位数:5000.0 记 1,764 记 0。空分布返回 None__str__ 用的两个 get_fractional_digits_* 则返回 "_":193-199)。update_from:207-210)把对方 cast_CurrencyContext 再合并直方图,这个 cast 由 2025-08-03 commit df4b77fe(#968)为类型检查加入,运行时不做检查。

4.3 _FixedPrecisionContext:精度写死,宽度照学

display_context.py:227-249。构造时接收 fractional_digits:238-240),三个查询方法都返回它,MOST_COMMONMAXIMUM 无差别(:242-249);update / update_from 继承基类,只更新 integer_max。2025-05-22 commit 9e148b76 引入,一周后 311f4ba3 抽出 _ContextBase,作者在提交说明里称这是 "a bit of ugly concrete inheritance",因为都在一个文件里而接受。测试 display_context_test.py:312-414 五个用例逐项锁定。

5. DisplayContext:累加、量化、构建

5.1 字典结构与 __default__

def __init__(self) -> None:
    self.ccontexts: dict[str, _ContextBase] = collections.defaultdict(_CurrencyContext)
    self.ccontexts["__default__"] = _CurrencyContext()
    self.commas = False

display_context.py:261-264defaultdictupdate:277-284)对新币种自动建上下文;__default__ 是没传币种时的落点,也是 DisplayFormatter.format 对未见币种的回退(:485-490)。set_commas:266-268)只存默认值,build(commas=None) 时取用(:352-353);解析器在 grammar.py:206-208render_commas 选项灌入,注释说这是为了让逗号设定 "propagates everywhere it is used automatically"。update_from:286-293)按币种逐个合并,供 loader.py:550 在 include 多文件时把子文件的统计并入主文件(2021-05-15 commit c46eaacd,作者 Xidorn Quan)。set_fixed_precision:295-296)直接用 _FixedPrecisionContext 替换该币种条目,之前学到的分布与 integer_max 一并丢弃(测试 :114-123;解析路径上的影响见 6.3)。

5.2 quantize:临时抬高 prec 的来龙去脉

qdigit = Decimal(1).scaleb(-num_fractional_digits)

with decimal.localcontext() as ctx:
    # Allow precision for numbers as large as 1 billion in addition to
    # the required number of fractional digits.
    #
    # TODO(blais, 2020-11-25): Review this to assess performance impact,
    # and whether we could fold this outside a calling loop.
    # NOTE(blais, 2025-12-21): Not sure what the context was, but this
    # prevents computation on very large numbers. Consider removing this
    # or rewriting.
    ctx.prec = num_fractional_digits + 12
    return number.quantize(qdigit)

display_context.py:319-331。精度为 None(从未见过该币种)时原数返回(:316-318)。Decimal.quantize 在结果系数长度超过上下文 prec 时抛 InvalidOperation:issue #584 的病态输入让某币种推断出极大的小数位数,随后量化一个普通整数就超出默认的 28 位(本机验证 Decimal("100").quantize(Decimal(1).scaleb(-27)) 在默认上下文报错)。2020-11-25 commit 4c1e87bb 引入 localcontext 并设 prec = 小数位 + 9,提交说明写明 "allowing for numbers as large as 1B" 并怀疑会拖慢渲染;2025-12-21 commit c9132bba 改成 + 12 并加上 NOTE,作者自述已不记得原始上下文。ctx.prec 限的是量化结果的总有效数字数,不是直接检查输入整数部分位数:当前 + 12 设置最多容纳量化结果 12 位整数部分,999999999999.99(12 位整数)可量化;但 999999999999.999 量化到两位小数时因四舍五入进位到 13 位整数,同样抛 InvalidOperation(本机验证)——超限不只发生在原始位数已经过长的输入上。模块不设置舍入模式,formatquantize 都沿用调用时的 decimal 上下文:Python 默认上下文是 ROUND_HALF_EVENquantize(2.345) → 2.34quantize(2.355) → 2.36),仓库非测试代码没有任何 ROUND_ 设置,但调用方可以改变这一行为(本机验证:ROUND_UP 上下文下 Decimal("2.341").2f 格式化与 quantize 都得 2.35ROUND_DOWN 上下文下都得 2.34)。assert isinstance(number, Decimal):313)在 python -O 下失效。

5.3 build 与三种对齐

build:333-364)按 alignment 分派到三个私有方法,各自返回 {币种: 格式串},再封装成 DisplayFormatter(self, precision, fmtstrings):364)。每次调用都重新计算,没有缓存(TODO:553-554 列为待办)。

NATURAL:366-377)不看宽度,reserved 参数名为 unused_reserved

fmtfmt = (
    "{{:{comma}f}}" if num_fractional_digits is None else "{{:{comma}.{frac}f}}"
)

:371-373。有统计时是 {:.2f},无统计时是 {:f}f 后缀由 2017-07-23 commit 1fa59c71(#179)加上:Decimal 走不带类型码的 format 会在指数大时输出科学计数法(本机验证 "{:}".format(Decimal("1E+10"))1E+10,加 f10000000000)。

RIGHT:379-410)先对所有币种算宽度上界:符号 1 位、integer_max、逗号位 int(integer_max / 3)、小数点 1 位(小数位非零时)、小数位数,取最大再加 reserved:381-395);然后每个币种用同一 width 生成 {:11.2f} 这类格式串(:398-409)。无统计的币种退回 {:{width}{comma}},没有 f:402-405),科学计数法在这条路径上仍可能出现(本机验证 "{:2}".format(Decimal("1E+10"))1E+10)。逗号位估算 int(n/3) 是上界:3 位整数不需要逗号却算 1。

DOT:414-458)分别求最大符号位、最大整数位(含逗号估算)、是否有小数点、最大小数位(:416-433);无任何小数位统计时把最大小数位设为 8(:412,435-436),所以未初始化的上下文在 DOT 下补到 8 位小数,与 NATURAL / RIGHT 的原样输出不同(测试 :225-230 对照 :129-134,170-175)。符号位既计入总宽(:421-422 在任一币种 has_sign 时置 max_sign = 1:438 把它并入 max_width),也在格式串里用空格符号标志体现(:442,451),使正数占住那一位。每个币种的格式串宽度为 max_width - len_padding,右侧再拼 len_padding 个空格(:446-457),整数币种在有小数点的列里额外多补一位(:449-450),使小数点纵向对齐。测试 :238-254 的五币种样例本机复现为 {: 10.4f} 加 4 空格(USD)、{: 5.0f} 加 9 空格(CAD)、{: 14.8f}(RBFF)。

6. DisplayFormatter、默认实例与采集/消费链路

6.1 DisplayFormatter

self.fmtfuncs = {currency: fmtstr.format for currency, fmtstr in fmtstrings.items()}

def format(self, number: Decimal, currency: Currency = "__default__") -> str:
    try:
        func = self.fmtfuncs[currency]
    except KeyError:
        func = self.fmtfuncs["__default__"]
    return func(number)

display_context.py:480,485-490。构造时把每条格式串的 .format 绑定方法取出(:480),format 只剩一次字典查找加一次调用;__call__ = format:495)。quantize 转发给 dcontext.quantize 并带上自身构建时的 precision:492-493,2015-09-12 commit e2720590)。这个 quantize 接口在仓库非测试代码里没有调用点;插值走的是 interpolate.py 里由容差推出的 quantum。

6.2 默认实例

display_context.py:498-500DEFAULT_DISPLAY_CONTEXT = DisplayContext()DEFAULT_FORMATTER = DEFAULT_DISPLAY_CONTEXT.build()。空上下文构建的格式串只有 {'__default__': '{:f}'},效果是保留 Decimal 自身的小数位、不加逗号;amount.py:63position.py:155,209inventory.py:110 用它作默认参数,printer.py:116 在没传 dcontext 时用 DEFAULT_DISPLAY_CONTEXTbuild

6.3 采集:解析器与选项

def _dcupdate(self, number, currency):
    """Update the display context."""
    if isinstance(number, Decimal) and currency and currency is not MISSING:
        self.display_context_update(number, currency)

grammar.py:175-178Builder.__init__ 建一个 DisplayContext 并缓存 update 绑定方法(:172-173);仓库里只有 amount:461-475)和 compound_amount:477-497)调用 _dcupdate,且要求结果是 Decimal、币种非空且不是 MISSING。语法层 grammar.y 中 posting 单位与 @ 价格走 incomplete_amount:420-421,670-675),balanceprice 指令走 amount:607-619,722),成本走 compound_amount:652-668),全部汇入统计;balance~ 容差数字只作为第二个值传给 Builder.balance,不进统计(grammar.y:621-634)。元数据 key_value_value:465-475)与 custom 指令 custom_value:758-782)都允许裸 number_expr,这类数字不经 amount/compound_amount,不进入统计。number_expr 的四则运算在 C 里用 PyNumber_*Decimal 直接求值(:316-349);在默认 decimal 上下文(prec=28)下,作为有币种 amount 使用的 10/3 会产生 27 位小数样本,具体位数取决于调用时的 decimal 上下文精度(本机验证:localcontext().prec=6 时只得 5 位小数)。finalizegrammar.py:180-215)在解析结束时灌入两个选项:render_commas:208)与 display_precision:211-213,取示例数字的 -exponentset_fixed_precision);get_options 把上下文挂到 options["dcontext"]:225-234)。options.py:449-482display_precision 文档说统计推断 "led to a lot of confusion",因此开放显式指定(:459-466),并指出宽度仍从输入学习(:468-469);但 set_fixed_precision:295-296)用全新 _FixedPrecisionContext 替换整个币种条目,integer_max 随之回到 1(:110-111),而 finalize 是在 parser.parse() 跑完整个文件之后才被调用(parser.py:226-227),此时该币种已经学到的宽度被丢弃,之后也不再有数字流入重新学习,options.py:468-469 的说法在这条解析路径上不成立。值经 options_validate_tolerance_map:73-89)转成 (currency, D(str))options.py:258-265 声明 dcontext 选项,默认值是一个新 DisplayContext():607-613 声明 render_commas,默认 False

6.4 消费:printer、doctor

self.dformat = self.dcontext.build(precision=display_context.Precision.MOST_COMMON)
self.dformat_max = self.dcontext.build(precision=display_context.Precision.MAXIMUM)

printer.py:117-118。两份 formatter 的分工:posting 的 units 与 cost 经 position.to_string(posting, self.dformat) 用众数(:288position.py:155-175,69-104),Balance 金额与 ~ 容差用众数(:319,327),只有 posting 的 @ 价格(:299)与 price 指令(:396)用 dformat_max。渲染即舍入:{:.2f} 把三位小数的 posting 静默舍到两位,DisplayFormatter.format 不做任何精度丢失检查(display_context.py:485-490);Balance 的隐式容差由金额 exponent 推出(ops/balance.py:34-41),打印成众数位数后重新解析,容差随之改变。TODO:4843-4845 的 issue #107 "Review all codes that renders units, costs and prices for precision" 仍在待办,TODO:567-568 另有 "issue a warning if numbers are rendered through it that lose some precision",TODO:503-507 记录 formatter 只有单一精度、而 to_string 同时打印 units / cost / price 的矛盾。cost_to_strCostSpec 的数字调用 dformat.format(number) 不传币种(position.py:96,99,102),落到 __default__ 上下文。doctor.py:542-548display_context 命令直接 str(dcontext) 打印每币种一行统计;:422render_mini_balances(被 linkedregion 调用,:349,402)用 build(alignment=Align.DOT, reserved=2),是非测试代码里唯一同时使用 DOT 对齐与 reserved 的调用点;core/realization_test.py:702-703test_dump_balances 也用同一组合覆盖 dump_balances

7. 设计决策与理由

决策 理由 证据
精度按币种从输入统计,默认取众数 同一币种绝大多数数字位数一致;少数高精度数字不应拉长整列 display_context.py:16-20CHANGES:4324-4331
另提供 MAXIMUM 供价格使用 价格与汇率需要保留更多位 :20printer.py:118,299,396TODO:490-491
统计与渲染分离:DisplayContext 是 builder,DisplayFormatter 是产物 格式串一次烤好、到处传递,渲染只剩字典查找 :364,480CHANGES:4187-4191(2014-12-26)
has_sign 硬编码 True 新数字可能带符号;未预留会错位 :106-110;commit 10b6f5ef
格式串带 f 禁止科学计数法出现在账本输出 :372;commit 1fa59c71(#179)
quantizelocalcontextprec 极端精度下量化普通整数会超出默认 28 位 :321-331;commit 4c1e87bbc9132bba
提供固定精度上下文与 display_precision 选项 统计推断让用户困惑,需要显式覆盖 options.py:459-466;commit 9e148b76
render_commas 织进 DisplayContext 让逗号设定随上下文自动传播到所有渲染点 grammar.py:206-208CHANGES:2381-2393(#106)
空上下文作为默认 formatter 没有统计时原样输出,不引入舍入 :498-500
update_from 跨文件合并 include 的子文件各有自己的 Builder 与统计 loader.py:550;commit c46eaacd

8. 行为细节与边界

现象 后果 证据
众数平局取较大值 四个位数各异的样本 → 8 位;test_natural_no_clear_mode 正是这种情况 distribution.py:74-75display_context_test.py:136-140
舍入模式随调用时的 decimal 上下文 模块不设置舍入模式;Python 默认上下文是 ROUND_HALF_EVEN2.345 → 2.34),调用方改上下文后 format/quantize 结果一起变 仓库无 ROUND_ 设置;:321-331,371-376;本机验证
渲染即舍入,不校验 高于众数精度的 posting 被静默截短;只有 @ 价格与 price 指令用最大精度 printer.py:288,299,319,327,396:485-490
quantizeprec 限的是结果总位数 当前设置容纳 12 位整数部分的量化结果;舍入进位到第 13 位同样抛 InvalidOperation,不限于原始位数超长的输入 :319-331;本机验证
integer_max 只取单个数字的最大整数位 不是多个数累加后余额的宽度,RIGHT/DOT 可能预留不足;reserved 只是调用方手动补偿 TODO:539-544:161-163,379-395,414-438
DisplayFormatter.format 是快照,.quantize 是实时 build() 之后再 update 同一个 DisplayContextformat 仍用旧格式串,quantize 却读到新精度 :362-364,474-480,485-493
_CurrencyContext.update(None)AttributeError 基类的 None 守卫对学习型上下文无效;_dcupdate 已先过滤非 Decimal :153-154,201-205grammar.py:177
quantize 对未见币种有副作用 defaultdict 为该币种插入空上下文,之后 build 的格式串里多一个键 :262,314;本机验证
学习型上下文 update_from 固定型抛 AttributeError 子文件对某币种设了 display_precision 而主文件没设时 loader.py:550 合并失败;反向合并正常 :209-210;本机验证
RIGHT 未初始化格式串无 f Decimal("1E+10") 渲染成 1E+10;#179 只修了 NATURAL :402-405;本机验证
DOT 未初始化补 8 位小数 与 NATURAL / RIGHT 的原样输出不一致 :412,435-436;测试 :225-230
正指数样本产生负小数位 Decimal("1E+3") 入统计后格式串为 {:.-3f},渲染抛 ValueError;词法规则 lexer.l:282-283 不接受指数写法,解析路径不会触发 :205,372;本机验证
语法层除法结果直接入统计 默认 decimal 上下文下 10/3 贡献 27 位小数样本,MAXIMUM 被拉到 27;位数随调用时上下文精度变化(第 6.3 节) grammar.y:333-337;本机验证
reserved 在 NATURAL 下被忽略 只有 RIGHT / DOT 加宽 :366,395,438;测试 :159-164
逗号位估算 int(n/3) 上界而非精确值,n 为 3 的倍数时多一位 :388,426
set_fixed_precision 丢弃已学分布 之后 MOST_COMMON / MAXIMUM 无区别 :295-296;测试 :87-95,114-123
build 无缓存 每次构建都遍历全部币种 :333-364TODO:553-554
【文档漂移】docstring 说无负号时 "we save the space" has_sign 恒为 True,空间总是预留 :48-49,100-101,110
【文档漂移】_FixedPrecisionContext docstring "Sign ... still learned" 符号位不再学习 :228
【文档漂移】DisplayContext docstring "construct a DisplayContext from a series of numbers" 构建的是 DisplayFormatter :253
【文档漂移】测试注释 "most common is 0"、"has_sign=False" EUR 样本位数 4 / 0 / 8 平局取 8;has_sign 恒真 display_context_test.py:111,352
【文档漂移】TODO 仍列 "Implement reserved number of digits"、"Add display_precision input file option" 前者自 2016-03-12 commit 47fd0b71 起可用,后者 2025-05-22 已实现 TODO:556,570-571

9. 测试锁定了什么

display_context_test.py 共 28 个测试、6 个类;辅助方法 assertFormatNumbers:27-48)先 updatebuild 再逐个 formatnoinit=True 跳过 updatedistribution_test.py 2 个测试。

测试 行号 锁定的行为
test_dump 52-57 str(dcontext)sign=
test_set_fixed_precision 59-123 固定精度下 MOST_COMMONMAXIMUM 同结果;quantize(99.999) → 100.00;替换后类型变为 _FixedPrecisionContext
test_natural_* 129-164 未初始化原样输出;平局取 8 位;众数 2 位、最大 4 位;逗号;reserved 无效
test_right_* 170-219 有无负数宽度相同;整数列宽;逗号加宽;小数补零与舍入 0.0002 → 0.00
test_dot_* 225-298 未初始化补 8 位;多币种小数点对齐与右侧补空格;整数币种在有小数列里多补一位
test_quantize_basic 302-309 量化位数随后续 update 从 2 变 4
TestFixedContext 313-414 has_sign 恒真;integer_max 仍学习;update(None) 安全;update_from 不改精度;__str__ 格式
test_distribution / test_update_from distribution_test.py:22-32,36-53 mode / min / max / empty;合并后众数变化

未覆盖:get_fractional 收到非法 Precision 时的三种分支——非空 _CurrencyContextValueError、空 _CurrencyContext 在检查枚举值之前就已返回 None_FixedPrecisionContext 完全忽略 precision 返回固定值(:212-224,248-249);build 收到非法 AlignValueError:361);quantize 对未知币种返回原数(:316-318)与 12 位整数上限;_CurrencyContext.update(None);学习型上下文合并固定型的方向(_CurrencyContext.update_from(_FixedPrecisionContext),即第 8 节中会抛 AttributeError 的那条;反方向已由 display_context_test.py:350-374,388-393 覆盖);DisplayContext.update_from 本身(只测过币种上下文层);DisplayFormatter.quantize__call__set_commasbuild(commas=None) 的默认传播;RIGHT 未初始化路径的科学计数法;__default__ 回退。

本模块之外的集成测试:parser/options_test.py:166-234 覆盖 display_precision 的解析、生效与非法值;core/realization_test.py:685-715 覆盖 DOT 对齐加 reserved=2parser/printer_test.py:711-730 是禁止科学计数法的回归测试;scripts/doctor_test.py:146-165 覆盖 display-context 命令。

10. 演变史

日期 提交 / 记录 变化
2014-11-09 CHANGES:4355-4359 解析器开始记录数字精度分布,开销约 4%
2014-11-11 7ac0cddbCHANGES:4324-4335 创建模块;Amount.str(int) 改为 to_string(DisplayContext);逗号默认关闭,新增 render_commas
2014-11-15 / 16 1237033be76f840d9fa0f875 builder 模式;reserved 暂以 NotImplementedError 禁用
2014-11-22 6c32a8b3 NumFormatter 更名 DisplayFormatter,变量 numfmt → dformat
2014-12-28 2afb4f9c DisplayContext.quantize
2015-05-17 9ea24584 Distributionutils.misc_utils 移入 core.distribution
2015-07-12 / 20 CHANGES:3504-3510,3487-3489 web / report 与 bean-query 改用 DisplayContext 渲染
2015-09-12 e2720590 DisplayFormatter.quantize
2016-03-12 d29e74a3(#106)、47fd0b71 render_commas 真正生效;reserved 恢复可用
2017-07-23 1fa59c71(#179) NATURAL 格式串加 f
2017-12-09 97cabe0aCHANGES:1236 price 指令按显示精度渲染(改动在 prices/price.py
2020-11-25 4c1e87bb(#584) quantizelocalcontextprec = 小数位 + 9
2021-05-15 c46eaacd 三层 update_from,供 include 合并
2025-05-22 9e148b76 _FixedPrecisionContextdisplay_precision 选项
2025-05-29 311f4ba310b6f5ef 抽出 _ContextBasehas_sign 硬编码 True
2025-08-03 df4b77fe(#968) ccontexts 注解为 dict[str, _ContextBase]update_fromcast
2025-12-21 c9132bba prec 改为 + 12,加 NOTE

11. 与其他模块的关系

12. 参考索引

beancount/core/display_context.py:1-59 模块 docstring(16-20 精度、24-46 对齐示例、48-49 SIGN、51-52 COMMAS、54-57 RESERVED);75 导入 distribution;77-78 TYPE_CHECKING;81-85 Precision;88-93 Align;96-176 _ContextBase(105-111 __init__、113-146 __str__、148-163 update、165-167 update_from、169-176 抽象方法);179-224 _CurrencyContext(189-191、193-199、201-205、207-210、212-224);227-249 _FixedPrecisionContext;252-458 DisplayContext(261-264、266-268、270-275、277-284、286-293、295-296、298-331 quantize、333-364 build、366-377 _build_natural、379-410 _build_right、412、414-458 _build_dot);461-495 DisplayFormatter(474-480、485-490、492-493、495);498-500 默认实例。

beancount/core/distribution.py:9-12 类 docstring;14-15 __init__;17-23 empty;25-31 update;33-40 update_from;42-51 min;53-62 max;64-78 mode

测试display_context_test.py 14-21、24-48、52-57、59-123、129-164、170-219、225-298、302-309、313-414;distribution_test.py 13-18、22-32、36-53。

其它beancount/parser/grammar.py:21,172-178,180-215,225-234,461-475,477-497beancount/parser/grammar.y:316-349,420-421,465-475,607-634,652-675,722,758-782beancount/parser/lexer.l:282-283beancount/parser/parser.py:224-227beancount/parser/options.py:73-89,258-265,449-482,607-613beancount/loader.py:532-560beancount/parser/printer.py:116-118,288,299,319,327,396beancount/core/position.py:69-104,155-175,209beancount/core/inventory.py:52,110-120beancount/core/amount.py:21,63beancount/core/data.py:30beancount/ops/balance.py:29-45beancount/scripts/doctor.py:28,256,349,391,402,405,422,542-548CHANGES:83-85,1236,1290,2368-2371,2381-2393,3487-3489,3504-3510,4187-4191,4324-4335,4355-4359TODO:486-499,503-507,527-534,539-544,553-554,556,567-571,4843-4845

集成测试parser/options_test.py:166-234core/realization_test.py:685-715,702-703parser/printer_test.py:711-730scripts/doctor_test.py:146-165

commit7ac0cddb(2014-11-11)、1237033b(2014-11-15)、e76f840d(2014-11-16)、9fa0f875(2014-11-16)、6c32a8b3(2014-11-22)、2afb4f9c(2014-12-28)、9ea24584(2015-05-17)、e2720590(2015-09-12)、d29e74a3(2016-03-12)、47fd0b71(2016-03-12)、1fa59c71(2017-07-23)、97cabe0a(2017-12-09)、4c1e87bb(2020-11-25)、c46eaacd(2021-05-15)、9e148b76(2025-05-22)、311f4ba3(2025-05-29)、10b6f5ef(2025-05-29)、eaeaa645(2025-05-31)、df4b77fe(2025-08-03)、c9132bba(2025-12-21)。