A Beancount v3 compatible plain-text double-entry accounting engine in MoonBit: parser, booking, validation, reports and BQL queries, embeddable in native, wasm and js.
| 示例 | 行为、位置与原因 |
|---|---|
| plugin.beancount | 第 1 行 validation.plugin 警告;插件只记录、不执行;第 2 行交易的两个未声明账户各报 validation.unknown_account,check 退出 1。依赖自动开户、自动生成条目等插件效果的账本须先显式写出这些数据,否则还会有账户/余额错误。 |
| document.beancount | 第 2 行 validation.document_unchecked 警告;保留附件路径并校验账户,但不检查文件是否存在、内容和访问权限。缺失收据不导致 check 失败;这与 Beancount 文件检查有差异。 |
| include.beancount | 第 1 行正常加载同目录的 document.beancount(附件警告指向 document.beancount:2);第 2 行找不到文件,include.missing,退出 1。native CLI 解析相对路径;库须用 check_expanded 提供读取回调,浏览器不会自动读取磁盘。 |
| cycle.beancount | 第 1 行 include.cycle,退出 1;规范化后的循环路径不会重复加载。 |
| queries.beancount | 第 6–8 行分别保存子查询、JOIN、PIVOT。query 指令作为命名文本保留,check 不执行它;显式运行下列查询时均退出 1。 |
moonbean query examples/migration/queries.beancount 'SELECT account FROM (SELECT account)'
# unsupported query.unsupported.subquery at BQL:1:22
moonbean query examples/migration/queries.beancount 'SELECT account FROM postings JOIN postings'
# unsupported query.unsupported.join at BQL:1:30
moonbean query examples/migration/queries.beancount 'SELECT account PIVOT currency'
# unsupported query.unsupported.pivot at BQL:1:16./scripts/build-web.sh # 产物在 web/dist
python3 -m http.server -d web/dist 8000
node web/smoke.mjs # 用示例账本断言导出函数的结果
node web/dom-smoke.mjs # 无头 Chrome:账本里的恶意字符串不会变成 HTML| 函数 | 请求字段 | 返回 |
|---|---|---|
| check | source | 是否通过、带行号的错误和警告、账户列表、operating_currency |
| balance_sheet / income_statement / trial_balance | source、convert?、at?(损益表另有 start?) | 报表行与合计,以及 complete、diagnostics(折算不完整时给出账户、币种和目标币种)和 display_precision;资产负债表另有 balanced。不传 convert 时按唯一的 operating_currency 折算,传 "" 保留原币 |
| register | source、account、subtree?、at? | 流水、累计余额和 lot 成本 |
| query | source、bql、at? | 列名与结果行 |
| format | source | 对齐后的文本和是否有变化 |
| 面 | 对应 | 说明 |
|---|---|---|
| 发布源码 | tag v0.1.1 | mooncakes 0.1.1 的打包内容与该 tag 指向的提交一致 |
| mooncakes | sundaysebasidian-byte/moonbean@0.1.1 | 包内容固定,不随 main 变化 |
| Pages | 最近一次推送到 main 的构建 | pages.yml 每次 push 到 main 都会重新构建,可能领先于 0.1.1 |
# 源码测试
git checkout v0.1.1
moon test --target all
# 安装包调用:在仓库外新建模块,复制 examples/library_demo 的 main.mbt 和 moon.pkg 到 cmd/main
moon new consumer && cd consumer
moon add sundaysebasidian-byte/moonbean@0.1.1
moon run cmd/main
# 线上引擎:打开 Pages;对应的构建见 Actions 中 Pages 工作流的运行记录
gh run list --workflow pages.yml --limit 1moon add sundaysebasidian-byte/moonbeanmoon version --all
moon build# 校验账本,错误输出为 file:line: code: message
moon run cmd/main --target native -- check examples/full-year-2024.beancount
# 试算平衡表,可以指定日期快照和折算币种
moon run cmd/main --target native -- bal examples/full-year-2024.beancount
moon run cmd/main --target native -- bal examples/full-year-2024.beancount --at 2024-06-30 --convert CNY
# 资产负债表、损益表、某个账户的流水、重复交易
moon run cmd/main --target native -- bs examples/full-year-2024.beancount
moon run cmd/main --target native -- is examples/full-year-2024.beancount
moon run cmd/main --target native -- register examples/full-year-2024.beancount Assets:Bank:Checking
moon run cmd/main --target native -- dups examples/full-year-2024.beancount
# BQL 查询,输出文本或 JSON
moon run cmd/main --target native -- query examples/full-year-2024.beancount \
'SELECT account, sum(units) FROM postings GROUP BY account ORDER BY account'
moon run cmd/main --target native -- query examples/full-year-2024.beancount \
'SELECT account, sum(units) FROM postings GROUP BY account ORDER BY account' --json
# 格式化(默认打印到标准输出,加 --write 原地写回)
moon run cmd/main --target native -- fmt examples/full-year-2024.beancount
# 导出结构化 JSON
moon run cmd/main --target native -- export examples/full-year-2024.beancount --jsonmoon run examples/library_demoimport {
"sundaysebasidian-byte/moonbean",
}fn analyze(source : String) -> Unit {
// 纯函数,不访问文件系统;include 由宿主提供 read 回调。
let ledger = @moonbean.check_source("ledger.beancount", source)
if ledger.has_errors() {
for diagnostic in ledger.errors {
println(@moonbean.diagnostic_text(diagnostic))
}
return
}
let realized = @moonbean.realize(ledger)
for position in realized.inventory("Assets:Cash").positions {
println(position.to_string())
}
let result = @moonbean.run_query(
ledger,
"SELECT account, sum(units) FROM postings GROUP BY account ORDER BY account",
)
println(result.to_text())
}| API | 说明 |
|---|---|
| check_source(file, source) | 解析并校验,返回 ValidatedLedger |
| check_expanded(file, base_dir, source, read) | 先展开 include 再校验 |
| expand_includes(source, base_dir, read) | 返回展开后的源码、缺失路径和带位置的 include 诊断 |
| realize(ledger, at~) | 生成 inventory、价格库和账簿快照 |
| run_query(ledger, bql, at~) | 执行 BQL 子集 |
| format_source(source) | 对齐金额列 |
| trial_balance、balance_sheet、income_statement、inventory_rows、register_rows | 报表,输出文本或 JSON |
| @validation.find_duplicate_transactions(entries) | 查找重复交易 |
| 能力 | 状态 | 边界 |
|---|---|---|
| option | 部分 | 解析并保留。title、booking_method、tolerance_multiplier、inferred_tolerance_default、infer_tolerance_from_cost 会影响语义;account_rounding、render_commas 只解析。只有一个 operating_currency 时作为 bal/bs/is 的默认折算币种。报表文本的小数位由 display_precision 指定,没有指定时按每个币种最常见的账面小数位推断(并列取较大值);JSON 保留原精度 |
| plugin | 部分 | 记录名称和配置,并在指令所在行给出 validation.plugin 警告,不执行插件 |
| include | 支持 | 库和命令行共用同一个展开器:循环 include 报 include.cycle,缺失的文件报 include.missing 并保留原行,最大嵌套 1024 层 |
| pushtag / poptag / pushmeta / popmeta | 支持 | 作用域内的交易继承标签和元数据 |
| open | 支持 | 多币种约束、booking method、重复 open 检查 |
| close | 支持 | 关闭后的交易检查、未开户就关闭、重复 close |
| commodity | 支持 | 解析并记录 |
| balance | 支持 | 支持 ~ 显式容差,否则按末位推断;同日断言先于同日交易 |
| pad | 支持 | 自动补记;没有被用到的 pad 会报错 |
| transaction | 支持 | flag、payee/narration、标签、链接、交易和 posting 元数据、posting flag |
| posting 金额表达式 | 支持 | + - * /、括号、一元正负号 |
| cost {} / {{}} | 支持 | 单价、总价、日期、label;FIFO/LIFO/HIFO/STRICT/STRICT_WITH_SIZE/NONE/AVERAGE |
| {*} 合并成本 | 支持 | MoonBean 扩展,beancount 3.2.3 不支持 |
| price @ / @@ | 支持 | 单价和总价 |
| 省略金额 | 支持 | 每种币种最多一个自动 posting;无法确定时按 bean-check 的类别报错 |
| price / note / event / query / custom / document | 部分 | 解析、校验账户并保留;document 不检查文件是否存在,query 不会自动执行 |
| 能力 | 状态 | 边界 |
|---|---|---|
| 精确十进制 | 支持 | 自己实现的 decimal,除法和舍入与 Python decimal 对齐 |
| 容差推断 | 支持 | 交易容差取最不精确金额的末位乘以 multiplier,再应用 inferred_tolerance_default;balance 断言按断言金额的末位推断 |
| booking 方法 | 支持 | STRICT、STRICT_WITH_SIZE、FIFO、LIFO、HIFO、AVERAGE、NONE |
| lot 匹配与错误 | 支持 | 歧义、库存不足、无匹配都带 booking.* 错误和位置 |
| 跨 lot 减记 | 支持 | 一次卖出跨多个 lot 时拆成多条 posting,各带自己的成本、日期和 label;BQL、JOURNAL、register 和 JSON 导出一致 |
| 交易平衡 | 支持 | 包含 cost、price 和成本分摊的权重 |
| 账户生命周期 | 支持 | 未开户、已关闭、重复 open/close |
| 币种约束 | 支持 | 按 open 声明的币种集合检查 |
| balance 断言 / pad | 支持 | 见上表 |
| 重复检测 | 部分 | open/close/balance 在校验时检查;重复交易用 dups 命令或 find_duplicate_transactions 按需检查 |
| 能力 | 状态 | 边界 |
|---|---|---|
| inventory / 价格库 / 市值折算 | 支持 | 按截止日取价,支持逆汇率和多跳折算(确定性路径,带环检测);找不到路径时报表标记 complete: false,返回 report.conversion_incomplete,合计按币种分开 |
| 试算平衡 / 资产负债表 / 损益表 / register | 支持 | 资产负债表把当期和往期收益结转到 Equity:Earnings:*,恒等式按币种精确成立;不折算时按成本,指定折算币种时按市值,市值相对成本的变化列在 Equity:Earnings:Unrealized。命令行有 bal、bs、is、register FILE ACCOUNT(可加 --json)和 dups FILE |
| SELECT ... FROM postings/balances | 支持 | 列:date、year、month、flag、payee、narration、account、root、parent、leaf、units、currency、cost、value、position |
| WHERE | 部分 | 比较、is [not] null、and/or/not,支持 ~ 正则字面量;没有 LIKE 和子查询 |
| HAVING / 数值算术 | 支持 | HAVING 支持聚合条件;Decimal 支持 + - * / 和括号,不含 Amount/Inventory 运算 |
| tags / links / 元数据 | 支持 | tags、links 在 JSON 里是数组;meta('key') 读取 posting 元数据 |
| GROUP BY / ORDER BY / LIMIT | 支持 | 多键分组和排序,ASC / DESC |
| 函数 | 部分 | year、month、root、parent、leaf、sum、count;没有其他聚合、窗口和字符串函数 |
| BALANCES / JOURNAL | 支持 | BALANCES AT units|cost FROM OPEN ON date CLOSE ON date;JOURNAL <account> [LIMIT n];时间点由 run_query 的 at~ 或命令行的 --at 控制 |
| 不支持的 BQL | 不支持 | 子查询、JOIN、PIVOT、SQL LIKE;FROM transactions 解析时就报不支持(没有按交易的行源);BQL 的 CLOSE ON 不合成币种转换条目,关闭和结转由报表层处理 |
| 能力 | 状态 | 边界 |
|---|---|---|
| moonbean fmt | 支持 | 对齐 posting 金额列;--write 原地写回;roundtrip 测试保证解析结果不变 |
| check / bal / query / fmt / export --json | 支持 | 退出码 0 表示成功,1 表示失败 |
| js / wasm-gc 库 | 支持 | 根包是纯函数;命令行只支持 native |
ledgers: 58, compared: 56, passed: 56, failed: 0, extensions: 2./scripts/oracle.sh # 安装固定版本的 oracle 并运行比较
./scripts/oracle.sh --markdown # 重新生成逐份结果表| 结果 | 数量 | 说明 |
|---|---|---|
| 与 beancount 完全一致 | 858 | 其中 468 份无错误,比较余额、固定 BQL、posting 行数和成本的金额、日期、label、资产负债表、损益表、报表恒等式和价格路径;390 份是故意写错的账本,只比较诊断 |
| 使用 AVERAGE booking | 142 | beancount 3.2.3 报 average_unsupported,MoonBean 实现了 AVERAGE |
| 其他差异 | 0 |
python3 scripts/random_differential.py --seed 7 --out .oracle-cache/differential/seed-7 --allow-known-extensions
python3 scripts/random_differential.py --seed 20261101 --out .oracle-cache/differential/seed-20261101 --allow-known-extensions
python3 scripts/report_differential.py
python3 scripts/include_smoke.py
python3 scripts/bql_differential.py
python3 scripts/cli_smoke.py| 操作 | 耗时 |
|---|---|
| check(解析 + 校验 + booking) | 0.564 s |
| bal(realization + 试算平衡) | 0.603 s |
| BQL 聚合查询 | 0.665 s |
| fmt | 0.235 s |
python3 scripts/performance.py --compare-decimal --samplemoon check --deny-warn --target all
moon test --target all
moon fmt --check
moon info && git diff --exit-code
./scripts/build-web.sh && node web/smoke.mjs && node web/dom-smoke.mjspub(all) struct ExpandedSource {
source : String
source_map : Array[Loc]
missing : Array[String]
errors : Array[Diagnostic]
}pub struct IncludeExpander {
frames : Array[IncludeFrame]
output : Array[String]
source_map : Array[Loc]
missing : Array[String]
errors : Array[Diagnostic]
pending : (String, String, Loc)?
}fn check_expanded(file : String, base_dir : String, source : String, read : (String) -> String?) -> ValidatedLedgerfn expand_includes(source : String, base_dir : String, read : (String) -> String?, file? : String) -> ExpandedSourcefn format_source(source : String) -> Stringfn include_target(line : String) -> String?fn path_dirname(path : String) -> Stringfn path_join(base : String, relative : String) -> StringInstall
Download zipA Beancount v3 compatible plain-text double-entry accounting engine in MoonBit: parser, booking, validation, reports and BQL queries, embeddable in native, wasm and js.