forked from howeyc/ledger
-
Notifications
You must be signed in to change notification settings - Fork 1
/
parse.go
189 lines (169 loc) · 4.59 KB
/
parse.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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
package ledger
import (
"bufio"
"fmt"
"io"
"math/big"
"regexp"
"sort"
"strings"
date "github.com/howeyc/ledger/internal/github.com/joyt/godate"
"github.com/marcmak/calc/calc"
)
const (
whitespace = " \t"
)
// ParseLedger parses a ledger file and returns a list of Transactions.
//
// Transactions are sorted by date.
func ParseLedger(ledgerReader io.Reader) (generalLedger []*Transaction, err error) {
parseLedger(ledgerReader, func(t *Transaction, e error) (stop bool) {
if e != nil {
err = e
stop = true
return
}
generalLedger = append(generalLedger, t)
return
})
if err != nil {
sort.Sort(sortTransactionsByDate{generalLedger})
}
return
}
// ParseLedgerAsync parses a ledger file and returns a Transaction and error channels .
//
func ParseLedgerAsync(ledgerReader io.Reader) (c chan *Transaction, e chan error) {
c = make(chan *Transaction)
e = make(chan error)
go func() {
parseLedger(ledgerReader, func(t *Transaction, err error) (stop bool) {
if err != nil {
e <- err
} else {
c <- t
}
return
})
e <- nil
}()
return c, e
}
var accountToAmountSpace = regexp.MustCompile(" {2,}|\t+")
func parseLedger(ledgerReader io.Reader, callback func(t *Transaction, err error) (stop bool)) {
var trans *Transaction
scanner := bufio.NewScanner(ledgerReader)
var line string
var filename string
var lineCount int
errorMsg := func(msg string) (stop bool) {
return callback(nil, fmt.Errorf("%s:%d: %s", filename, lineCount, msg))
}
for scanner.Scan() {
line = scanner.Text()
// update filename/line if sentinel comment is found
if strings.HasPrefix(line, markerPrefix) {
filename, lineCount = parseMarker(line)
continue
}
// remove heading and tailing space from the line
trimmedLine := strings.Trim(line, whitespace)
lineCount++
// handle comments
if commentIdx := strings.Index(trimmedLine, ";"); commentIdx >= 0 {
trimmedLine = trimmedLine[:commentIdx]
if len(trimmedLine) == 0 {
continue
}
}
if len(trimmedLine) == 0 {
if trans != nil {
transErr := balanceTransaction(trans)
if transErr != nil {
errorMsg("Unable to balance transaction, " + transErr.Error())
}
callback(trans, nil)
trans = nil
}
} else if trans == nil {
lineSplit := strings.SplitN(line, " ", 2)
if len(lineSplit) != 2 {
if errorMsg("Unable to parse payee line: " + line) {
return
}
continue
}
dateString := lineSplit[0]
transDate, dateErr := date.Parse(dateString)
if dateErr != nil {
errorMsg("Unable to parse date: " + dateString)
}
payeeString := lineSplit[1]
trans = &Transaction{Payee: payeeString, Date: transDate}
} else {
var accChange Account
lineSplit := accountToAmountSpace.Split(trimmedLine, -1)
var nonEmptyWords []string
for _, word := range lineSplit {
if len(word) > 0 {
nonEmptyWords = append(nonEmptyWords, word)
}
}
lastIndex := len(nonEmptyWords) - 1
balErr, rationalNum := getBalance(strings.Trim(nonEmptyWords[lastIndex], whitespace))
if !balErr {
// Assuming no balance and whole line is account name
accChange.Name = strings.Join(nonEmptyWords, " ")
} else {
accChange.Name = strings.Join(nonEmptyWords[:lastIndex], " ")
accChange.Balance = rationalNum
}
trans.AccountChanges = append(trans.AccountChanges, accChange)
}
}
// If the file does not end on empty line, we must attempt to balance last
// transaction of the file.
if trans != nil {
transErr := balanceTransaction(trans)
if transErr != nil {
errorMsg("Unable to balance transaction, " + transErr.Error())
}
callback(trans, nil)
}
}
func getBalance(balance string) (bool, *big.Rat) {
rationalNum := new(big.Rat)
if strings.Contains(balance, "(") {
rationalNum.SetFloat64(calc.Solve(balance))
return true, rationalNum
}
_, isValid := rationalNum.SetString(balance)
return isValid, rationalNum
}
// Takes a transaction and balances it. This is mainly to fill in the empty part
// with the remaining balance.
func balanceTransaction(input *Transaction) error {
balance := new(big.Rat)
var emptyFound bool
var emptyAccIndex int
for accIndex, accChange := range input.AccountChanges {
if accChange.Balance == nil {
if emptyFound {
return fmt.Errorf("more than one account change empty")
}
emptyAccIndex = accIndex
emptyFound = true
} else {
balance = balance.Add(balance, accChange.Balance)
}
}
if balance.Sign() != 0 {
if !emptyFound {
return fmt.Errorf("no empty account change to place extra balance")
}
}
if emptyFound {
input.AccountChanges[emptyAccIndex].Balance = balance.Neg(balance)
}
return nil
}