Plaintext budgeting


Reading time: about 1 minute

For the past ~6 months, I’ve been using an Android application to keep track of my daily spending. To my annoyance, I found out that the app doesn’t have an export functionality. I didn’t want to invest more time in a platform that I couldn’t get my data out of, so I started looking for another solution.

I’ve looked into budgeting systems before, and I’ve seen both command-line (ledger) and GUI systems (GNUCash). Now; both of these are great software, and I can appreciate how Double-entry bookkeeping is a useful thing for accounting purposes. But while they are powerful, they’re not as simple as they could be.

I decided to go with CSV files. CSV is one of the most universal file formats, it’s simple and obvious. I can process it with pretty much every programming language and import it to pretty much every spreadsheet software. Or… I could use a shell script to run calculations with SQLite.

If I ever want to migrate to another system; it will probably be possible to convert this file with a shell script, or even a sed command.

I create monthly CSV files in order to keep everything nice and tidy, but the script adapts to everything from a single CSV file to one file for each day/hour/minute.

Here’s what an example file looks like:

Date,Amount,Currency,Category,Description
2019-04-02,5.45,EUR,Food,Centra
2019-04-03,2.75,EUR,Transport,Bus to work

And here’s the script:

#!/bin/sh

days=${1:-7}

cat *.csv | sed '/^Date/d' > combined.csv.temp

output=$(sqlite3 <<EOF
create table Transactions(Date, Amount, Currency, Category, Description);
.mode csv
.import combined.csv.temp Transactions
.mode list

select 'Amount spent today:',
coalesce(sum(Amount), 0) from Transactions where Date = '$(date +%Y-%m-%d)';

select '';
select 'Last $days days average:',
sum(Amount)/$days, Currency from Transactions where Date > '$(date --date="-$days days" +%Y-%m-%d)'
group by Currency;

select '';
select 'Last $days days by category';
select '=======================';

select Category, sum(Amount) from Transactions
where Date > '$(date --date="-$days days" +%Y-%m-%d)'
group by Category order by sum(Amount) desc;
EOF
      )

rm combined.csv.temp

echo "$output" | sed 's/|/ /g'

This is the output of the command

[leo@leo-arch budget]$ ./budget.sh
Amount spent today: 8.46

Last 7 days average: 15.35 EUR

Last 7 days by category
=======================
Groceries 41.09
Transport 35.06
Food 31.35
[leo@leo-arch budget]$ ./budget.sh 5
Amount spent today: 8.46

Last 5 days average: 11.54 EUR

Last 5 days by category
=======================
Groceries 29.74
Transport 17.06
Food 10.9
[leo@leo-arch budget]$

The following pages link here

Citation

If you find this work useful, please cite it as:
@article{yaltirakli201904plaintextbudgeting,
  title   = "Plaintext budgeting",
  author  = "Yaltirakli, Gokberk",
  journal = "gkbrk.com",
  year    = "2019",
  url     = "https://www.gkbrk.com/2019/04/plaintext-budgeting/"
}
Not using BibTeX? Click here for more citation styles.
IEEE Citation
Gokberk Yaltirakli, "Plaintext budgeting", April, 2019. [Online]. Available: https://www.gkbrk.com/2019/04/plaintext-budgeting/. [Accessed Nov. 12, 2024].
APA Style
Yaltirakli, G. (2019, April 03). Plaintext budgeting. https://www.gkbrk.com/2019/04/plaintext-budgeting/
Bluebook Style
Gokberk Yaltirakli, Plaintext budgeting, GKBRK.COM (Apr. 03, 2019), https://www.gkbrk.com/2019/04/plaintext-budgeting/

Comments

Comment by rjc
2019-04-06 at 19:08
Spam probability: 0.005%

There are very few things which **cannot** be _improved_, so here we go :^) ``` $ diff -u budget.sh.orig budget --- budget.sh.orig Sat Apr 6 19:46:23 2019 +++ budget Fri Apr 5 12:41:50 2019 @@ -1,21 +1,24 @@ #!/bin/sh +tempfile=$(mktemp) days=${1:-7} +today=$(date '+%Y-%m-%d') +date=$(date -r $(($(date +%s) - $(($days * 86400)))) '+%Y-%m-%d') -cat *.csv | sed '/^Date/d' > combined.csv.temp +sed '/^Date/d' *.csv > $tempfile -output=$(sqlite3 <<EOF +sqlite3 <<EOF | sed 's/|/ /g' create table Transactions(Date, Amount, Currency, Category, Description); .mode csv -.import combined.csv.temp Transactions +.import $tempfile Transactions .mode list select 'Amount spent today:', -coalesce(sum(Amount), 0) from Transactions where Date = '$(date +%Y-%m-%d)'; +coalesce(sum(Amount), 0) from Transactions where Date = '$today'; select ''; select 'Last $days days average:', -sum(Amount)/$days, Currency from Transactions where Date > '$(date --date="-$days days" +%Y-%m-%d)' +sum(Amount)/$days, Currency from Transactions where Date > '$date' group by Currency; select ''; @@ -23,12 +26,8 @@ select '======================='; select Category, sum(Amount) from Transactions -where Date > '$(date --date="-$days days" +%Y-%m-%d)' +where Date > '$date' group by Category order by sum(Amount) desc; EOF - ) -rm combined.csv.temp - -echo "$output" | sed 's/|/ /g' - +rm $tempfile ``` It should all be self-explanatory but just in case it isn't...: 1. Use `mktemp` for the temporary file - always a good habbit, IMHO. 2. `--date` is `GNU`-only, thus not portable so use something that will, **at least** work on _OpenBSD_, _macOS_, and _Linux_ - still not _POSIX_, but _oh well_... 3. Given that the same `date` command is ran twice, run it once and catch the output into a variable. 4. Remove a UUOC - http://porkmail.org/era/unix/award.html ;^) 5. No need to catch the output of `sqlite3` + _heredoc_ into a variable, so run it and pipe it to `sed` directly. We could also do something about `*.csv`, i.e. use `$1` for it and `$2` for days, maybe `getopt(1)`, but that would be a bit much for a simple tool like this, IMHO.

© 2024 Gokberk Yaltirakli