项目开源地址为 https://github.com/hamsterbase/ledger-ts/blob/main/src/example/beancount.ts
使用之前推荐先学习一下 beancount 语法。
在日常使用 beancount 记账时,我遇到了一些困扰。这些问题激发了我自行创造 DSL 的想法。简而言之,我的目标是开发一种更高级的记账语言,而将 beancount 当作底层的“汇编语言”。
我选择基于 TypeScript 开发,通过一些工具函数来实现账单的记录。在 typescript 代码里定义所有的货币、账户和交易记录。借助这种方法,无需自己开发编译器。 可以充分利用 Visual Studio Code 强大的代码补全功能,而且还可以在 TypeScript 的类型系统中进行进阶操作和验证。
此外,对于一些常见的账目模式,可以开发工具函数来进一步简化和自动化账单的处理
这个是原始的 beancount 账单
1970-10-01 commodity USD 1970-10-01 commodity CNY 1970-01-01 open Assets:CN:Cash CNY 1970-01-01 open Assets:Cash USD 1970-01-01 open Assets:UTrade:Account:AAPL USD 1970-01-01 open Assets:UTrade:Account:EWJ USD 1970-01-01 open Expenses:Food:Groceries USD 1970-01-01 open Expenses:Food:Alcool USD 1970-01-01 * "Distribution of cash expenses" Assets:Cash -300 USD Expenses:Food:Alcool 300 USD 1970-01-01 * "CN to usd" Assets:CN:Cash -700 CNY @@ 100 USD Assets:Cash 100 USD
这个是我发明的 DSL
import { EAccountType, Ledger, utils } from "../index.js"; // 声明货币 const { USD, CNY } = utils.createCurrencies({ defaultDate: "1970-10-01" }, [ "USD", "CNY", ] as const); // 声明 Assets 账户 const Assets = utils.buildAccountHierarchy(USD, EAccountType.Assets, { CN: { Cash: utils.createAccountNodeConfig({ open: "1970-01-01", currency: CNY }), }, Cash: utils.createAccountNodeConfig({ open: "1970-01-01" }), UTrade: { Account: { AAPL: utils.createAccountNodeConfig({ open: "1970-01-01" }), EWJ: utils.createAccountNodeConfig({ open: "1970-01-01" }), }, }, }); // 声明消费 Expenses 账户 const Expenses = utils.buildAccountHierarchy(USD, EAccountType.Expenses, { Food: { Groceries: utils.createAccountNodeConfig({ open: "1970-01-01" }), Alcool: utils.createAccountNodeConfig({ open: "1970-01-01" }), }, }); const ledger = new Ledger( [ ...utils.flattenAccountHierarchy(Assets), ...utils.flattenAccountHierarchy(Expenses), ], [USD, CNY] ); const { tr } = utils.transactionBuilder(ledger); // 记录账单 tr( "1970-01-01", "Distribution of cash expenses", Assets.Cash.posting(-300), Expenses.Food.Alcool.posting(300) ); tr( "1970-01-01", "CN to usd", Assets.CN.Cash.posting(-700).asCost(100, USD), Assets.Cash.posting(100) ); console.log(utils.beanCount.serializationLedger(ledger));
基于 typescript ,可以很方便的编写复杂逻辑。
比如 ledger 就内置了 prepaid 分帐这个函数,可以很方便的将年付的订阅服务,均摊到每一个月。
ledger.transaction( ...prepaid({ date: "2021-01-03", start: "2021-01-01", from: assets.CN.Bank.Card.USTC, to: expenses.XGP, amount: -100, prepaid: assets.Prepaid, parts: 12, payee: "xgp", narration: "xgp prepaid", }) );
2021-01-03 * "xgp" "xgp prepaid" Assets:CN:Bank:Card:USTC -100 CNY Assets:Prepaid 100 CNY 2021-01-01 * "xgp" "xgp prepaid" remain: 11 Assets:Prepaid -8.33 CNY Expenses:XGP 8.33 CNY 2021-02-01 * "xgp" "xgp prepaid" remain: 10 Assets:Prepaid -8.33 CNY Expenses:XGP 8.33 CNY 2021-03-01 * "xgp" "xgp prepaid" remain: 9 Assets:Prepaid -8.33 CNY Expenses:XGP 8.33 CNY 2021-04-01 * "xgp" "xgp prepaid" remain: 8 Assets:Prepaid -8.33 CNY Expenses:XGP 8.33 CNY 2021-05-01 * "xgp" "xgp prepaid" remain: 7 Assets:Prepaid -8.33 CNY Expenses:XGP 8.33 CNY 2021-06-01 * "xgp" "xgp prepaid" remain: 6 Assets:Prepaid -8.33 CNY Expenses:XGP 8.33 CNY 2021-07-01 * "xgp" "xgp prepaid" remain: 5 Assets:Prepaid -8.33 CNY Expenses:XGP 8.33 CNY 2021-08-01 * "xgp" "xgp prepaid" remain: 4 Assets:Prepaid -8.33 CNY Expenses:XGP 8.33 CNY 2021-09-01 * "xgp" "xgp prepaid" remain: 3 Assets:Prepaid -8.33 CNY Expenses:XGP 8.33 CNY 2021-10-01 * "xgp" "xgp prepaid" remain: 2 Assets:Prepaid -8.33 CNY Expenses:XGP 8.33 CNY 2021-11-01 * "xgp" "xgp prepaid" remain: 1 Assets:Prepaid -8.33 CNY Expenses:XGP 8.33 CNY 2021-12-01 * "xgp" "xgp prepaid" remain: 0 Assets:Prepaid -8.37 CNY Expenses:XGP 8.37 CNY
![]() | 1 iyeatse 2024-02-19 22:19:40 +08:00 via iPhone ![]() Beancount 本来的优势是纯文本,用 ts 封装之后看上去变得更复杂了,一个账本文件中有一半都是语法关键字而不是内容本身,还是挺头疼的 |
![]() | 2 hamsterbase OP @iyeatse 货币与账户只需要定义一次。 后续的账本是很简单的。 可以直接 Assets.xxx 来使用账单 ``` tr( "1970-01-01", "Distribution of cash expenses", Assets.Cash.posting(-300), Expenses.Food.Alcool.posting(300) ); tr( "1970-01-01", "CN to usd", Assets.CN.Cash.posting(-700).asCost(100, USD), Assets.Cash.posting(100) ); `` 我还会封装一些常用的账户,只需要记录消费多少钱就行了, 其他都是自动生成的 ``` recentAccount.zs 招商信用卡( Expenses.Food.Alcool.posting(10), "2021-01-01", "买酒" ); ``` |
3 ufo5260987423 2024-02-20 01:37:38 +08:00 有一种 00 年代买家用电脑附赠记账工具的感觉。 |
![]() | 4 R4rvZ6agNVWr56V0 2024-02-20 01:52:36 +08:00 DSL 已经不适合当今社会了,利用 LLM 模型使用自然语言进行非结构化/半结构化数据处理才是王道。 |
![]() | 5 param 2024-02-20 20:45:03 +08:00 via Android @GeekGao DSL 手写不适合了,但是生成了然后只读还是有用的。毕竟自然语言没有严格的逻辑,很多时候表达一些逻辑还要靠伪代码呢。 |
![]() | 6 skies457 2024-02-21 10:43:03 +08:00 在 beancount 里分账可以用 forecast 插件 https://beancount.github.io/docs/api_reference/beancount.plugins.html#beancount.plugins.forecast |