Skip to content

Instantly share code, notes, and snippets.

@fergusq
Last active November 4, 2018 19:01
Show Gist options
  • Save fergusq/48b87457968d6f6eb98a1dfba72a53f4 to your computer and use it in GitHub Desktop.
Save fergusq/48b87457968d6f6eb98a1dfba72a53f4 to your computer and use it in GitHub Desktop.
A script that creates a report with pie charts and transactions from an Aktia bank statement.
/*
* Copyright (c) 2018 Iikka Hauhio
* Do whatever you want with this.
*
* This Röda script will process an Aktia bank statement and
* create a report with nice pie charts. It relies on the
* pgf-pie LaTeX package. After creating pictures using LaTeX
* it creates the report using Troff (Groff) and the ms
* preprocessor.
*
* Example usage:
* $ pdftotext tammikuun-tiliote.pdf
* $ röda abstr.röd "Tammikuu" tammikuun-tiliote.txt tammikuun-raportti.pdf
*/
{
j2r := require("json").jsonToRödaObj
logExec := require("fileutil").logExec
PARTIES := new map
current_day := ""
}
loadParties {
readLines "osapuolet.json" | concat | json | j2r(_) | pull partylist
for party in partylist do
PARTIES[party["name"]] = party["category"]
done
}
saveParties {
{
push "["
push([keys(PARTIES) | for party do
push `{"name":"$party","category":"${PARTIES[party]}"}`
done]&",")
push "]"
} | writeStrings "osapuolet.json"
}
record Transaction(date, party, cat, val) {
date : string = date
party : string = party
category : string = cat
val : integer = val
function asString {
return party.."("..val/100..")"
}
function askCategoryIfNeeded {
return if [ self.category != "" ]
unless [ PARTIES[party]? or self.category != "" ] do
STDOUT.push "Kategoria: ", party, "? "
STDIN.pull category
PARTIES[party] = category
done
self.category = PARTIES[party]
}
}
loadTransactionsJson file {
readLines file | concat | json | j2r(_) | pull transactionList
return [push(new Transaction(t["date"], t["party"], t["category"], parseInteger(t["val"]))) for t in transactionList]
}
saveTransactionsJson file, transactions {
{
push "[\n"
push([transactions() | for t do
push `{"date":"${t.date}","party":"${t.party}","category":"${t.category}","val":"${t.val}"}`
done]&",\n")
push "]"
} | writeStrings file
}
createCharts expense_cat_sums, income_cat_sums {
createChart income_cat_sums, 1, "tulot.ps"
createChart expense_cat_sums, -1, "menot.ps"
}
createChart cat_sums, sign, out_file {
{
total := 0
keys cat_sums | total += sign*cat_sums[key]/100 for key
push `\documentclass{minimal}\usepackage[utf8]{inputenc}\usepackage[T1]{fontenc}\usepackage{pgf-pie}\begin{document}\begin{tikzpicture}\pie[text=legend,after number=e, sum=$total]{`
i := 0
sortedByValue cat_sums | for val, key do
push ", " if [ i > 0 ]
push `${sign*val/100}/$key`
i ++
done
push `}\end{tikzpicture}\end{document}`
} | writeStrings "tmp.tex"
logExec "pdflatex", "tmp.tex"
logExec "pdfcrop", "tmp.pdf"
logExec "pdftops", "tmp-crop.pdf"
logExec "mv", "tmp-crop.ps", out_file
logExec "zsh", "-c", "rm tmp*"
}
createReport time, transactions, expense_cat_sums, income_cat_sums, party_sums, total, out_file {
expense_total := transactions() | min([ _.val, 0 ]) | sum
income_total := transactions() | max([ _.val, 0 ]) | sum
{
print `.nr PO 0.65in`
print `.nr LL 7in`
print `.pl 11.7in`
print `.nr PI 0.3in`
print `.nr HM 0.5in`
print `.2C`
print ".LG\n", time
print ".NH 1\nTiivistelmä"
print `.LP`
print `.TS`
print `expand;`
print `lb rb rb`
print `l r r.`
print "Kategoria\t%\tSumma\n_"
sortedByValue expense_cat_sums | for val, key if [ val != 0 ] do
print key, "\t",
formatMoney(val/expense_total*10000), "\t",
formatMoney(val)
done
print `_`
print "MENOT YHTEENSÄ\t\t", formatMoney(expense_total)
print `=`
sortedByValue income_cat_sums | for val, key if [ val != 0 ] do
print key, "\t",
formatMoney(val/income_total*10000), "\t",
formatMoney(val)
done
print `_`
print "TULOT YHTEENSÄ\t\t", formatMoney(income_total)
print `=`
print "YLIJÄÄMÄ (ALIJÄÄMÄ)\t\t", formatMoney(total)
print `.TE`
print ".NH 2\nTulot"
print `.PSPIC -L tulot.ps`
print ".NH 2\nMenot"
print `.PSPIC -L menot.ps`
print ".NH 1\nYksityiskohtainen listaus"
print ".NH 2\nOsapuolet"
print `.LP`
print `.TS`
print `expand;`
print `lb rb`
print `l r.`
print "Osapuoli\tSumma\n_"
sortedByValue party_sums | for val, key if [ val != 0 ] do
print key, "\t", formatMoney(val)
done
print `=`
print "YLIJÄÄMÄ (ALIJÄÄMÄ)\t", formatMoney(total)
print `.TE`
print ".NH 2\nTilitapahtumat"
print `.LP`
print `.TS`
print `expand;`
print `lb lb rb`
print `l l r.`
print "Päiväys\tOsapuoli\tSumma\n_"
for t in transactions do
print t.date, "\t", t.party[:min([#t.party, 18])], "\t", formatMoney(t.val)
done
print `=`
print "YLIJÄÄMÄ\t(ALIJÄÄMÄ)\t", formatMoney(total)
print `.TE`
} | exec "groff", "-p", "-t", "-m", "ms", "-Kutf8", "-Tps", "-dpaper=a4l", "-P-pa4" | writeStrings "tmp.ps"
logExec "ps2pdf", "tmp.ps", out_file
logExec "zsh", "-c", "rm tmp*"
}
main time, in_file, out_file, flags... {
if [ "--jsonista" in flags ] do
loadTransactionsJson in_file
else
loadTransactionsTxt in_file
done | pull transactions
if [ "--jsoniksi" in flags ] do
saveTransactionsJson out_file, transactions
else
processTransactions transactions, time, out_file
done
}
loadTransactionsTxt in_file {
loadParties
transactions := []
readLines in_file | fixSpaces | {
skipHeader
while true do
break unless skipToRecord
readLine line
if [ fieldNum(line) = 4 ] do
parseFields line, code1, code2, n, val
readLine code3
readLine party
readLine location
readLine code4
readLine code5
transactions += new Transaction(current_day, party, "", parseMoney(val))
else
parseFields line, code1, code2, party, n, val
readLine code3
readLine message
transactions += new Transaction(current_day, party, "", parseMoney(val))
done
done
}
t.askCategoryIfNeeded for t in transactions
saveParties
return transactions
}
processTransactions transactions, time, out_file {
s := 0
expense_cat_sums := new map
income_cat_sums := new map
party_sums := new map
for t in transactions do
expense_cat_sums[t.category] = 0 if [ t.val < 0 ]
income_cat_sums[t.category] = 0 if [ t.val > 0 ]
party_sums[t.party] = 0
done
for t in transactions do
expense_cat_sums[t.category] += t.val if [ t.val < 0 ]
income_cat_sums[t.category] += t.val if [ t.val > 0 ]
party_sums[t.party] += t.val
s += t.val
done
expense_total := transactions() | min([ _.val, 0 ]) | sum
income_total := transactions() | max([ _.val, 0 ]) | sum
expense_divider := keys(expense_cat_sums) | abs(expense_cat_sums[_]) | max() / 120
income_divider := keys(income_cat_sums) | abs(income_cat_sums[_]) | max() / 120
{
push(["Kategoria","%","€",""])
push(["","","",""])
sortedByValue expense_cat_sums | for csum, cat if [ csum != 0 ] do
push([cat, csum/expense_total*100//1, csum/100, "-"*(-csum//expense_divider)])
done
sortedByValue income_cat_sums | for csum, cat if [ csum != 0 ] do
push([cat, csum/income_total*100//1, csum/100, "+"*(csum//income_divider)])
done
push(["","","",""])
push(["TOTAL","",s/100,""])
} | tasaa
createCharts expense_cat_sums, income_cat_sums
createReport time, transactions, expense_cat_sums, income_cat_sums, party_sums, s, out_file
}
fixSpaces {
replace `([AK]) (\d{4})`, "$1 $2 "
}
skipHeader {
for line do
break if [ line =~ "Kirj.pvm.*" ]
done
}
skipToRecord {
for line do
if [ line =~ "KIRJAUSPÄIVÄ .*" ] do
current_day = line[#"KIRJAUSPÄIVÄ ":]
done
if [ line =~ "FT.*" ] do
unpull line
true
return
done
done
false
}
fieldNum line {
return #[split(line, sep=" +")]
}
parseFields line, &vars... {
vars <> split line, sep=" +" | for var, field do
var := field
done
}
readLine &var {
pull var
while [ var =~ "\\s*SIIRTO\\s*" ] do
pull var
skipHeader
done
while [ var =~ "" ] do
pull var
done
var ~= "^\\s*|\\s*$", ""
}
parseMoney val {
val ~= "\\.", "", "(\\d+) (.)", "$2$1"
parseInteger val
}
formatMoney val {
val /= 100
val = match("(-?)(\\d+)\\.(\\d+)", `$val`)
ans := ""
ans .= "– " if [ val[1] = "-" ]
ans .= val[2]
ans .= ","
ans .= val[3] if [ #val[3] = 2 ]
ans .= val[3][:2] if [ #val[3] > 2 ]
ans .= val[3].."0" if [ #val[3] < 2 ]
return ans
}
tasaa {
kaikki := [identity()]
pituudet := [interleave(*kaikki) | while open do
head(#kaikki) | [ #`$_` ] | max()
done]
kaikki | for rivi do
pituudet <> rivi <> seq(1, #rivi) | for pituus, arvo, i do
push(arvo, " "*(pituus-#`$arvo`))
push(" | ") unless [ i = #rivi ]
done
print("")
done
}
sortedByValue kv {
keys kv | [[kv[_1], _1]] | sort | _
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment