package main import ( "bytes" "errors" "fmt" "io" "io/ioutil" "os" "path" "time" ) 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) NewTransaction() error { transactions, err := ledger.Transactions() if err != nil { return err } transactions = append(transactions, Transaction{ Date: time.Now().Format("2006-01-02"), Description: "?", Payer: "?", Payee: "?", Amount: 0, }) return ledger.SetTransactions(transactions) } func (ledger Ledger) SetTransaction(i int, transaction Transaction) error { transactions, err := ledger.Transactions() if err != nil { return err } if i >= len(transactions) { return errors.New("i too high") } transactions[i] = transaction return ledger.SetTransactions(transactions) } func (ledger Ledger) SetTransactions(transactions []Transaction) error { f, err := ioutil.TempFile(path.Dir(ledger.path), path.Base(ledger.path)+".*") if err != nil { return err } for _, transaction := range transactions { if _, err := fmt.Fprintf(f, "%s\n", transaction.Marshal()); err != nil { return err } } if err := f.Close(); err != nil { return err } return os.Rename(f.Name(), ledger.path) } func (ledger Ledger) LossyBalances() (Balances, error) { return ledger.balances(true) } func (ledger Ledger) Balances() (Balances, error) { return ledger.balances(false) } func (ledger Ledger) balances(lossy bool) (Balances, error) { transactions, err := ledger.Transactions() if !lossy && err != nil { return nil, err } balances := make(Balances) for _, transaction := range transactions { balances.Add(transaction.Payee, transaction.Amount) balances.Add(transaction.Payer, -1*transaction.Amount) } return balances, nil } func (ledger Ledger) LossyTransactions() ([]Transaction, error) { return ledger.transactions(true) } func (ledger Ledger) Transactions() ([]Transaction, error) { return ledger.transactions(false) } func (ledger Ledger) transactions(lossy bool) ([]Transaction, error) { f, err := ledger.open() if err != nil { return nil, err } defer f.Close() transactions := make([]Transaction, 0) for (!lossy && err == nil) || (lossy && err != io.EOF) { 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 || lossy { 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 }