diff --git a/ledger/file.go b/ledger/file.go index d8e8c04..dd6e72f 100644 --- a/ledger/file.go +++ b/ledger/file.go @@ -1,6 +1,7 @@ package ledger import ( + "bytes" "errors" "fmt" "io" @@ -66,14 +67,18 @@ type transaction struct { 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 transaction) empty() bool { - return fmt.Sprint(t) == fmt.Sprint(transaction{}) +func (t transactionRecipient) empty() bool { + return t == (transactionRecipient{}) } func (file File) transactions() ([]transaction, error) { @@ -83,9 +88,11 @@ func (file File) transactions() ([]transaction, error) { } defer f.Close() + var r io.Reader = f + result := make([]transaction, 0) for { - one, err := readTransaction(f) + newr, one, err := readTransaction(r) if !one.empty() { result = append(result, one) } @@ -95,51 +102,161 @@ func (file File) transactions() ([]transaction, error) { if err != nil { return result, err } + r = newr } } -func readTransaction(r io.Reader) (transaction, error) { +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 { - return result, fmt.Errorf("did not find transaction date: %w", err) + 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 result, fmt.Errorf("did not find transaction description: %w", err) + return nil, result, fmt.Errorf("did not find transaction description: %w", err) } - return transaction{}, errors.New("not impl: reading payee, recipients") + for { + newR, recipient, err := readTransactionRecipient(r) + r = newR + if !recipient.empty() { + 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) { - var firstByte [1]byte - for firstByte[0] < '0' || firstByte[0] > '9' { - if _, err := r.Read(firstByte[:]); err != nil { + 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")-1) + restOfDate := make([]byte, len("2006-01-02")-len(startOfDate)) if _, err := r.Read(restOfDate); err != nil { return "", err } - return fmt.Sprintf("%s%s", firstByte, restOfDate), nil + 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) - var firstByte [1]byte for { - if _, err := r.Read(firstByte[:]); err != nil { + b, err := readOne(r) + if err != nil { return "", err } - if firstByte[0] == '\n' { + if b == '\n' { break } - result = append(result, firstByte[0]) + 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 r, transactionRecipient{}, fmt.Errorf("didnt find leading whitespace for transaction recipient") + } + + 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 + } + break + } + if result.name == "" { + return nil, result, fmt.Errorf("did not parse any name for transaction recipient") + } + + for { + b, err := readOne(r) + if err != nil { + return r, result, err + } + if isSpaceByte(b) && b != '\n' { + continue + } + break + } + + 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 == '-' +} + +func readTransactionValueCurrency(r io.Reader) (io.Reader, float64, string, error) { + return nil, 0, "", errors.New("not impl read a value currency") +} diff --git a/ledger/file_test.go b/ledger/file_test.go index 5e431c1..dbb5752 100644 --- a/ledger/file_test.go +++ b/ledger/file_test.go @@ -115,7 +115,7 @@ func TestReadTransaction(t *testing.T) { for name, d := range cases { c := d t.Run(name, func(t *testing.T) { - got, err := readTransaction(strings.NewReader(c.input)) + _, got, err := readTransaction(strings.NewReader(c.input)) if err != c.err { t.Error(err) }