package ledger import ( "fmt" "io" "os" "sort" "unicode" ) var filesAppendDelim = "\t" type Files []string func NewFiles(p string, q ...string) (Files, error) { f := Files(append([]string{p}, q...)) _, err := f.Deltas() return f, err } func (files Files) Add(payee string, delta Delta) error { currencyValue := fmt.Sprintf("%s%.2f", delta.Currency, delta.Value) if delta.Currency != USD { currencyValue = fmt.Sprintf("%.2f %s", delta.Value, delta.Currency) } return files.append(fmt.Sprintf("%s %s\n%s%s%s%s\n%s%s", delta.Date, delta.Description, filesAppendDelim, delta.Name, filesAppendDelim+filesAppendDelim+filesAppendDelim, currencyValue, filesAppendDelim, payee, )) } func (files Files) append(s string) error { if err := files.trimTrainlingWhitespace(); err != nil { return err } f, err := os.OpenFile(string(files[0]), os.O_APPEND|os.O_CREATE|os.O_WRONLY, os.ModePerm) if err != nil { return err } defer f.Close() fmt.Fprintf(f, "\n%s", s) return f.Close() } func (files Files) trimTrainlingWhitespace() error { idx, err := files._lastNonWhitespacePos() if err != nil { return err } if idx < 1 { return nil } f, err := os.OpenFile(string(files[0]), os.O_CREATE|os.O_WRONLY, os.ModePerm) if err != nil { return err } defer f.Close() return f.Truncate(int64(idx + 1)) } func (files Files) _lastNonWhitespacePos() (int, error) { f, err := os.Open(string(files[0])) if os.IsNotExist(err) { return -1, nil } if err != nil { return -1, err } defer f.Close() b, err := io.ReadAll(f) if err != nil { return -1, err } for i := len(b) - 1; i >= 0; i-- { if !unicode.IsSpace(rune(b[i])) { return i, nil } } return len(b) - 1, nil } func (files Files) Deltas(like ...Like) (Deltas, error) { transactions, err := files.transactions() if err != nil { return nil, err } sort.Slice(transactions, func(i, j int) bool { return fmt.Sprintf("%s %s", transactions[i].date, transactions[i].description) < fmt.Sprintf("%s %s", transactions[j].date, transactions[j].description) }) result := make(Deltas, 0, len(transactions)*2) for _, transaction := range transactions { sums := map[string]float64{} for _, recipient := range transaction.recipients { sums[recipient.currency] += recipient.value delta := newDelta( transaction.date, transaction.description, recipient.name, recipient.value, recipient.currency, recipient.isSet, ) 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) } else { delta := newDelta( transaction.date, transaction.description, transaction.payee, -1.0*value, currency, false, ) result = append(result, delta) } } } balances := make(Balances) for i := range result { if result[i].isSet { var was float64 if m, ok := balances[result[i].Name]; ok { was = m[result[i].Currency] } result[i].Value = result[i].Value - was result[i].isSet = false } balances.Push(result[i]) } for i := range result { if result[i].isSet { return nil, fmt.Errorf("failed to resolve isSet: %+v", result[i]) } } return result.Like(like...), nil }