all repos

clerk @ ab4f6083f493cb5911c86ae5f723d4519f7d52da

missing tooling for ledger/hledger

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

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