// Package report aggregates transactions for the summary views. package report import ( "sort" "git.petrovv.com/nikola/money/internal/model" ) // TagTotal is one row of a spending breakdown. type TagTotal struct { Tag string Currency string In int64 // sum of inflows, positive Out int64 // sum of outflows, positive Count int Digits int } // Net is inflow minus outflow. func (t TagTotal) Net() int64 { return t.In - t.Out } // ByTag groups transactions by currency and effective tag. Untagged rows are // collected under "(untagged)" so they stay visible instead of vanishing. const Untagged = "(untagged)" // ByTag returns totals sorted by currency, then by largest outflow first. func ByTag(txns []model.Transaction) []TagTotal { type key struct{ currency, tag string } acc := map[key]*TagTotal{} for _, t := range txns { tag := t.Tag() if tag == "" { tag = Untagged } k := key{t.Currency, tag} row, ok := acc[k] if !ok { row = &TagTotal{Tag: tag, Currency: t.Currency, Digits: t.MinorDigits} acc[k] = row } if t.AmountMinor < 0 { row.Out += -t.AmountMinor } else { row.In += t.AmountMinor } row.Count++ } out := make([]TagTotal, 0, len(acc)) for _, row := range acc { out = append(out, *row) } sort.Slice(out, func(i, j int) bool { if out[i].Currency != out[j].Currency { return out[i].Currency < out[j].Currency } if out[i].Out != out[j].Out { return out[i].Out > out[j].Out } return out[i].Tag < out[j].Tag }) return out } // CurrencyTotal is the bottom line for one currency. type CurrencyTotal struct { Currency string In int64 Out int64 Digits int } // Net is inflow minus outflow. func (c CurrencyTotal) Net() int64 { return c.In - c.Out } // Totals sums the per-tag rows per currency. Currencies are never combined, // because the tool holds no exchange rates. func Totals(rows []TagTotal) []CurrencyTotal { acc := map[string]*CurrencyTotal{} for _, r := range rows { c, ok := acc[r.Currency] if !ok { c = &CurrencyTotal{Currency: r.Currency, Digits: r.Digits} acc[r.Currency] = c } c.In += r.In c.Out += r.Out } out := make([]CurrencyTotal, 0, len(acc)) for _, c := range acc { out = append(out, *c) } sort.Slice(out, func(i, j int) bool { return out[i].Currency < out[j].Currency }) return out } // Months lists the distinct YYYY-MM present in txns, most recent first. func Months(txns []model.Transaction) []string { seen := map[string]bool{} for _, t := range txns { if len(t.Date) >= 7 { seen[t.Date[:7]] = true } } out := make([]string, 0, len(seen)) for m := range seen { out = append(out, m) } sort.Sort(sort.Reverse(sort.StringSlice(out))) return out }