break out for take 2
parent
54febfef08
commit
d8724bb27f
264
ledger/file.go
264
ledger/file.go
|
|
@ -1,12 +1,7 @@
|
||||||
package ledger
|
package ledger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type File string
|
type File string
|
||||||
|
|
@ -59,262 +54,3 @@ func (file File) Deltas(like ...Like) ([]Delta, error) {
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t transactionRecipient) empty() bool {
|
|
||||||
return t == (transactionRecipient{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (file File) transactions() ([]transaction, error) {
|
|
||||||
f, err := os.Open(string(file))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
var r io.Reader = f
|
|
||||||
|
|
||||||
result := make([]transaction, 0)
|
|
||||||
for {
|
|
||||||
newr, one, err := readTransaction(r)
|
|
||||||
if !one.empty() {
|
|
||||||
result = append(result, one)
|
|
||||||
}
|
|
||||||
if err == io.EOF {
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
r = newr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readTransaction(r io.Reader) (io.Reader, transaction, error) {
|
|
||||||
result := transaction{}
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if r, err = readTransactionLeadingWhitespace(r); err != nil {
|
|
||||||
return r, transaction{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.date, err = readTransactionDate(r); err != nil {
|
|
||||||
if err == io.EOF {
|
|
||||||
return r, transaction{}, err
|
|
||||||
}
|
|
||||||
return nil, result, fmt.Errorf("did not find transaction date: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.description, err = readTransactionDescription(r); err != nil {
|
|
||||||
return nil, result, fmt.Errorf("did not find transaction description: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
newR, recipient, err := readTransactionRecipient(r)
|
|
||||||
r = newR
|
|
||||||
if !recipient.empty() {
|
|
||||||
if recipient.currency == "" {
|
|
||||||
result.payee = recipient.name
|
|
||||||
} else {
|
|
||||||
result.recipients = append(result.recipients, recipient)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil || recipient.empty() {
|
|
||||||
return r, result, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// from "\s*.* read \s*"
|
|
||||||
func readTransactionLeadingWhitespace(r io.Reader) (io.Reader, error) {
|
|
||||||
for {
|
|
||||||
b, err := readOne(r)
|
|
||||||
if err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
if b != 0 && !isSpaceByte(b) {
|
|
||||||
return io.MultiReader(bytes.NewReader([]byte{b}), r), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// from "20yy-mm-dd.* read "20yy-mm-dd"
|
|
||||||
func readTransactionDate(r io.Reader) (string, error) {
|
|
||||||
startOfDate := []byte{}
|
|
||||||
for len(startOfDate) == 0 {
|
|
||||||
b, err := readOne(r)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if '0' <= b && b <= '9' {
|
|
||||||
startOfDate = append(startOfDate, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
restOfDate := make([]byte, len("2006-01-02")-len(startOfDate))
|
|
||||||
if _, err := r.Read(restOfDate); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s%s", startOfDate, restOfDate), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// from "\s*.*\n.*" read \s*.*\n.*"
|
|
||||||
func readTransactionDescription(r io.Reader) (string, error) {
|
|
||||||
result := make([]byte, 0, 16)
|
|
||||||
for {
|
|
||||||
b, err := readOne(r)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if b == '\n' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
result = append(result, b)
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(string(result)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// from "\s+.*\n?.*" read "\s+.*\n?"
|
|
||||||
func readTransactionRecipient(r io.Reader) (io.Reader, transactionRecipient, error) {
|
|
||||||
if b, err := readOne(r); err != nil {
|
|
||||||
return r, transactionRecipient{}, err
|
|
||||||
} else if !isSpaceByte(b) {
|
|
||||||
return io.MultiReader(bytes.NewReader([]byte{b}), r), transactionRecipient{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := readTransactionLeadingWhitespace(r)
|
|
||||||
if err != nil {
|
|
||||||
return r, transactionRecipient{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result := transactionRecipient{}
|
|
||||||
for {
|
|
||||||
b, err := readOne(r)
|
|
||||||
if err != nil {
|
|
||||||
if result.empty() {
|
|
||||||
return nil, result, err
|
|
||||||
}
|
|
||||||
return nil, result, fmt.Errorf("failed to read transaction recipient name after skipping whitespace: %w", err)
|
|
||||||
} else if !isSpaceByte(b) {
|
|
||||||
result.name += string([]byte{b})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
r = io.MultiReader(bytes.NewReader([]byte{b}), r)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if result.name == "" {
|
|
||||||
return nil, result, fmt.Errorf("did not parse any name for transaction recipient")
|
|
||||||
}
|
|
||||||
|
|
||||||
// read "NAME:NAME", now "(\s+|\n).*"
|
|
||||||
|
|
||||||
for {
|
|
||||||
b, err := readOne(r)
|
|
||||||
if err != nil {
|
|
||||||
return r, result, err
|
|
||||||
}
|
|
||||||
if isSpaceByte(b) && b != '\n' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
r = io.MultiReader(bytes.NewReader([]byte{b}), r)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// read "\s+", now "(\n|\$1.00)"
|
|
||||||
|
|
||||||
if b, err := readOne(r); err != nil {
|
|
||||||
return r, result, err
|
|
||||||
} else if isSpaceByte(b) {
|
|
||||||
return r, result, err
|
|
||||||
} else if newr, value, currency, err := readTransactionValueCurrency(io.MultiReader(bytes.NewReader([]byte{b}), r)); err != nil {
|
|
||||||
return r, result, fmt.Errorf("failed to read transaction recipient's value and currency: %w", err)
|
|
||||||
} else {
|
|
||||||
r = newr
|
|
||||||
result.value = value
|
|
||||||
result.currency = currency
|
|
||||||
}
|
|
||||||
return r, result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func readOne(r io.Reader) (byte, error) {
|
|
||||||
var firstByte [1]byte
|
|
||||||
_, err := r.Read(firstByte[:])
|
|
||||||
return firstByte[0], err
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSpaceByte(b byte) bool {
|
|
||||||
return len(bytes.TrimSpace([]byte{b})) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func isNumericByte(b byte) bool {
|
|
||||||
return '0' <= b && b <= '9' || b == '.' || b == '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
// from "[^\n]*\n?.*" read "[^\n]*\n?"
|
|
||||||
func readTransactionValueCurrency(r io.Reader) (io.Reader, float64, string, error) {
|
|
||||||
restOfLine := make([]byte, 0, 16)
|
|
||||||
for {
|
|
||||||
b, err := readOne(r)
|
|
||||||
if b == '\n' || err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return r, 0, "", err
|
|
||||||
}
|
|
||||||
restOfLine = append(restOfLine, b)
|
|
||||||
}
|
|
||||||
restOfLine = bytes.TrimSpace(restOfLine)
|
|
||||||
if len(restOfLine) == 0 {
|
|
||||||
return r, 0, "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
firstNonNumericIdx := -1
|
|
||||||
firstNumericIdx := -1
|
|
||||||
for i := range restOfLine {
|
|
||||||
if !isNumericByte(restOfLine[i]) && firstNonNumericIdx == -1 {
|
|
||||||
firstNonNumericIdx = i
|
|
||||||
}
|
|
||||||
if isNumericByte(restOfLine[i]) && firstNumericIdx == -1 {
|
|
||||||
firstNumericIdx = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if firstNonNumericIdx == -1 || firstNumericIdx == -1 {
|
|
||||||
return r, 0, "", fmt.Errorf("needed numeric and non-numeric bytes in %q", restOfLine)
|
|
||||||
}
|
|
||||||
|
|
||||||
var valueS, currency string
|
|
||||||
if firstNonNumericIdx < firstNumericIdx {
|
|
||||||
currency = string(restOfLine[firstNonNumericIdx:firstNumericIdx])
|
|
||||||
valueS = string(restOfLine[firstNumericIdx:])
|
|
||||||
} else {
|
|
||||||
valueS = string(restOfLine[firstNumericIdx:firstNonNumericIdx])
|
|
||||||
currency = string(restOfLine[firstNonNumericIdx:])
|
|
||||||
}
|
|
||||||
valueS = strings.TrimSpace(valueS)
|
|
||||||
currency = strings.TrimSpace(currency)
|
|
||||||
|
|
||||||
value, err := strconv.ParseFloat(valueS, 64)
|
|
||||||
if err != nil {
|
|
||||||
return r, 0, "", fmt.Errorf("value %q is not a float: %w", valueS, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r, value, currency, nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
package ledger
|
package ledger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -97,61 +94,3 @@ func TestFileDeltas(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReadTransaction(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
input string
|
|
||||||
want transaction
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
"empty": {
|
|
||||||
input: "",
|
|
||||||
want: transaction{},
|
|
||||||
err: io.EOF,
|
|
||||||
},
|
|
||||||
"white space": {
|
|
||||||
input: " ",
|
|
||||||
want: transaction{},
|
|
||||||
err: io.EOF,
|
|
||||||
},
|
|
||||||
"verbose": {
|
|
||||||
input: `
|
|
||||||
2003-04-05 Reasoning here
|
|
||||||
A:B $1.00
|
|
||||||
C:D $-1.00
|
|
||||||
`,
|
|
||||||
want: transaction{
|
|
||||||
date: "2003-04-05",
|
|
||||||
description: "Reasoning here",
|
|
||||||
payee: "",
|
|
||||||
recipients: []transactionRecipient{
|
|
||||||
{
|
|
||||||
name: "A:B",
|
|
||||||
value: 1.0,
|
|
||||||
currency: "$",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "C:D",
|
|
||||||
value: -1.0,
|
|
||||||
currency: "$",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
err: io.EOF,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, d := range cases {
|
|
||||||
c := d
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
_, got, err := readTransaction(strings.NewReader(c.input))
|
|
||||||
if err != c.err {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if fmt.Sprintf("%+v", got) != fmt.Sprintf("%+v", c.want) {
|
|
||||||
t.Errorf("want\n\t%+v, got\n\t%+v", c.want, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,269 @@
|
||||||
|
package ledger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t transactionRecipient) empty() bool {
|
||||||
|
return t == (transactionRecipient{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (file File) transactions() ([]transaction, error) {
|
||||||
|
f, err := os.Open(string(file))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var r io.Reader = f
|
||||||
|
|
||||||
|
result := make([]transaction, 0)
|
||||||
|
for {
|
||||||
|
newr, one, err := readTransaction(r)
|
||||||
|
if !one.empty() {
|
||||||
|
result = append(result, one)
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
r = newr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readTransaction(r io.Reader) (io.Reader, transaction, error) {
|
||||||
|
result := transaction{}
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if r, err = readTransactionLeadingWhitespace(r); err != nil {
|
||||||
|
return r, transaction{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.date, err = readTransactionDate(r); err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
return r, transaction{}, err
|
||||||
|
}
|
||||||
|
return nil, result, fmt.Errorf("did not find transaction date: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.description, err = readTransactionDescription(r); err != nil {
|
||||||
|
return nil, result, fmt.Errorf("did not find transaction description: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
newR, recipient, err := readTransactionRecipient(r)
|
||||||
|
r = newR
|
||||||
|
if !recipient.empty() {
|
||||||
|
if recipient.currency == "" {
|
||||||
|
result.payee = recipient.name
|
||||||
|
} else {
|
||||||
|
result.recipients = append(result.recipients, recipient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil || recipient.empty() {
|
||||||
|
return r, result, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// from "\s*.* read \s*"
|
||||||
|
func readTransactionLeadingWhitespace(r io.Reader) (io.Reader, error) {
|
||||||
|
for {
|
||||||
|
b, err := readOne(r)
|
||||||
|
if err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
if b != 0 && !isSpaceByte(b) {
|
||||||
|
return io.MultiReader(bytes.NewReader([]byte{b}), r), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// from "20yy-mm-dd.* read "20yy-mm-dd"
|
||||||
|
func readTransactionDate(r io.Reader) (string, error) {
|
||||||
|
startOfDate := []byte{}
|
||||||
|
for len(startOfDate) == 0 {
|
||||||
|
b, err := readOne(r)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if '0' <= b && b <= '9' {
|
||||||
|
startOfDate = append(startOfDate, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
restOfDate := make([]byte, len("2006-01-02")-len(startOfDate))
|
||||||
|
if _, err := r.Read(restOfDate); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s%s", startOfDate, restOfDate), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// from "\s*.*\n.*" read \s*.*\n.*"
|
||||||
|
func readTransactionDescription(r io.Reader) (string, error) {
|
||||||
|
result := make([]byte, 0, 16)
|
||||||
|
for {
|
||||||
|
b, err := readOne(r)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if b == '\n' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result = append(result, b)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(result)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// from "\s+.*\n?.*" read "\s+.*\n?"
|
||||||
|
func readTransactionRecipient(r io.Reader) (io.Reader, transactionRecipient, error) {
|
||||||
|
if b, err := readOne(r); err != nil {
|
||||||
|
return r, transactionRecipient{}, err
|
||||||
|
} else if !isSpaceByte(b) {
|
||||||
|
return io.MultiReader(bytes.NewReader([]byte{b}), r), transactionRecipient{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := readTransactionLeadingWhitespace(r)
|
||||||
|
if err != nil {
|
||||||
|
return r, transactionRecipient{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := transactionRecipient{}
|
||||||
|
for {
|
||||||
|
b, err := readOne(r)
|
||||||
|
if err != nil {
|
||||||
|
if result.empty() {
|
||||||
|
return nil, result, err
|
||||||
|
}
|
||||||
|
return nil, result, fmt.Errorf("failed to read transaction recipient name after skipping whitespace: %w", err)
|
||||||
|
} else if !isSpaceByte(b) {
|
||||||
|
result.name += string([]byte{b})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r = io.MultiReader(bytes.NewReader([]byte{b}), r)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if result.name == "" {
|
||||||
|
return nil, result, fmt.Errorf("did not parse any name for transaction recipient")
|
||||||
|
}
|
||||||
|
|
||||||
|
// read "NAME:NAME", now "(\s+|\n).*"
|
||||||
|
|
||||||
|
for {
|
||||||
|
b, err := readOne(r)
|
||||||
|
if err != nil {
|
||||||
|
return r, result, err
|
||||||
|
}
|
||||||
|
if isSpaceByte(b) && b != '\n' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r = io.MultiReader(bytes.NewReader([]byte{b}), r)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// read "\s+", now "(\n|\$1.00)"
|
||||||
|
|
||||||
|
if b, err := readOne(r); err != nil {
|
||||||
|
return r, result, err
|
||||||
|
} else if isSpaceByte(b) {
|
||||||
|
return r, result, err
|
||||||
|
} else if newr, value, currency, err := readTransactionValueCurrency(io.MultiReader(bytes.NewReader([]byte{b}), r)); err != nil {
|
||||||
|
return r, result, fmt.Errorf("failed to read transaction recipient's value and currency: %w", err)
|
||||||
|
} else {
|
||||||
|
r = newr
|
||||||
|
result.value = value
|
||||||
|
result.currency = currency
|
||||||
|
}
|
||||||
|
return r, result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func readOne(r io.Reader) (byte, error) {
|
||||||
|
var firstByte [1]byte
|
||||||
|
_, err := r.Read(firstByte[:])
|
||||||
|
return firstByte[0], err
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSpaceByte(b byte) bool {
|
||||||
|
return len(bytes.TrimSpace([]byte{b})) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNumericByte(b byte) bool {
|
||||||
|
return '0' <= b && b <= '9' || b == '.' || b == '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
// from "[^\n]*\n?.*" read "[^\n]*\n?"
|
||||||
|
func readTransactionValueCurrency(r io.Reader) (io.Reader, float64, string, error) {
|
||||||
|
restOfLine := make([]byte, 0, 16)
|
||||||
|
for {
|
||||||
|
b, err := readOne(r)
|
||||||
|
if b == '\n' || err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return r, 0, "", err
|
||||||
|
}
|
||||||
|
restOfLine = append(restOfLine, b)
|
||||||
|
}
|
||||||
|
restOfLine = bytes.TrimSpace(restOfLine)
|
||||||
|
if len(restOfLine) == 0 {
|
||||||
|
return r, 0, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
firstNonNumericIdx := -1
|
||||||
|
firstNumericIdx := -1
|
||||||
|
for i := range restOfLine {
|
||||||
|
if !isNumericByte(restOfLine[i]) && firstNonNumericIdx == -1 {
|
||||||
|
firstNonNumericIdx = i
|
||||||
|
}
|
||||||
|
if isNumericByte(restOfLine[i]) && firstNumericIdx == -1 {
|
||||||
|
firstNumericIdx = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if firstNonNumericIdx == -1 || firstNumericIdx == -1 {
|
||||||
|
return r, 0, "", fmt.Errorf("needed numeric and non-numeric bytes in %q", restOfLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
var valueS, currency string
|
||||||
|
if firstNonNumericIdx < firstNumericIdx {
|
||||||
|
currency = string(restOfLine[firstNonNumericIdx:firstNumericIdx])
|
||||||
|
valueS = string(restOfLine[firstNumericIdx:])
|
||||||
|
} else {
|
||||||
|
valueS = string(restOfLine[firstNumericIdx:firstNonNumericIdx])
|
||||||
|
currency = string(restOfLine[firstNonNumericIdx:])
|
||||||
|
}
|
||||||
|
valueS = strings.TrimSpace(valueS)
|
||||||
|
currency = strings.TrimSpace(currency)
|
||||||
|
|
||||||
|
value, err := strconv.ParseFloat(valueS, 64)
|
||||||
|
if err != nil {
|
||||||
|
return r, 0, "", fmt.Errorf("value %q is not a float: %w", valueS, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, value, currency, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
package ledger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadTransaction(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
input string
|
||||||
|
want transaction
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
"empty": {
|
||||||
|
input: "",
|
||||||
|
want: transaction{},
|
||||||
|
err: io.EOF,
|
||||||
|
},
|
||||||
|
"white space": {
|
||||||
|
input: " ",
|
||||||
|
want: transaction{},
|
||||||
|
err: io.EOF,
|
||||||
|
},
|
||||||
|
"verbose": {
|
||||||
|
input: `
|
||||||
|
2003-04-05 Reasoning here
|
||||||
|
A:B $1.00
|
||||||
|
C:D $-1.00
|
||||||
|
`,
|
||||||
|
want: transaction{
|
||||||
|
date: "2003-04-05",
|
||||||
|
description: "Reasoning here",
|
||||||
|
payee: "",
|
||||||
|
recipients: []transactionRecipient{
|
||||||
|
{
|
||||||
|
name: "A:B",
|
||||||
|
value: 1.0,
|
||||||
|
currency: "$",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "C:D",
|
||||||
|
value: -1.0,
|
||||||
|
currency: "$",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: io.EOF,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
c := d
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
_, got, err := readTransaction(strings.NewReader(c.input))
|
||||||
|
if err != c.err {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fmt.Sprintf("%+v", got) != fmt.Sprintf("%+v", c.want) {
|
||||||
|
t.Errorf("want\n\t%+v, got\n\t%+v", c.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue