使用之前推荐先学习一下 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
usd, 1970-01-01, assets, Cash