all repos

clerk @ e2e9e2b

missing tooling for ledger/hledger

clerk/internal/linter/rule_unbalanced_transaction.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
improve analyzer, 1 day ago
1
package linter
2
3
import (
4
	"fmt"
5
6
	"olexsmir.xyz/clerk/internal/analyzer"
7
	"olexsmir.xyz/clerk/internal/decimal"
8
	"olexsmir.xyz/clerk/journal/ast"
9
	"olexsmir.xyz/clerk/journal/token"
10
)
11
12
// UnbalancedTransaction flags transactions whose postings don't balance to zero.
13
type UnbalancedTransaction struct{}
14
15
func (UnbalancedTransaction) ID() RuleID         { return "unbalanced-transaction" }
16
func (UnbalancedTransaction) Severity() Severity { return SeverityError }
17
func (r *UnbalancedTransaction) CheckJournal(an *analyzer.Analysis) []Find {
18
	var finds []Find
19
	for _, txn := range an.Transactions {
20
		finds = append(finds, r.check(txn.Postings, txn.Span)...)
21
	}
22
	for _, ptx := range an.PeriodicTransactions {
23
		finds = append(finds, r.check(ptx.Postings, ptx.Span)...)
24
	}
25
	for _, atx := range an.AutomatedTransactions {
26
		finds = append(finds, r.check(atx.Postings, atx.Span)...)
27
	}
28
	return finds
29
}
30
31
func (r *UnbalancedTransaction) check(postings []*ast.Posting, span token.Span) []Find {
32
	var hasExpr, hasCost bool
33
	var realPostings []*ast.Posting
34
	autoBalancingPostings := 0
35
36
	for _, posting := range postings {
37
		if posting.Type != ast.PostingReal {
38
			continue
39
		}
40
		realPostings = append(realPostings, posting)
41
		if posting.Amount == nil {
42
			autoBalancingPostings++
43
		} else {
44
			if posting.Amount.IsExpr {
45
				hasExpr = true
46
			}
47
			if posting.Cost != nil {
48
				hasCost = true
49
			}
50
		}
51
	}
52
53
	if len(realPostings) == 0 ||
54
		autoBalancingPostings == 1 ||
55
		autoBalancingPostings > 1 ||
56
		hasExpr || hasCost {
57
		return nil
58
	}
59
60
	sums := make(map[string]decimal.Decimal)
61
	for _, posting := range realPostings {
62
		if posting.Amount == nil {
63
			continue
64
		}
65
		sums[posting.Amount.Commodity] = sums[posting.Amount.Commodity].Add(posting.Amount.Quantity)
66
	}
67
68
	var finds []Find
69
	for commodity, sum := range sums {
70
		if !sum.IsZero() {
71
			var msg string
72
			if commodity != "" {
73
				msg = fmt.Sprintf("transaction is unbalanced; %s balance is %s", commodity, sum.String())
74
			} else {
75
				msg = fmt.Sprintf("transaction is unbalanced; net balance is %s", sum.String())
76
			}
77
			finds = append(finds, Find{
78
				Code:     r.ID(),
79
				Severity: r.Severity(),
80
				Span:     span,
81
				Message:  msg,
82
			})
83
		}
84
	}
85
	return finds
86
}