Skip to content

Commit

Permalink
perf: compute balances faster
Browse files Browse the repository at this point in the history
instead of adding to the balance of each account up the heirarchy
for each transaction, add to the account in the transaction and
roll-up after
  • Loading branch information
howeyc committed Oct 14, 2023
1 parent 024e293 commit 19723ab
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 12 deletions.
53 changes: 41 additions & 12 deletions balances.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package ledger
import (
"slices"
"strings"

"github.com/howeyc/ledger/decimal"
)

// GetBalances provided a list of transactions and filter strings, returns account balances of
Expand All @@ -13,6 +15,37 @@ import (
func GetBalances(generalLedger []*Transaction, filterArr []string) []*Account {
var accList []*Account
balances := make(map[string]*Account)

// at every depth, for each account, track the parent account
depthMap := make(map[int]map[string]string)
var maxDepth int

incAccount := func(accName string, val decimal.Decimal) {
// track parent
var pmap map[string]string
pmapfound := false
accDepth := strings.Count(accName, ":") + 1
pmap, pmapfound = depthMap[accDepth]
if !pmapfound {
pmap = make(map[string]string)
depthMap[accDepth] = pmap
}
if _, foundparent := pmap[accName]; !foundparent && accDepth > 1 {
colIdx := strings.LastIndex(accName, ":")
pmap[accName] = accName[:colIdx]
maxDepth = max(maxDepth, accDepth)
}

// add to balance
if acc, ok := balances[accName]; !ok {
acc := &Account{Name: accName, Balance: val}
accList = append(accList, acc)
balances[accName] = acc
} else {
acc.Balance = acc.Balance.Add(val)
}
}

for _, trans := range generalLedger {
for _, accChange := range trans.AccountChanges {
inFilter := len(filterArr) == 0
Expand All @@ -22,22 +55,18 @@ func GetBalances(generalLedger []*Transaction, filterArr []string) []*Account {
}
}
if inFilter {
accHier := strings.Split(accChange.Name, ":")
accDepth := len(accHier)
for currDepth := accDepth; currDepth > 0; currDepth-- {
currAccName := strings.Join(accHier[:currDepth], ":")
if acc, ok := balances[currAccName]; !ok {
acc := &Account{Name: currAccName, Balance: accChange.Balance}
accList = append(accList, acc)
balances[currAccName] = acc
} else {
acc.Balance = acc.Balance.Add(accChange.Balance)
}
}
incAccount(accChange.Name, accChange.Balance)
}
}
}

// roll-up balances
for curDepth := maxDepth; curDepth > 1; curDepth-- {
for accName, parentName := range depthMap[curDepth] {
incAccount(parentName, balances[accName].Balance)
}
}

slices.SortFunc(accList, func(a, b *Account) int {
return strings.Compare(a.Name, b.Name)
})
Expand Down
64 changes: 64 additions & 0 deletions balances_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package ledger
import (
"bytes"
"encoding/json"
"fmt"
"math/rand"
"testing"
"time"

"github.com/howeyc/ledger/decimal"
)
Expand Down Expand Up @@ -38,6 +41,36 @@ var testBalCases = []testBalCase{
},
nil,
},
{
"heirarchy",
`1970/01/01 Payee
Expense:test (123 * 3)
Assets
1970/01/01 Payee
Expense:foo 123
Assets
`,
[]Account{
{
Name: "Assets",
Balance: decimal.NewFromFloat(-4 * 123),
},
{
Name: "Expense",
Balance: decimal.NewFromFloat(123 + 369),
},
{
Name: "Expense:foo",
Balance: decimal.NewFromFloat(123),
},
{
Name: "Expense:test",
Balance: decimal.NewFromFloat(369),
},
},
nil,
},
}

func TestBalanceLedger(t *testing.T) {
Expand All @@ -56,6 +89,37 @@ func TestBalanceLedger(t *testing.T) {
}
}

func BenchmarkGetBalances(b *testing.B) {
var trans []*Transaction
for i := 0; i < 100000; i++ {
a := rand.Intn(50)
b := rand.Intn(10)
c := rand.Intn(5)
d := rand.Intn(50)
e := rand.Intn(10)
f := rand.Intn(5)
amt := rand.Float64() * 10000
trans = append(trans, &Transaction{
Date: time.Now(),
Payee: fmt.Sprintf("Trans %d", i),
AccountChanges: []Account{
{
Name: fmt.Sprintf("Acc%d:Acc%d:Acc%d", a, b, c),
Balance: decimal.NewFromFloat(amt),
},
{
Name: fmt.Sprintf("Acc%d:Acc%d:Acc%d", d, e, f),
Balance: decimal.NewFromFloat(-amt),
},
},
})
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
GetBalances(trans, []string{})
}
}

func TestBalancesByPeriod(t *testing.T) {
b := bytes.NewBufferString(`
2022/02/02 Payee
Expand Down

0 comments on commit 19723ab

Please sign in to comment.