V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
hamsterbase
V2EX  ›  分享创造

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

  •  
  •   hamsterbase ·
    carytrivett · 95 天前 · 2400 次点击
    这是一个创建于 95 天前的主题,其中的信息可能已经有所发展或是发生改变。

    项目开源地址为 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
    
    6 条回复    2024-02-21 10:43:03 +08:00
    iyeatse
        1
    iyeatse  
       95 天前 via iPhone   ❤️ 2
    Beancount 本来的优势是纯文本,用 ts 封装之后看上去变得更复杂了,一个账本文件中有一半都是语法关键字而不是内容本身,还是挺头疼的
    hamsterbase
        2
    hamsterbase  
    OP
       95 天前
    @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
        3
    ufo5260987423  
       95 天前
    有一种 00 年代买家用电脑附赠记账工具的感觉。
    GeekGao
        4
    GeekGao  
       95 天前
    DSL 已经不适合当今社会了,利用 LLM 模型使用自然语言进行非结构化/半结构化数据处理才是王道。
    param
        5
    param  
       94 天前 via Android
    @GeekGao DSL 手写不适合了,但是生成了然后只读还是有用的。毕竟自然语言没有严格的逻辑,很多时候表达一些逻辑还要靠伪代码呢。
    skies457
        6
    skies457  
       94 天前
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2279 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 10:43 · PVG 18:43 · LAX 03:43 · JFK 06:43
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.