ledger-ts:基于 TypeScript 的开源记账 DSL

80 天前
 hamsterbase

项目开源地址为 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
2371 次点击
所在节点    分享创造
6 条回复
iyeatse
80 天前
Beancount 本来的优势是纯文本,用 ts 封装之后看上去变得更复杂了,一个账本文件中有一半都是语法关键字而不是内容本身,还是挺头疼的
hamsterbase
80 天前
@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",
"买酒"
);
```
ufo5260987423
80 天前
有一种 00 年代买家用电脑附赠记账工具的感觉。
GeekGao
80 天前
DSL 已经不适合当今社会了,利用 LLM 模型使用自然语言进行非结构化/半结构化数据处理才是王道。
param
79 天前
@GeekGao DSL 手写不适合了,但是生成了然后只读还是有用的。毕竟自然语言没有严格的逻辑,很多时候表达一些逻辑还要靠伪代码呢。
skies457
78 天前

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1016670

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX