/
balancer.go
161 lines (145 loc) · 4.53 KB
/
balancer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package process
import (
"context"
"fmt"
"sort"
"github.com/sboehler/knut/lib/common/amounts"
"github.com/sboehler/knut/lib/common/cpr"
"github.com/sboehler/knut/lib/journal"
"github.com/sboehler/knut/lib/journal/ast"
)
// Balancer processes ASTs.
type Balancer struct {
Context journal.Context
}
// Process processes days.
func (pr *Balancer) Process(ctx context.Context, inCh <-chan *ast.Day, outCh chan<- *ast.Day) error {
amounts := make(amounts.Amounts)
accounts := make(accounts)
for {
d, ok, err := cpr.Pop(ctx, inCh)
if err != nil {
return err
}
if !ok {
break
}
var transactions []*ast.Transaction
if err := pr.processOpenings(ctx, accounts, d); err != nil {
return err
}
if err := pr.processTransactions(ctx, accounts, amounts, d); err != nil {
return err
}
if transactions, err = pr.processValues(ctx, accounts, amounts, d); err != nil {
return err
}
if err = pr.processAssertions(ctx, accounts, amounts, d); err != nil {
return err
}
if err = pr.processClosings(ctx, accounts, amounts, d); err != nil {
return err
}
d.Transactions = append(d.Transactions, transactions...)
sort.Slice(d.Transactions, func(i, j int) bool {
return d.Transactions[i].Less(d.Transactions[j])
})
d.Amounts = amounts.Clone()
if err := cpr.Push(ctx, outCh, d); err != nil {
return err
}
}
return nil
}
func (pr *Balancer) processOpenings(ctx context.Context, accounts accounts, d *ast.Day) error {
for _, o := range d.Openings {
if err := accounts.Open(o.Account); err != nil {
return err
}
}
return nil
}
func (pr *Balancer) processTransactions(ctx context.Context, accounts accounts, amts amounts.Amounts, d *ast.Day) error {
for _, t := range d.Transactions {
for _, p := range t.Postings() {
if !accounts.IsOpen(p.Credit) {
return Error{t, fmt.Sprintf("credit account %s is not open", p.Credit)}
}
if !accounts.IsOpen(p.Debit) {
return Error{t, fmt.Sprintf("debit account %s is not open", p.Debit)}
}
amts.Add(amounts.AccountCommodityKey(p.Credit, p.Commodity), p.Amount.Neg())
amts.Add(amounts.AccountCommodityKey(p.Debit, p.Commodity), p.Amount)
}
}
return nil
}
func (pr *Balancer) processValues(ctx context.Context, accounts accounts, amts amounts.Amounts, d *ast.Day) ([]*ast.Transaction, error) {
var transactions []*ast.Transaction
for _, v := range d.Values {
if !accounts.IsOpen(v.Account) {
return nil, Error{v, "account is not open"}
}
valAcc := pr.Context.ValuationAccountFor(v.Account)
p := ast.NewPostingWithTargets(valAcc, v.Account, v.Commodity, v.Amount.Sub(amts.Amount(amounts.AccountCommodityKey(v.Account, v.Commodity))), []*journal.Commodity{v.Commodity})
amts.Add(amounts.AccountCommodityKey(p.Credit, p.Commodity), p.Amount.Neg())
amts.Add(amounts.AccountCommodityKey(p.Debit, p.Commodity), p.Amount)
transactions = append(transactions, ast.TransactionBuilder{
Date: v.Date,
Description: fmt.Sprintf("Valuation adjustment for %v in %v", v.Commodity, v.Account),
Postings: []ast.Posting{p},
}.Build())
}
return transactions, nil
}
func (pr *Balancer) processAssertions(ctx context.Context, accounts accounts, amts amounts.Amounts, d *ast.Day) error {
for _, a := range d.Assertions {
if !accounts.IsOpen(a.Account) {
return Error{a, "account is not open"}
}
position := amounts.AccountCommodityKey(a.Account, a.Commodity)
if va, ok := amts[position]; !ok || !va.Equal(a.Amount) {
return Error{a, fmt.Sprintf("assertion failed: account %s has %s %s", a.Account, va, position.Commodity)}
}
}
return nil
}
func (pr *Balancer) processClosings(ctx context.Context, accounts accounts, amounts amounts.Amounts, d *ast.Day) error {
for _, c := range d.Closings {
for pos, amount := range amounts {
if pos.Account != c.Account {
continue
}
if !amount.IsZero() {
return Error{c, "account has nonzero position"}
}
delete(amounts, pos)
}
if err := accounts.Close(c.Account); err != nil {
return err
}
}
return nil
}
// accounts keeps track of open accounts.
type accounts map[*journal.Account]bool
// Open opens an account.
func (oa accounts) Open(a *journal.Account) error {
if oa[a] {
return fmt.Errorf("account %v is already open", a)
}
oa[a] = true
return nil
}
// Close closes an account.
func (oa accounts) Close(a *journal.Account) error {
if !oa[a] {
return fmt.Errorf("account %v is already closed", a)
}
delete(oa, a)
return nil
}
// IsOpen returns whether an account is open.
func (oa accounts) IsOpen(a *journal.Account) bool {
return oa[a] || a.Type() == journal.EQUITY
}