@zuccs/beancount-lite
v0.1.0
Published
Lightweight TypeScript library that parses and validates beancount-format plain-text journals. Runs in Cloudflare Workers. Zero dependencies.
Downloads
13
Maintainers
Readme
beancount-lite
Lightweight TypeScript library that parses and validates Beancount-format plain-text journals. Runs in Cloudflare Workers. Zero dependencies.
This project implements a subset of the Beancount plain-text accounting language created by Martin Blais. Beancount is the original system — a powerful, mature double-entry bookkeeping tool written in Python. All credit for the language design, syntax specification, and accounting semantics belongs to Martin and the Beancount community. This library is an independent TypeScript implementation that reads the same file format; it is not affiliated with or endorsed by the Beancount project.
If you're new to plain-text accounting, start with the real thing: beancount.github.io/docs
Why this exists
We needed to parse and validate Beancount journals inside Cloudflare Workers — no filesystem, no Python runtime, no binary dependencies. This library takes a journal string in, gives structured data out.
It deliberately implements only the subset of Beancount we need. If you need the full language (commodity prices, cost basis, lot tracking, plugins, queries), use Beancount itself or Fava.
Install
npm install beancount-liteQuick start
import { parse, validate, balances, balanceReport, formatTransaction } from "beancount-lite";
const journal = parse(`
2024-01-01 open Assets:Bank:Checking AUD
2024-01-01 open Expenses:Food
2024-01-01 open Income:Salary
2024-01-01 open Equity:Opening-Balances
2024-01-01 * "Opening Balance"
Assets:Bank:Checking 5000.00 AUD
Equity:Opening-Balances -5000.00 AUD
2024-01-15 * "Woolworths" "Weekly groceries" #household
Expenses:Food 125.50 AUD
Assets:Bank:Checking -125.50 AUD
2024-01-31 * "Employer" "January salary"
Assets:Bank:Checking 4500.00 AUD
Income:Salary -4500.00 AUD
2024-02-01 balance Assets:Bank:Checking 9374.50 AUD
`);
// Validate — returns errors (empty array = all good)
const errors = validate(journal);
console.log(errors); // []
// Query balances
const bal = balances(journal);
console.log(bal.get("Assets:Bank:Checking")); // 9374.50
console.log(bal.get("Expenses:Food")); // 125.50
// Balance report with hierarchy rollup
const report = balanceReport(journal, { account: "Expenses" });
// Expenses → 125.50
// Expenses:Food → 125.50API
parse(text: string): Journal
Parse Beancount journal text into structured data.
const journal = parse(journalText);
// journal.transactions — Transaction[]
// journal.balanceDirectives — BalanceDirective[]
// journal.accountDirectives — AccountDirective[] (open/close)validate(journal: Journal, options?): ValidationError[]
Validate a parsed journal. Returns an array of errors (empty = valid).
const errors = validate(journal);
// Options:
// strict?: boolean — require all accounts to be declared with `open` (default: auto-detected)
// duplicates?: boolean — check for duplicate transactions (default: true)Checks performed:
- Every transaction balances to zero (or has exactly one auto-balanced posting)
- Balance assertions match running totals (beginning-of-day semantics, per the Beancount spec)
- All accounts are declared with
opendirectives (when open directives are present) - No duplicate transactions (same date + narration + amount)
balances(journal: Journal, filter?): Map<string, number>
Compute per-account balances, optionally filtered.
const bal = balances(journal, {
dateFrom: "2024-01-01",
dateTo: "2024-01-31",
account: "Expenses", // prefix match
});balanceReport(journal: Journal, filter?): Map<string, number>
Like balances() but rolls up totals through the account hierarchy.
const report = balanceReport(journal, { account: "Expenses" });
// Expenses:Property:Utilities:Electricity → 150
// Expenses:Property:Utilities:Water → 80
// Expenses:Property:Utilities → 230 (rolled up)
// Expenses:Property → 230 (rolled up)
// Expenses → 230 (rolled up)filterTransactions(journal: Journal, filter?): Transaction[]
Filter transactions by date range and/or account prefix.
formatTransaction(tx: Transaction): string
Format a Transaction object as valid Beancount journal text.
const text = formatTransaction({
date: "2024-01-15",
flag: "*",
payee: "Woolworths",
narration: "Weekly groceries",
postings: [
{ account: "Expenses:Food", amount: 125.50, currency: "AUD" },
{ account: "Assets:Bank:Checking", amount: -125.50, currency: "AUD" },
],
tags: new Set(["household"]),
links: new Set(),
meta: {},
line: 0,
});formatBalanceReport(balanceMap: Map<string, number>, currency?): string
Format a balance map as aligned plain text.
checkAssertion(journal, account, expectedBalance, asOfDate, currency?): boolean
Check a single balance assertion against journal state.
Beancount syntax supported
This library parses the following Beancount directives and syntax:
| Feature | Supported | Example |
|---|---|---|
| Transactions | ✅ | 2024-01-15 * "Payee" "Narration" |
| Flags | ✅ | * (cleared), ! (pending) |
| txn keyword | ✅ | 2024-01-15 txn "Narration" |
| Payee + narration | ✅ | "Payee" "Narration" |
| Narration only | ✅ | "Just a narration" |
| Bare transactions | ✅ | 2024-01-15 * (no narration) |
| Postings with amounts | ✅ | Assets:Bank:Checking 100.00 AUD |
| Auto-balanced postings | ✅ | Assets:Bank:Checking (amount inferred) |
| open directive | ✅ | 2024-01-01 open Assets:Bank AUD |
| close directive | ✅ | 2024-12-31 close Assets:Bank |
| balance directive | ✅ | 2024-02-01 balance Assets:Bank 1000.00 AUD |
| Tags | ✅ | #tag-name |
| Links | ✅ | ^link-name |
| Metadata | ✅ | key: "value" (indented under directive) |
| Comments | ✅ | ; comment text |
| option directive | ✅ | Recognised and skipped |
| ISO dates | ✅ | YYYY-MM-DD and YYYY/MM/DD |
| Account types | ✅ | Assets, Liabilities, Equity, Income, Expenses |
Not supported (by design)
These Beancount features are intentionally excluded to keep the library small:
- Multi-currency / commodity prices / cost basis (
{...},@ price) - Lot tracking and inventory booking
paddirectivenote,document,event,query,customdirectivespushtag/poptagincludeandplugindirectives- Arithmetic expressions in amounts
- The full Beancount query language (BQL)
- Budget tracking
Data types
type Transaction = {
line: number; // Source line number (1-based)
date: string; // "2024-01-15"
flag: "*" | "!"; // Cleared or pending
payee?: string; // "AGL Energy"
narration: string; // "Electricity bill"
postings: Posting[];
tags: Set<string>; // #tag
links: Set<string>; // ^link
meta: Record<string, string>; // key: "value"
comment?: string; // ; inline comment
};
type Posting = {
account: string; // "Expenses:Property:Utilities:Electricity"
amount?: number; // Undefined = auto-balanced
currency?: string; // "AUD"
};
type Journal = {
transactions: Transaction[];
balanceDirectives: BalanceDirective[];
accountDirectives: AccountDirective[];
};Environment
- Pure TypeScript, no Node-specific APIs
- Runs in Cloudflare Workers (no
fs, nopath, noprocess) - Input is always a string
- Zero external runtime dependencies
- ESM and CJS builds included
Credits
Beancount was created by Martin Blais and is the foundation this library builds on. The language syntax, accounting semantics (balance assertion timing, transaction balancing rules, account hierarchy, auto-balanced postings), and overall design philosophy all come from Beancount.
- Beancount documentation: beancount.github.io/docs
- Beancount language syntax: Syntax Reference
- Fava (web UI for Beancount): github.com/beancount/fava
- Plain-text accounting community: plaintextaccounting.org
This library is an independent implementation. It is not affiliated with, endorsed by, or a fork of Beancount.
License
MIT
