package ledger import ( "errors" "fmt" "io" "os" "strings" ) type File string func NewFile(p string) (File, error) { f := File(p) _, err := f.Deltas() return f, err } func (file File) Deltas(like ...Like) ([]Delta, error) { transactions, err := file.transactions() if err != nil { return nil, err } result := make([]Delta, 0, len(transactions)*2) for _, transaction := range transactions { sums := map[string]float64{} for _, acc := range transaction.recipients { sums[acc.currency] += acc.value delta := newDelta( transaction.date, transaction.description, acc.name, acc.value, acc.currency, ) if likes(like).all(delta) { result = append(result, delta) } } for currency, value := range sums { if value == 0 { continue } if transaction.payee == "" { return nil, fmt.Errorf("didnt find net zero and no dumping ground payee set: %+v", transaction) } delta := newDelta( transaction.date, transaction.description, transaction.payee, value, currency, ) if likes(like).all(delta) { result = append(result, delta) } } } return result, nil } type transaction struct { date string description string payee string recipients []transactionRecipient } type transactionRecipient struct { name string value float64 currency string } func (t transaction) empty() bool { return fmt.Sprint(t) == fmt.Sprint(transaction{}) } func (file File) transactions() ([]transaction, error) { f, err := os.Open(string(file)) if err != nil { return nil, err } defer f.Close() result := make([]transaction, 0) for { one, err := readTransaction(f) if !one.empty() { result = append(result, one) } if err == io.EOF { return result, nil } if err != nil { return result, err } } } func readTransaction(r io.Reader) (transaction, error) { result := transaction{} var err error if result.date, err = readTransactionDate(r); err != nil { return 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 transaction{}, errors.New("not impl: reading payee, recipients") } 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 { return "", err } } restOfDate := make([]byte, len("2006-01-02")-1) if _, err := r.Read(restOfDate); err != nil { return "", err } return fmt.Sprintf("%s%s", firstByte, restOfDate), nil } func readTransactionDescription(r io.Reader) (string, error) { result := make([]byte, 0, 16) var firstByte [1]byte for { if _, err := r.Read(firstByte[:]); err != nil { return "", err } if firstByte[0] == '\n' { break } result = append(result, firstByte[0]) } return strings.TrimSpace(string(result)), nil }