mv /ana, /ledger to /src/
This commit is contained in:
223
src/ledger/transaction.go
Normal file
223
src/ledger/transaction.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package ledger
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type transaction struct {
|
||||
date string
|
||||
description string
|
||||
payee string
|
||||
recipients []transactionRecipient
|
||||
}
|
||||
|
||||
func (t transaction) empty() bool {
|
||||
return fmt.Sprint(t) == fmt.Sprint(transaction{})
|
||||
}
|
||||
|
||||
type transactionRecipient struct {
|
||||
name string
|
||||
value float64
|
||||
currency string
|
||||
isSet bool
|
||||
}
|
||||
|
||||
func (t transactionRecipient) empty() bool {
|
||||
return t == (transactionRecipient{})
|
||||
}
|
||||
|
||||
func (files Files) transactions() ([]transaction, error) {
|
||||
result := make([]transaction, 0)
|
||||
for i := range files {
|
||||
some, err := files._transactions(files[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, some...)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (files Files) _transactions(file string) ([]transaction, error) {
|
||||
f, err := os.Open(string(file))
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
r := bufio.NewReaderSize(f, 2048)
|
||||
|
||||
result := make([]transaction, 0)
|
||||
for {
|
||||
one, err := readTransaction(r)
|
||||
if !one.empty() {
|
||||
result = append(result, one)
|
||||
}
|
||||
if err == io.EOF {
|
||||
return result, nil
|
||||
}
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readTransaction(r *bufio.Reader) (transaction, error) {
|
||||
result, err := _readTransaction(r)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if result.empty() {
|
||||
return result, nil
|
||||
}
|
||||
if result.payee == "" && len(result.recipients) < 2 {
|
||||
return result, fmt.Errorf("found a transaction with no payee and less than 2 recipeints: %+v", result)
|
||||
}
|
||||
if result.payee != "" && len(result.recipients) < 1 {
|
||||
return result, fmt.Errorf("found a transaction with payee but no recipeints: %+v", result)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func _readTransaction(r *bufio.Reader) (transaction, error) {
|
||||
readTransactionLeadingWhitespace(r)
|
||||
|
||||
firstLine, err := readTransactionLine(r)
|
||||
if len(bytes.TrimSpace(firstLine)) == 0 {
|
||||
return transaction{}, err
|
||||
}
|
||||
|
||||
dateDescriptionPattern := regexp.MustCompile(`^([0-9]+-[0-9]+-[0-9]+)\s+(.*)$`)
|
||||
dateDescriptionMatches := dateDescriptionPattern.FindAllSubmatch(firstLine, 4)
|
||||
if len(dateDescriptionMatches) != 1 {
|
||||
return transaction{}, fmt.Errorf("bad first line: %v matches: %q", len(dateDescriptionMatches), firstLine)
|
||||
} else if len(dateDescriptionMatches[0]) != 3 {
|
||||
return transaction{}, fmt.Errorf("bad first line: %v submatches: %q", len(dateDescriptionMatches[0]), firstLine)
|
||||
}
|
||||
result := transaction{
|
||||
date: string(dateDescriptionMatches[0][1]),
|
||||
description: string(dateDescriptionMatches[0][2]),
|
||||
}
|
||||
|
||||
for {
|
||||
name, value, currency, isSet, err := readTransactionName(r)
|
||||
if name != "" {
|
||||
if currency == "" {
|
||||
result.payee = name
|
||||
} else {
|
||||
result.recipients = append(result.recipients, transactionRecipient{
|
||||
name: name,
|
||||
value: value,
|
||||
currency: currency,
|
||||
isSet: isSet,
|
||||
})
|
||||
}
|
||||
}
|
||||
if name == "" || err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readTransactionLeadingWhitespace(r *bufio.Reader) {
|
||||
b, err := r.Peek(2048)
|
||||
if err != nil && err != io.EOF {
|
||||
return
|
||||
}
|
||||
|
||||
i := 0
|
||||
for i < len(b) {
|
||||
if len(bytes.TrimSpace(b[:i])) != 0 {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
r.Read(make([]byte, i-1))
|
||||
}
|
||||
}
|
||||
|
||||
func readTransactionLine(r *bufio.Reader) ([]byte, error) {
|
||||
for {
|
||||
b, err := _readTransactionLine(r)
|
||||
if err != nil || (len(bytes.TrimSpace(b)) > 0 && bytes.TrimSpace(b)[0] != '#') {
|
||||
return b, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func _readTransactionLine(r *bufio.Reader) ([]byte, error) {
|
||||
b, err := r.Peek(2048)
|
||||
if len(b) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endOfLine := len(b)
|
||||
if idx := bytes.Index(b, []byte{'\n'}); idx > -1 {
|
||||
endOfLine = idx
|
||||
}
|
||||
|
||||
b2 := make([]byte, endOfLine)
|
||||
n, err := r.Read(b2)
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
|
||||
if check, _ := r.Peek(1); len(check) == 1 && check[0] == '\n' {
|
||||
r.Read(make([]byte, 1))
|
||||
}
|
||||
|
||||
return b2[:n], err
|
||||
}
|
||||
|
||||
func readTransactionName(r *bufio.Reader) (string, float64, string, bool, error) {
|
||||
line, err := readTransactionLine(r)
|
||||
if err != nil {
|
||||
return "", 0, "", false, err
|
||||
}
|
||||
|
||||
if len(line) > 0 && !unicode.IsSpace(rune(line[0])) {
|
||||
r2 := *r
|
||||
*r = *bufio.NewReader(io.MultiReader(bytes.NewReader(append(line, '\n')), &r2))
|
||||
return "", 0, "", false, nil
|
||||
}
|
||||
|
||||
fields := bytes.Fields(line)
|
||||
|
||||
isSet := false
|
||||
if len(fields) > 2 && string(fields[1]) == "=" {
|
||||
isSet = true
|
||||
fields = append(fields[:1], fields[2:]...)
|
||||
}
|
||||
|
||||
switch len(fields) {
|
||||
case 1: // payee
|
||||
return string(fields[0]), 0, "", false, nil
|
||||
case 2: // payee $00.00
|
||||
b := bytes.TrimLeft(fields[1], "$")
|
||||
value, err := strconv.ParseFloat(string(b), 64)
|
||||
if err != nil {
|
||||
return "", 0, "", isSet, fmt.Errorf("failed to parse value from $XX.YY from %q (%q): %w", line, fields[1], err)
|
||||
}
|
||||
return string(fields[0]), value, string(USD), isSet, nil
|
||||
case 3: // payee 00.00 XYZ
|
||||
value, err := strconv.ParseFloat(string(fields[1]), 64)
|
||||
if err != nil {
|
||||
return "", 0, "", false, fmt.Errorf("failed to parse value from XX.YY XYZ from %q (%q): %w", line, fields[1], err)
|
||||
}
|
||||
return string(fields[0]), value, string(fields[2]), isSet, nil
|
||||
default:
|
||||
return "", 0, "", isSet, fmt.Errorf("cannot interpret %q", line)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user