This post is about a DIY way to split bills with friends. It sets itself apart from a smartphone app solution in the following aspects:
- It combines small programs that are readily available on UNIX-based operating systems (Linux, MacOS, etc);
- Its ledgers are stored in plain-text files;
- It operates on the command-line;
- It uses shell scripts to process data and produce html reports.
Essentially, a way to split bills UNIX style.
Double entry bookkeeping
This is the standard bookkeeping method in accounting. I’m by no means an accounting professional but here is how I make sense of it for this simple bill-splitting use case: Essentially, every transaction should involve at least two accounts and at least one positive and one negative records, summing up to zero. Say Alice pays $90 for the lunch of the group (Alice, Bob and Carol). The two accounts involved here are Alice’s income account and the expense account of the rest of the group - so actually four of them:
- Alice’s income;
- Alice’s expense;
- Bob’s expense;
- Carol’s expense.
When Alice pays, the convention is that this incurs a negative entry in her income account (I guess that makes sense). On the opposite side, consuming on something results in a positive entry in the expense account. So the lunch transaction can be booked with the following entries:
Negative | Positive |
---|---|
Alice’s income: -$90 | Alice’s expense: +$30 |
Bob’s expense: +$30 | |
Carol’s expense: +$30 |
All values balance out, which is very important (satisfying the accounting equation). Now let’s say the place where the group goes for dinner only accepts cash and the bill is $150. None of them has enough cash so they combine what they have:
Negative | Positive |
---|---|
Alice’s income: -$10 | Alice’s expense: +$50 |
Bob’s income: -$120 | Bob’s expense: +$50 |
Carol’s income: -$20 | Carol’s expense: +$50 |
With every shared bill booked this way, finding how much a person owes (or is owed) becomes a simple matter of adding all the entries from that person’s accounts:
Settling-up | |
---|---|
Alice | -$20 (= -$90-$10+$30+$50) |
Bob | -$40 (= -$120+$30+$50) |
Carol | +$60 (= -$20+$30+$50) |
A negative value means the amount one is owed. In this case, Carol needs to pay Alice $20 and Bob $40.
Two “read more” links on double entry bookkeeping: wikipedia’s article, beancount’s article.
Beancount, plain-text accounting
The free software I use that
does the bookkeeping and the math above is
Beancount. Its killer feature, for me, is the
plain-text ledger file (in contrast with GnuCash,
for example). The ledger file (with .beancount
extension) for the example
above would look like this (note that ;;
starts a comment line):
;; Opening accounts
2024-05-01 open Income:Alice
2024-05-01 open Income:Bob
2024-05-01 open Income:Carol
2024-05-01 open Expenses:Lunch:Alice
2024-05-01 open Expenses:Lunch:Bob
2024-05-01 open Expenses:Lunch:Carol
2024-05-01 open Expenses:Dinner:Alice
2024-05-01 open Expenses:Dinner:Bob
2024-05-01 open Expenses:Dinner:Carol
;; Ledger starts
2024-05-02 * "Restaurant A" "Lunch"
Income:Alice -90.00 CAD
Expenses:Lunch:Alice 30.00 CAD
Expenses:Lunch:Bob 30.00 CAD
Expenses:Lunch:Carol 30.00 CAD
2024-05-02 * "Restaurant B" "Dinner"
Income:Alice -10.00 CAD
Income:Bob -120.00 CAD
Income:Carol -20.00 CAD
Expenses:Dinner:Alice 50.00 CAD
Expenses:Dinner:Bob 50.00 CAD
Expenses:Dinner:Carol 50.00 CAD
And here is a greatly simplified version thanks to beancount’s smart plugins:
;; This plugin splits expense equally automatically
plugin "beancount.plugins.split_expenses" "Alice Bob Carol"
;; This plugin opens accounts automatically
plugin "beancount.plugins.auto_accounts"
2024-05-01 * "Restaurant A" "Lunch"
Income:Alice -90.00 CAD
Expenses:Lunch
2024-05-01 * "Restaurant B" "Dinner"
Income:Alice -10.00 CAD
Income:Bob -120.00 CAD
Income:Carol -20.00 CAD
Expenses:Dinner
Now you can run bean-query
on your ledger file, which is Beancount’s own
SQL-like query client. For example, if Alice wants to see her part of the
ledger:
$ bean-query file.beancount 'SELECT date,account,payee,position WHERE account ~ "Alice"'
date account payee position
---------- --------------------- ------------ ----------
2024-05-01 Income:Alice Restaurant A -90.00 CAD
2024-05-01 Expenses:Lunch:Alice Restaurant A 30.00 CAD
2024-05-01 Income:Alice Restaurant B -10.00 CAD
2024-05-01 Expenses:Dinner:Alice Restaurant B 50.00 CAD
And if Alice asks for the net amount:
$ bean-query file.beancount 'SELECT sum(position) WHERE account ~ "Alice"'
sum_positi
----------
-20.00 CAD
So she is being owed 20 bucks. Check out here for more supported queries.
Editing-wise, Vim users should check out the beancount plugin. Emacs support is also available and probably comes with more goodies since Beancount’s author uses Emacs and contributes to the plugin.
Of course, this toy bill-splitting example does very little justice to the power of Beancount. For more possibilities, see the cookbooks & examples section in its documentation.
Processing and reporting with a shell script
After editting the ledger file, I use a shell script to process the data and generate an html report. There is a css stylesheet that beautifies the report but I tried to make it minimalistic. See here for a demo of a ledger splitting the bills during a group trip. Another example would be sharing house utility bills with your roommates. See here for an example.
The small set of scripts (including the two example beancount files in the previous paragrah) is downloadable here. And here is what you are downloading:
.
├── fold.awk
├── house.beancount
├── ledger.css
├── ledger_update
└── trip.beancount
1 directory, 5 files
The main script ledger_update
takes two arguments: (1) the main beancount
file and (2) the user-specified title of the html report. For the house utility
ledger, you may run it like this
./ledger_update house.beancount "Sharing utilities with roommates"
This will produce the
ledger page as a ledger.html
.
Remark: the fold.awk
script called in ledger_update
is some
awk magic that folds the
ledger to de-clutter the html page.
Finally, the full script
The full script can be downloaded here.