diff --git a/README.md b/README.md index 3319fc9..2f245cc 100644 --- a/README.md +++ b/README.md @@ -78,26 +78,31 @@ imported — creating an `account.toml` is not enough on its own. Run `money import` (or press `i` in the TUI) after adding one. `--uniq` turns `ls` into a list of patterns still to write rather than a list of -rows to read: one line per distinct account and description, since fifty visits -to the same shop are one glob, not fifty lines. Descriptions print exactly as a -glob sees them — upper-cased with whitespace collapsed — so two statements that -worded the same payee differently collapse into the one row they deserve, and a -pattern written from the list matches what the list showed you. +rows to read: one line per distinct description, since fifty visits to the same +shop are one glob, not fifty lines. Descriptions print exactly as a glob sees +them — upper-cased with whitespace collapsed — so two statements that worded the +same payee differently collapse into the one row they deserve, and a pattern +written from the list matches what the list showed you. ``` $ money ls --untagged --uniq -ACCOUNT DESCRIPTION -checking KAUFLAND 4412 SOFIA -checking LIDL SOFIA 4412 -savings INTEREST PAID +DESCRIPTION +INTEREST PAID +KAUFLAND 4412 SOFIA +LIDL SOFIA 4412 3 distinct descriptions ``` +The account is not part of what makes a row distinct. A rule matches on the +description and only optionally narrows to an account, so one payee seen on two +accounts is still one pattern to write — and it is one row in the rule builder +on `4`, which groups the same way. Pass `--account` to scope the listing +instead. + It composes with the other filters (`--account`, `--month`, `--search`), and `--limit` caps the rows printed, saying how many it held back. It cannot be -combined with `--wide`: a reported balance belongs to one transaction, and -there is nothing sensible to print for a whole group of them. +combined with `--wide`, whose columns all belong to a single transaction. ### TUI keys diff --git a/cmd/money/main.go b/cmd/money/main.go index cd781da..0febac6 100644 --- a/cmd/money/main.go +++ b/cmd/money/main.go @@ -270,13 +270,13 @@ func cmdLs(root string, args []string) error { untagged := fs.Bool("untagged", false, "only transactions no rule tagged and no transfer claimed") limit := fs.Int("limit", 0, "maximum rows (0 = no limit)") wide := fs.Bool("wide", false, "also show type and reported balance") - uniq := fs.Bool("uniq", false, "one row per distinct account and description") + uniq := fs.Bool("uniq", false, "one row per distinct description") if err := fs.Parse(args); err != nil { return err } if *uniq && *wide { - // Both add columns, but a reported balance belongs to one row and - // nothing sensible can be printed for a whole group of them. + // --wide's columns all belong to a single transaction, and nothing + // sensible can be printed for a whole group of them. return fmt.Errorf("--uniq and --wide cannot be combined") } @@ -332,35 +332,34 @@ func cmdLs(root string, args []string) error { return nil } -// printUniq lists each account and description once, which is the shape of the -// question "what still needs a rule?" — fifty visits to one shop are one -// pattern to write, not fifty rows to read. +// printUniq lists each description once, which is the shape of the question +// "what still needs a rule?" — fifty visits to one shop are one pattern to +// write, not fifty rows to read. +// +// The account is deliberately not part of the key. A rule matches on the +// description and only optionally narrows to an account, so the same payee +// seen on two accounts is still one pattern; splitting the row per account +// would overstate the work left and disagree with the rule builder's list, +// which groups the same way. Use --account to scope the listing instead. // // Descriptions are printed as rules.Engine matches them: normalised, since that // is the string a glob is actually tested against, so a pattern written from // this list behaves the way the list reads. It also means two statements that // differ only in spacing or case collapse to the one row they deserve. func printUniq(out io.Writer, txns []model.Transaction, limit int) error { - type row struct{ account, desc string } - seen := map[row]bool{} - var rows []row + seen := map[string]bool{} + var rows []string for _, t := range txns { - r := row{t.AccountSlug, model.NormalizeDescription(t.Description)} - if seen[r] { + desc := model.NormalizeDescription(t.Description) + if seen[desc] { continue } - seen[r] = true - rows = append(rows, r) + seen[desc] = true + rows = append(rows, desc) } - // Grouped by account and alphabetical within it: the same payee under - // slightly different wordings then lands on adjacent lines, where one glob - // covering both is easy to see. - sort.Slice(rows, func(i, j int) bool { - if rows[i].account != rows[j].account { - return rows[i].account < rows[j].account - } - return rows[i].desc < rows[j].desc - }) + // Alphabetical, so the same payee under slightly different wordings lands + // on adjacent lines, where one glob covering both is easy to see. + sort.Strings(rows) total := len(rows) if limit > 0 && len(rows) > limit { @@ -368,9 +367,9 @@ func printUniq(out io.Writer, txns []model.Transaction, limit int) error { } w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0) - fmt.Fprintln(w, "ACCOUNT\tDESCRIPTION") - for _, r := range rows { - fmt.Fprintf(w, "%s\t%s\n", r.account, r.desc) + fmt.Fprintln(w, "DESCRIPTION") + for _, desc := range rows { + fmt.Fprintln(w, desc) } w.Flush() diff --git a/cmd/money/main_test.go b/cmd/money/main_test.go index 996f5cd..c180f15 100644 --- a/cmd/money/main_test.go +++ b/cmd/money/main_test.go @@ -52,15 +52,20 @@ func TestUniqNormalisesLikeAGlob(t *testing.T) { } } -// The same description on two accounts is two rows: a rule can be scoped to an -// account, and the two may well want different tags. -func TestUniqKeepsAccountsApart(t *testing.T) { +// The account is not part of the key. A rule matches on the description and +// only optionally narrows to an account, so one payee seen on two accounts is +// still one pattern to write -- and the rule builder's list groups the same +// way, which is the list this one has to agree with. +func TestUniqIgnoresTheAccount(t *testing.T) { out := uniqOutput(t, 0, txn("checking", "TRANSFER"), txn("savings", "TRANSFER"), ) - if !strings.Contains(out, "2 distinct descriptions") { - t.Errorf("want a row per account:\n%s", out) + if !strings.Contains(out, "1 distinct descriptions") { + t.Errorf("want one row for one description:\n%s", out) + } + if strings.Contains(out, "checking") || strings.Contains(out, "savings") { + t.Errorf("the account has no column here:\n%s", out) } }