parse ledger files
commit
12cb3b81f7
|
|
@ -0,0 +1,55 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Ledger struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func NewLedger(path string) (Ledger, error) {
|
||||
info, err := os.Stat(path)
|
||||
if err == nil && info.IsDir() {
|
||||
return Ledger{}, errors.New("path is dir")
|
||||
}
|
||||
return Ledger{
|
||||
path: path,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (ledger Ledger) Transactions() ([]Transaction, error) {
|
||||
f, err := ledger.open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
transactions := make([]Transaction, 0)
|
||||
for err == nil {
|
||||
var transaction Transaction
|
||||
transaction, err = readTransaction(f)
|
||||
if err == io.EOF {
|
||||
} else if err == nil {
|
||||
transactions = append(transactions, transaction)
|
||||
} else {
|
||||
err = fmt.Errorf("error parsing transaction #%d: %v", len(transactions), err)
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
return transactions, err
|
||||
}
|
||||
|
||||
func (ledger Ledger) open() (io.ReadCloser, error) {
|
||||
f, err := os.Open(ledger.path)
|
||||
if os.IsNotExist(err) {
|
||||
return ioutil.NopCloser(bytes.NewReader([]byte{})), nil
|
||||
}
|
||||
return f, err
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLedgerTransactions(t *testing.T) {
|
||||
ledger, err := NewLedger("./testdata/ledger.dat")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
transactions, err := ledger.Transactions()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(transactions) != 2 {
|
||||
t.Fatal(transactions)
|
||||
}
|
||||
if want := (Transaction{
|
||||
Date: "2021-07-29",
|
||||
Description: "Qt needs pizza n kodiak",
|
||||
Payee: "Withdrawal:Home:Grocery+Resturaunt:Target",
|
||||
Payer: "Debts:Credit:ChaseAarpChaseVisa",
|
||||
Amount: "$37.86",
|
||||
}); transactions[0] != want {
|
||||
t.Fatalf("want \n\t%+v, got \n\t%+v", want, transactions[0])
|
||||
}
|
||||
if want := (Transaction{
|
||||
Date: "2021-07-30",
|
||||
Description: "Testing detecting deposits via email alerts 5421705162",
|
||||
Payer: "AssetAccount:Cash:Uccu",
|
||||
Payee: "Debts:Credit:ChaseAarpChaseVisa",
|
||||
Amount: "$100.00",
|
||||
}); transactions[1] != want {
|
||||
t.Fatalf("want \n\t%+v, got \n\t%+v", want, transactions[1])
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,6 @@
|
|||
2021-07-29 Qt needs pizza n kodiak
|
||||
Debts:Credit:ChaseAarpChaseVisa $-37.86
|
||||
Withdrawal:Home:Grocery+Resturaunt:Target
|
||||
2021-07-30 Testing detecting deposits via email alerts 5421705162
|
||||
AssetAccount:Cash:Uccu $-100.00
|
||||
Debts:Credit:ChaseAarpChaseVisa
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Transaction struct {
|
||||
Date string
|
||||
Description string
|
||||
Payer string
|
||||
Payee string
|
||||
Amount string
|
||||
}
|
||||
|
||||
func readTransaction(r io.Reader) (Transaction, error) {
|
||||
lines := make([][]byte, 3)
|
||||
for i := range lines {
|
||||
line, err := nextLine(r)
|
||||
if err != nil {
|
||||
return Transaction{}, err
|
||||
}
|
||||
lines[i] = line
|
||||
}
|
||||
for i := range lines {
|
||||
if len(lines[i]) == 0 {
|
||||
return Transaction{}, fmt.Errorf("stub transaction: line %d empty: %q, %q, %q", i, lines[0], lines[1], lines[2])
|
||||
}
|
||||
}
|
||||
var transaction Transaction
|
||||
for _, foo := range []func([][]byte) error{
|
||||
transaction.readDate,
|
||||
transaction.readDescription,
|
||||
transaction.readAmount,
|
||||
transaction.readPayerPayee,
|
||||
} {
|
||||
if err := foo(lines); err != nil {
|
||||
return Transaction{}, err
|
||||
}
|
||||
}
|
||||
return transaction, nil
|
||||
}
|
||||
|
||||
func nextLine(r io.Reader) ([]byte, error) {
|
||||
line, err := readLine(r)
|
||||
for err == nil && len(bytes.TrimSpace(line)) == 0 {
|
||||
line, err = readLine(r)
|
||||
}
|
||||
return line, err
|
||||
}
|
||||
|
||||
func readLine(r io.Reader) ([]byte, error) {
|
||||
w := bytes.NewBuffer(make([]byte, 0, 128))
|
||||
buff := make([]byte, 1)
|
||||
n, err := r.Read(buff)
|
||||
for err == nil && n > 0 && buff[0] != '\n' {
|
||||
w.Write(buff)
|
||||
n, err = r.Read(buff)
|
||||
}
|
||||
return w.Bytes(), err
|
||||
}
|
||||
|
||||
func words(b []byte) [][]byte {
|
||||
scanner := bufio.NewScanner(bytes.NewReader(b))
|
||||
scanner.Split(bufio.ScanWords)
|
||||
words := make([][]byte, 0)
|
||||
for scanner.Scan() {
|
||||
words = append(words, []byte(scanner.Text()))
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
func (transaction *Transaction) readDate(lines [][]byte) error {
|
||||
transaction.Date = string(words(lines[0])[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (transaction *Transaction) readDescription(lines [][]byte) error {
|
||||
transaction.Description = string(bytes.Join(words(lines[0])[1:], []byte(" ")))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (transaction *Transaction) readPayerPayee(lines [][]byte) error {
|
||||
payer := string(words(lines[1])[0])
|
||||
payee := string(words(lines[2])[0])
|
||||
if !strings.Contains(transaction.Amount, "-") {
|
||||
tmp := payer
|
||||
payer = payee
|
||||
payee = tmp
|
||||
} else {
|
||||
transaction.Amount = strings.ReplaceAll(transaction.Amount, "-", "")
|
||||
}
|
||||
transaction.Payer = payer
|
||||
transaction.Payee = payee
|
||||
return nil
|
||||
}
|
||||
|
||||
func (transaction *Transaction) readAmount(lines [][]byte) error {
|
||||
transaction.Amount = string(words(lines[1])[1])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (transaction Transaction) Marshal() string {
|
||||
return fmt.Sprintf(
|
||||
"%-25s%s\n%25s%-50s%s\n%25s%s",
|
||||
transaction.Date,
|
||||
transaction.Description,
|
||||
"",
|
||||
transaction.Payee,
|
||||
transaction.Amount,
|
||||
"",
|
||||
transaction.Payer,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWords(t *testing.T) {
|
||||
input := `
|
||||
hello world
|
||||
i have s ome things
|
||||
`
|
||||
got := words([]byte(input))
|
||||
want := [][]byte{
|
||||
[]byte("hello"),
|
||||
[]byte("world"),
|
||||
[]byte("i"),
|
||||
[]byte("have"),
|
||||
[]byte("s"),
|
||||
[]byte("ome"),
|
||||
[]byte("things"),
|
||||
}
|
||||
if fmt.Sprint(got) != fmt.Sprint(want) {
|
||||
t.Fatal(want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLines(t *testing.T) {
|
||||
s := `
|
||||
|
||||
hello world
|
||||
`
|
||||
scanner := strings.NewReader(s)
|
||||
line, _ := nextLine(scanner)
|
||||
if strings.TrimSpace(string(line)) != "hello world" {
|
||||
t.Fatalf("%q", line)
|
||||
}
|
||||
if string(line) == strings.TrimSpace(string(line)) {
|
||||
t.Fatalf("%q", line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoReadTransaction(t *testing.T) {
|
||||
input := `
|
||||
date1 desc1
|
||||
payee1 $1.00
|
||||
payer1
|
||||
date2 desc2
|
||||
payee2 $2.00
|
||||
payer2
|
||||
`
|
||||
r := strings.NewReader(input)
|
||||
got1, _ := readTransaction(r)
|
||||
got2, _ := readTransaction(r)
|
||||
t.Logf("%+v", got1)
|
||||
t.Logf("%+v", got2)
|
||||
}
|
||||
|
||||
func TestReadTransaction(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
input string
|
||||
want Transaction
|
||||
}{
|
||||
"ez": {
|
||||
input: `
|
||||
2021-07-01 description
|
||||
|
||||
payee $1.00
|
||||
payer
|
||||
`,
|
||||
want: Transaction{
|
||||
Date: "2021-07-01",
|
||||
Description: "description",
|
||||
Payee: "payee",
|
||||
Amount: "$1.00",
|
||||
Payer: "payer",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, d := range cases {
|
||||
c := d
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := readTransaction(strings.NewReader(c.input))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != c.want {
|
||||
t.Fatalf("\n%s =!> %+v, got %+v", c.input, c.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue