58 files changed,
3041 insertions(+),
642 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-07-29 19:32:29 +0300
Authored at:
2026-07-28 13:45:51 +0300
Change ID:
lnrnoykrpskukmvkrxspzzosytmwnmwx
Parent:
a1448b5
jump to
A
internal/analyzer/analyzer.go
··· 1 +package analyzer 2 + 3 +import ( 4 + "olexsmir.xyz/clerk/journal" 5 + "olexsmir.xyz/clerk/journal/ast" 6 +) 7 + 8 +type Analysis struct { 9 + Files []*journal.ParsedFile 10 + 11 + Transactions []*ast.Transaction 12 + PeriodicTransactions []*ast.PeriodicTransaction 13 + AutomatedTransactions []*ast.AutomatedTransaction 14 + Directives []ast.Entry // account, commodity, payee, etc 15 + 16 + Accounts map[string]*AccountInfo 17 + Commodities map[string]*CommodityInfo 18 + Payees map[string]*PayeeInfo 19 + 20 + AccountNames []string // sorted for binary search 21 + PayeeNames []string // sorted, all payee names from directives + usage 22 + AccountsByPrefix map[string][]string // "expenses:" -> ["expenses:food", "expenses:taxi"] 23 + 24 + // PayeeTemplates holds the last transaction's postings per payee name. 25 + PayeeTemplates map[string][]PostingTemplate 26 + 27 + TagNames []string // unique, from tag directives 28 + 29 + Dates []ast.Date // unique transaction dats in sorted order 30 + DateStrings []string // same order as Dates, "YYYY-MM-DD" 31 + 32 + // CommodityDecimalMarks decimal mark per commodity, detected from D/amounts. 33 + CommodityDecimalMarks map[string]byte 34 + 35 + // TransactionsByKey groups transactions by [TxDuplicateKey] signature. 36 + TransactionsByKey map[string][]*ast.Transaction 37 +} 38 + 39 +type AccountInfo struct { 40 + Directives []*ast.AccountDirective 41 + Usages []AccountUsage 42 + UsedCount int 43 + LastUsed ast.Date 44 +} 45 + 46 +type AccountUsage struct { 47 + FileIndex int 48 + Posting *ast.Posting 49 +} 50 + 51 +type CommodityInfo struct { 52 + Directives []*ast.CommodityDirective 53 + Usages []CommodityUsage 54 + UsedCount int 55 + LastUsed ast.Date 56 +} 57 + 58 +type CommodityUsage struct { 59 + FileIndex int 60 + Amount *ast.Amount 61 +} 62 + 63 +type PayeeInfo struct { 64 + Directives []*ast.Payee 65 + Usage []PayeeUsage 66 + UsedCount int 67 + LastUsed ast.Date 68 +} 69 + 70 +type PayeeUsage struct { 71 + FileIndex int 72 + Payee *ast.Payee 73 +} 74 + 75 +type PostingTemplate struct { 76 + Account string 77 + Amount string 78 + Commodity string 79 + IsInferred bool // true if the amount was inferred (auto-balanced) 80 +}
A
internal/analyzer/analyzer_test.go
··· 1 +package analyzer 2 + 3 +import ( 4 + "fmt" 5 + "io" 6 + "path/filepath" 7 + "sort" 8 + "strings" 9 + "testing" 10 + 11 + "olexsmir.xyz/clerk/internal/testutil/golden" 12 + "olexsmir.xyz/clerk/journal" 13 + "olexsmir.xyz/clerk/journal/ast" 14 +) 15 + 16 +func TestAnalysis(t *testing.T) { 17 + tests := []string{ 18 + "empty", 19 + "include", 20 + "journal", 21 + "typed-entries", 22 + "account-indexing", 23 + "account-usage", 24 + "commodity-usage", 25 + "payees", 26 + "periodic-automated", 27 + "directives", 28 + "cost-balance", 29 + "posting-types", 30 + "dates", 31 + "tags", 32 + "decimal-marks", 33 + "duplicate-transactions", 34 + } 35 + 36 + for _, tname := range tests { 37 + t.Run(tname, func(t *testing.T) { 38 + a := golden.Read(t, tname) 39 + fsys, err := a.FS() 40 + if err != nil { 41 + t.Fatal(err) 42 + } 43 + 44 + l := journal.NewLoader() 45 + if _, err := l.LoadFS(fsys, "in.journal"); err != nil { 46 + t.Fatalf("failed to load test journal: %v", err) 47 + } 48 + 49 + ctx := Build(l.Ordered()) 50 + var b strings.Builder 51 + fprint(&b, ctx) 52 + golden.Assert(t, a, b.String()) 53 + }) 54 + } 55 +} 56 + 57 +func fprint(w io.Writer, a *Analysis) { 58 + fmt.Fprintf(w, "files (%d):\n", len(a.Files)) 59 + for i, pf := range a.Files { 60 + fmt.Fprintf(w, " %d: %s\n", i, filepath.Base(pf.Path)) 61 + } 62 + 63 + // commodities 64 + csyms := make([]string, 0, len(a.Commodities)) 65 + for sym := range a.Commodities { 66 + csyms = append(csyms, sym) 67 + } 68 + sort.Strings(csyms) 69 + 70 + fmt.Fprintf(w, "\ncommodities (%d):\n", len(a.Commodities)) 71 + for _, sym := range csyms { 72 + info := a.Commodities[sym] 73 + fmt.Fprintf(w, " %s\n", sym) 74 + fmt.Fprintf(w, " directives: %d\n", len(info.Directives)) 75 + fmt.Fprintf(w, " used: %d\n", info.UsedCount) 76 + for _, cd := range info.Directives { 77 + fmt.Fprintf(w, " commodity %s\n", cd.Commodity) 78 + } 79 + fmt.Fprintf(w, " usages: %d\n", len(info.Usages)) 80 + for _, u := range info.Usages { 81 + fmt.Fprintf(w, " file %d: %s (%q)\n", u.FileIndex, u.Amount.Commodity, u.Amount.Quantity.String()) 82 + } 83 + if info.LastUsed.Year != 0 { 84 + fmt.Fprintf(w, " last-used: %d-%02d-%02d\n", info.LastUsed.Year, info.LastUsed.Month, info.LastUsed.Day) 85 + } 86 + } 87 + 88 + // accounts 89 + anames := make([]string, 0, len(a.Accounts)) 90 + for name := range a.Accounts { 91 + anames = append(anames, name) 92 + } 93 + sort.Strings(anames) 94 + 95 + fmt.Fprintf(w, "\naccounts (%d):\n", len(a.Accounts)) 96 + for _, name := range anames { 97 + info := a.Accounts[name] 98 + fmt.Fprintf(w, " %s\n", name) 99 + fmt.Fprintf(w, " directives: %d\n", len(info.Directives)) 100 + fmt.Fprintf(w, " used: %d\n", info.UsedCount) 101 + for _, d := range info.Directives { 102 + fmt.Fprintf(w, " account %s\n", d.Account.String()) 103 + } 104 + fmt.Fprintf(w, " usages: %d\n", len(info.Usages)) 105 + for _, u := range info.Usages { 106 + fmt.Fprintf(w, " file %d: %s\n", u.FileIndex, u.Posting.Account.String()) 107 + } 108 + if info.LastUsed.Year != 0 { 109 + fmt.Fprintf(w, " last-used: %d-%02d-%02d\n", info.LastUsed.Year, info.LastUsed.Month, info.LastUsed.Day) 110 + } 111 + } 112 + 113 + // payees 114 + pnames := make([]string, 0, len(a.Payees)) 115 + for name := range a.Payees { 116 + pnames = append(pnames, name) 117 + } 118 + sort.Strings(pnames) 119 + 120 + fmt.Fprintf(w, "\npayees (%d):\n", len(a.Payees)) 121 + for _, name := range pnames { 122 + info := a.Payees[name] 123 + fmt.Fprintf(w, " %s\n", name) 124 + fmt.Fprintf(w, " directives: %d\n", len(info.Directives)) 125 + fmt.Fprintf(w, " used: %d\n", info.UsedCount) 126 + for _, d := range info.Directives { 127 + fmt.Fprintf(w, " payee %s\n", d.Name) 128 + } 129 + fmt.Fprintf(w, " usages: %d\n", len(info.Usage)) 130 + for _, u := range info.Usage { 131 + fmt.Fprintf(w, " file %d: %s\n", u.FileIndex, u.Payee.Name) 132 + } 133 + if info.LastUsed.Year != 0 { 134 + fmt.Fprintf(w, " last-used: %d-%02d-%02d\n", info.LastUsed.Year, info.LastUsed.Month, info.LastUsed.Day) 135 + } 136 + } 137 + 138 + // prefix index 139 + prefixes := make([]string, 0, len(a.AccountsByPrefix)) 140 + for p := range a.AccountsByPrefix { 141 + prefixes = append(prefixes, p) 142 + } 143 + sort.Strings(prefixes) 144 + fmt.Fprintf(w, "\nprefixes (%d):\n", len(a.AccountsByPrefix)) 145 + for _, p := range prefixes { 146 + names := a.AccountsByPrefix[p] 147 + sort.Strings(names) 148 + fmt.Fprintf(w, " %s\n", p) 149 + for _, n := range names { 150 + fmt.Fprintf(w, " %s\n", n) 151 + } 152 + } 153 + 154 + // typed entry counts 155 + fmt.Fprintf(w, "\ntransactions: %d\n", len(a.Transactions)) 156 + fmt.Fprintf(w, "periodic transactions: %d\n", len(a.PeriodicTransactions)) 157 + fmt.Fprintf(w, "automated transactions: %d\n", len(a.AutomatedTransactions)) 158 + 159 + // directives 160 + fmt.Fprintf(w, "\ndirectives (%d):\n", len(a.Directives)) 161 + for _, d := range a.Directives { 162 + switch d := d.(type) { 163 + case *ast.AccountDirective: 164 + fmt.Fprintf(w, " account %s\n", d.Account.String()) 165 + case *ast.CommodityDirective: 166 + fmt.Fprintf(w, " commodity %s\n", d.Commodity) 167 + case *ast.PayeeDirective: 168 + fmt.Fprintf(w, " payee %s\n", d.Name) 169 + case *ast.TagDirective: 170 + fmt.Fprintf(w, " tag %s\n", d.Name) 171 + case *ast.IncludeDirective: 172 + fmt.Fprintf(w, " include %s\n", d.Path) 173 + case *ast.AliasDirective: 174 + fmt.Fprintf(w, " alias %s = %s\n", d.From.String(), d.To.String()) 175 + case *ast.YearDirective: 176 + fmt.Fprintf(w, " year %d\n", d.Year) 177 + case *ast.DecimalMarkDirective: 178 + fmt.Fprintf(w, " decimal-mark %c\n", d.Mark) 179 + case *ast.DefaultCommodityDirective: 180 + fmt.Fprintf(w, " D %s%s\n", d.Amount.Commodity, d.Amount.Quantity.String()) 181 + case *ast.MarketPriceDirective: 182 + fmt.Fprintf(w, " P %s %s\n", d.Commodity, d.Amount.Quantity.String()) 183 + case *ast.ConversionDirective: 184 + fmt.Fprintf(w, " C %s%s @@ %s%s\n", d.From.Commodity, d.From.Quantity.String(), d.To.Commodity, d.To.Quantity.String()) 185 + case *ast.ApplyDirective: 186 + fmt.Fprintf(w, " apply %s\n", d.Expr) 187 + case *ast.EndDirective: 188 + fmt.Fprintf(w, " end %s\n", d.Expr) 189 + case *ast.CommentBlockDirective: 190 + fmt.Fprintf(w, " comment\n") 191 + case *ast.IgnoredDirective: 192 + fmt.Fprintf(w, " N %s\n", d.Text) 193 + } 194 + } 195 + 196 + // transaction duplicate keys 197 + keys := make([]string, 0, len(a.TransactionsByKey)) 198 + for k := range a.TransactionsByKey { 199 + keys = append(keys, k) 200 + } 201 + sort.Strings(keys) 202 + fmt.Fprintf(w, "\ntransaction keys (%d):\n", len(a.TransactionsByKey)) 203 + for _, k := range keys { 204 + txs := a.TransactionsByKey[k] 205 + if len(txs) > 1 { 206 + fmt.Fprintf(w, " %s (dup: %d)\n", k, len(txs)) 207 + } else { 208 + fmt.Fprintf(w, " %s\n", k) 209 + } 210 + } 211 + 212 + // payee templates 213 + payeeNames := make([]string, 0, len(a.PayeeTemplates)) 214 + for name := range a.PayeeTemplates { 215 + payeeNames = append(payeeNames, name) 216 + } 217 + sort.Strings(payeeNames) 218 + fmt.Fprintf(w, "\npayee templates (%d):\n", len(a.PayeeTemplates)) 219 + for _, name := range payeeNames { 220 + templates := a.PayeeTemplates[name] 221 + fmt.Fprintf(w, " %s\n", name) 222 + for _, t := range templates { 223 + fmt.Fprintf(w, " %s", t.Account) 224 + if t.Amount != "" { 225 + fmt.Fprintf(w, " %s%s", t.Commodity, t.Amount) 226 + } 227 + if t.IsInferred { 228 + fmt.Fprintf(w, " (inferred)") 229 + } 230 + fmt.Fprintln(w) 231 + } 232 + } 233 + 234 + // dates 235 + fmt.Fprintf(w, "\ndates (%d):\n", len(a.Dates)) 236 + for _, d := range a.Dates { 237 + fmt.Fprintf(w, " %d-%02d-%02d\n", d.Year, d.Month, d.Day) 238 + } 239 + 240 + // tag names 241 + fmt.Fprintf(w, "\ntag names (%d):\n", len(a.TagNames)) 242 + for _, t := range a.TagNames { 243 + fmt.Fprintf(w, " %s\n", t) 244 + } 245 + 246 + // commodity decimal marks 247 + csyms2 := make([]string, 0, len(a.CommodityDecimalMarks)) 248 + for sym := range a.CommodityDecimalMarks { 249 + csyms2 = append(csyms2, sym) 250 + } 251 + sort.Strings(csyms2) 252 + fmt.Fprintf(w, "\ncommodity decimal marks (%d):\n", len(a.CommodityDecimalMarks)) 253 + for _, sym := range csyms2 { 254 + fmt.Fprintf(w, " %s: %c\n", sym, a.CommodityDecimalMarks[sym]) 255 + } 256 +}
A
internal/analyzer/benchmark_test.go
··· 1 +package analyzer 2 + 3 +import ( 4 + "testing" 5 + 6 + "olexsmir.xyz/clerk/journal" 7 +) 8 + 9 +func BenchmarkBuild_BasicJournal(b *testing.B) { 10 + files := loadJournal("../../journal/testdata/journals/basic.journal") 11 + b.ResetTimer() 12 + for b.Loop() { 13 + Build(files) 14 + } 15 +} 16 + 17 +func BenchmarkBuild_1kTxns(b *testing.B) { 18 + files := loadJournal("../../journal/testdata/journals/actual-1ktxns-100accts.journal") 19 + b.ResetTimer() 20 + for b.Loop() { 21 + Build(files) 22 + } 23 +} 24 + 25 +func BenchmarkBuild_Standard(b *testing.B) { 26 + files := loadJournal("../../journal/testdata/journals/actual-ledger-input-standard.dat") 27 + b.ResetTimer() 28 + for b.Loop() { 29 + Build(files) 30 + } 31 +} 32 + 33 +func BenchmarkBuild_Wow(b *testing.B) { 34 + files := loadJournal("../../journal/testdata/journals/actual-ledger-input-wow.dat") 35 + b.ResetTimer() 36 + for b.Loop() { 37 + Build(files) 38 + } 39 +} 40 + 41 +func BenchmarkBuild_1kTxns_Allocs(b *testing.B) { 42 + files := loadJournal("../../journal/testdata/journals/actual-1ktxns-100accts.journal") 43 + b.ResetTimer() 44 + b.ReportAllocs() 45 + for b.Loop() { 46 + Build(files) 47 + } 48 +} 49 + 50 +func BenchmarkBuild_Parallel_1kTxns(b *testing.B) { 51 + files := loadJournal("../../journal/testdata/journals/actual-1ktxns-100accts.journal") 52 + b.RunParallel(func(pb *testing.PB) { 53 + for pb.Next() { 54 + Build(files) 55 + } 56 + }) 57 +} 58 + 59 +func BenchmarkPrefixLookup_1kTxns(b *testing.B) { 60 + a := Build(loadJournal("../../journal/testdata/journals/actual-1ktxns-100accts.journal")) 61 + prefixes := make([]string, 0, len(a.AccountsByPrefix)) 62 + for p := range a.AccountsByPrefix { 63 + prefixes = append(prefixes, p) 64 + } 65 + b.ResetTimer() 66 + for b.Loop() { 67 + for _, p := range prefixes { 68 + _ = a.AccountsByPrefix[p] 69 + } 70 + } 71 +} 72 + 73 +func BenchmarkAccountIteration_1kTxns(b *testing.B) { 74 + a := Build(loadJournal("../../journal/testdata/journals/actual-1ktxns-100accts.journal")) 75 + names := a.AccountNames 76 + b.ResetTimer() 77 + for b.Loop() { 78 + for _, name := range names { 79 + _ = a.Accounts[name].UsedCount 80 + } 81 + } 82 +} 83 + 84 +func loadJournal(path string) []*journal.ParsedFile { 85 + ldr := journal.NewLoader() 86 + if _, err := ldr.Load(path); err != nil { 87 + panic("loadJournal: " + err.Error()) 88 + } 89 + return ldr.Ordered() 90 +}
A
internal/analyzer/build.go
··· 1 +package analyzer 2 + 3 +import ( 4 + "fmt" 5 + "sort" 6 + "strings" 7 + 8 + "olexsmir.xyz/clerk/journal" 9 + "olexsmir.xyz/clerk/journal/ast" 10 +) 11 + 12 +// Build constructs [Analysis] from a list of parsed files. 13 +// Files should be in dependency order (includes before includers). 14 +func Build(files []*journal.ParsedFile) *Analysis { 15 + a := &Analysis{ 16 + Files: files, 17 + Accounts: make(map[string]*AccountInfo), 18 + Commodities: make(map[string]*CommodityInfo), 19 + Payees: make(map[string]*PayeeInfo), 20 + AccountsByPrefix: make(map[string][]string), 21 + PayeeTemplates: make(map[string][]PostingTemplate), 22 + CommodityDecimalMarks: make(map[string]byte), 23 + TransactionsByKey: make(map[string][]*ast.Transaction), 24 + } 25 + for i, pf := range a.Files { 26 + for _, entry := range pf.Ast.Entries { 27 + a.addEntry(i, entry) 28 + } 29 + } 30 + a.buildPrefixIndex() 31 + a.sortAccountNames() 32 + a.collectPayeeNames() 33 + a.collectDates() 34 + a.collectTagNames() 35 + return a 36 +} 37 + 38 +func TxDuplicateKey(tx *ast.Transaction) string { 39 + var b strings.Builder 40 + fmt.Fprintf(&b, "%04d-%02d-%02d|", tx.Date.Year, tx.Date.Month, tx.Date.Day) 41 + if tx.Payee != nil { 42 + b.WriteString(tx.Payee.Name) 43 + } 44 + b.WriteByte('|') 45 + for _, p := range tx.Postings { 46 + b.WriteString(p.Account.String()) 47 + b.WriteByte(',') 48 + } 49 + return b.String() 50 +} 51 + 52 +func (a *Analysis) addEntry(fileIndex int, entry ast.Entry) { 53 + switch e := entry.(type) { 54 + case *ast.AccountDirective: 55 + a.addAccountDirective(e) 56 + case *ast.CommodityDirective: 57 + a.addCommodityDirective(e) 58 + case *ast.PayeeDirective: 59 + a.addPayeeDirective(e) 60 + case *ast.Transaction: 61 + a.Transactions = append(a.Transactions, e) 62 + a.addPostings(fileIndex, e.Postings, &e.Date) 63 + a.addPayee(fileIndex, e.Payee) 64 + a.addPayeeTemplate(e) 65 + key := TxDuplicateKey(e) 66 + a.TransactionsByKey[key] = append(a.TransactionsByKey[key], e) 67 + case *ast.PeriodicTransaction: 68 + a.PeriodicTransactions = append(a.PeriodicTransactions, e) 69 + a.addPostings(fileIndex, e.Postings, nil) 70 + case *ast.AutomatedTransaction: 71 + a.AutomatedTransactions = append(a.AutomatedTransactions, e) 72 + a.addPostings(fileIndex, e.Postings, nil) 73 + case *ast.DecimalMarkDirective: 74 + // Collect commodity decimal marks from directives with explicit format, 75 + // but DecimalMarkDirective sets the default for all commodities. 76 + // We don't track a "default" — individual commodities get theirs from amount formatting. 77 + case *ast.DefaultCommodityDirective: 78 + if e.Amount.Commodity != "" { 79 + mark := e.Amount.QuantityFmt.Decimal 80 + if mark != 0 { 81 + a.CommodityDecimalMarks[e.Amount.Commodity] = mark 82 + } 83 + } 84 + } 85 + 86 + // Every directive-like entry goes into Directives. 87 + switch entry.(type) { 88 + case *ast.AccountDirective, *ast.CommodityDirective, *ast.PayeeDirective, *ast.TagDirective, *ast.IncludeDirective, 89 + *ast.AliasDirective, *ast.YearDirective, *ast.DecimalMarkDirective, *ast.DefaultCommodityDirective, *ast.MarketPriceDirective, 90 + *ast.ConversionDirective, *ast.ApplyDirective, *ast.EndDirective, *ast.CommentBlockDirective, *ast.IgnoredDirective: 91 + a.Directives = append(a.Directives, entry) 92 + } 93 +} 94 + 95 +func (a *Analysis) addAccountDirective(ad *ast.AccountDirective) { 96 + aname := ad.Account.String() 97 + info, ok := a.Accounts[aname] 98 + if !ok { 99 + info = &AccountInfo{} 100 + a.Accounts[aname] = info 101 + } 102 + info.Directives = append(info.Directives, ad) 103 +} 104 + 105 +func (a *Analysis) addPayeeDirective(pd *ast.PayeeDirective) { 106 + info, ok := a.Payees[pd.Name] 107 + if !ok { 108 + info = &PayeeInfo{} 109 + a.Payees[pd.Name] = info 110 + } 111 + info.Directives = append(info.Directives, &ast.Payee{ 112 + Name: pd.Name, 113 + Span: pd.Span, 114 + }) 115 +} 116 + 117 +func (a *Analysis) addCommodityDirective(cd *ast.CommodityDirective) { 118 + info, ok := a.Commodities[cd.Commodity] 119 + if !ok { 120 + info = &CommodityInfo{} 121 + a.Commodities[cd.Commodity] = info 122 + } 123 + info.Directives = append(info.Directives, cd) 124 +} 125 + 126 +func (a *Analysis) addPayee(fileIndex int, payee *ast.Payee) { 127 + if payee == nil { 128 + return 129 + } 130 + info, ok := a.Payees[payee.Name] 131 + if !ok { 132 + info = &PayeeInfo{} 133 + a.Payees[payee.Name] = info 134 + } 135 + info.Usage = append(info.Usage, PayeeUsage{ 136 + FileIndex: fileIndex, 137 + Payee: payee, 138 + }) 139 + info.UsedCount++ 140 +} 141 + 142 +func (a *Analysis) addPostings(fileIndex int, postings []*ast.Posting, date *ast.Date) { 143 + for _, posting := range postings { 144 + aname := posting.Account.String() 145 + info, ok := a.Accounts[aname] 146 + if !ok { 147 + info = &AccountInfo{} 148 + a.Accounts[aname] = info 149 + } 150 + info.Usages = append(info.Usages, AccountUsage{ 151 + FileIndex: fileIndex, 152 + Posting: posting, 153 + }) 154 + info.UsedCount++ 155 + if date != nil { 156 + info.LastUsed = maxDate(info.LastUsed, *date) 157 + } 158 + 159 + a.addCommodityUsage(fileIndex, posting.Amount, date) 160 + if posting.Cost != nil { 161 + a.addCommodityUsage(fileIndex, &posting.Cost.Amount, date) 162 + } 163 + if posting.Balance != nil { 164 + a.addCommodityUsage(fileIndex, &posting.Balance.Amount, date) 165 + if posting.Balance.Cost != nil { 166 + a.addCommodityUsage(fileIndex, &posting.Balance.Cost.Amount, date) 167 + } 168 + } 169 + 170 + // Collect decimal mark from amount formatting. 171 + if posting.Amount != nil && posting.Amount.Commodity != "" && posting.Amount.QuantityFmt.Decimal != 0 { 172 + if _, ok := a.CommodityDecimalMarks[posting.Amount.Commodity]; !ok { 173 + a.CommodityDecimalMarks[posting.Amount.Commodity] = posting.Amount.QuantityFmt.Decimal 174 + } 175 + } 176 + } 177 +} 178 + 179 +func (a *Analysis) addPayeeTemplate(tx *ast.Transaction) { 180 + payee := "" 181 + if tx.Payee != nil { 182 + payee = tx.Payee.Name 183 + } 184 + if payee == "" { 185 + return 186 + } 187 + 188 + templates := make([]PostingTemplate, len(tx.Postings)) 189 + for i, p := range tx.Postings { 190 + t := PostingTemplate{ 191 + Account: p.Account.String(), 192 + IsInferred: p.Amount == nil, 193 + } 194 + if p.Amount != nil { 195 + t.Amount = p.Amount.Quantity.String() 196 + t.Commodity = p.Amount.Commodity 197 + } 198 + templates[i] = t 199 + } 200 + a.PayeeTemplates[payee] = templates 201 +} 202 + 203 +func (a *Analysis) collectDates() { 204 + seen := make(map[string]bool) 205 + for _, tx := range a.Transactions { 206 + s := formatDate(tx.Date) 207 + if s != "" && !seen[s] { 208 + seen[s] = true 209 + a.Dates = append(a.Dates, tx.Date) 210 + a.DateStrings = append(a.DateStrings, s) 211 + } 212 + } 213 + sort.Slice(a.Dates, func(i, j int) bool { 214 + return dateLess(a.Dates[i], a.Dates[j]) 215 + }) 216 + sort.Strings(a.DateStrings) 217 +} 218 + 219 +func (a *Analysis) collectTagNames() { 220 + seen := make(map[string]bool) 221 + for _, d := range a.Directives { 222 + if td, ok := d.(*ast.TagDirective); ok && td.Name != "" && !seen[td.Name] { 223 + seen[td.Name] = true 224 + a.TagNames = append(a.TagNames, td.Name) 225 + } 226 + } 227 + sort.Strings(a.TagNames) 228 +} 229 + 230 +func formatDate(d ast.Date) string { 231 + if d.Year == 0 && d.Month == 0 && d.Day == 0 { 232 + return "" 233 + } 234 + if d.Month < 1 || d.Month > 12 || d.Day < 1 || d.Day > 31 { 235 + return "" 236 + } 237 + return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day) 238 +} 239 + 240 +func dateLess(a, b ast.Date) bool { 241 + if a.Year != b.Year { 242 + return a.Year < b.Year 243 + } 244 + if a.Month != b.Month { 245 + return a.Month < b.Month 246 + } 247 + return a.Day < b.Day 248 +} 249 + 250 +func (a *Analysis) addCommodityUsage(fileIndex int, am *ast.Amount, date *ast.Date) { 251 + if am == nil || am.Commodity == "" { 252 + return 253 + } 254 + info, ok := a.Commodities[am.Commodity] 255 + if !ok { 256 + info = &CommodityInfo{} 257 + a.Commodities[am.Commodity] = info 258 + } 259 + info.Usages = append(info.Usages, CommodityUsage{ 260 + FileIndex: fileIndex, 261 + Amount: am, 262 + }) 263 + info.UsedCount++ 264 + if date != nil { 265 + info.LastUsed = maxDate(info.LastUsed, *date) 266 + } 267 +} 268 + 269 +func (a *Analysis) buildPrefixIndex() { 270 + for name := range a.Accounts { 271 + parts := strings.Split(name, ":") 272 + for i := 1; i < len(parts); i++ { 273 + prefix := strings.Join(parts[:i], ":") + ":" 274 + a.AccountsByPrefix[prefix] = append(a.AccountsByPrefix[prefix], name) 275 + } 276 + } 277 +} 278 + 279 +func (a *Analysis) collectPayeeNames() { 280 + names := make([]string, 0, len(a.Payees)) 281 + for name := range a.Payees { 282 + names = append(names, name) 283 + } 284 + sort.Strings(names) 285 + a.PayeeNames = names 286 +} 287 + 288 +func (a *Analysis) sortAccountNames() { 289 + names := make([]string, 0, len(a.Accounts)) 290 + for name := range a.Accounts { 291 + names = append(names, name) 292 + } 293 + sort.Strings(names) 294 + a.AccountNames = names 295 +} 296 + 297 +func maxDate(a, b ast.Date) ast.Date { 298 + if a.Year > b.Year || 299 + (a.Year == b.Year && (a.Month > b.Month || 300 + (a.Year == b.Year && a.Month == b.Month && a.Day > b.Day))) { 301 + return a 302 + } 303 + return b 304 +}
A
internal/analyzer/testdata/account-indexing.txtar
··· 1 +-- in.journal -- 2 +2024-01-01 groceries 3 + expenses:food:groceries $10 4 + expenses:transport:fuel $5 5 + assets:bank:checking $15 6 + 7 +2024-01-02 transport 8 + expenses:transport:fuel $20 9 + assets:cash $-20 10 + 11 +-- expect -- 12 +files (1): 13 + 0: in.journal 14 + 15 +commodities (1): 16 + $ 17 + directives: 0 18 + used: 5 19 + usages: 5 20 + file 0: $ ("10") 21 + file 0: $ ("5") 22 + file 0: $ ("15") 23 + file 0: $ ("20") 24 + file 0: $ ("-20") 25 + last-used: 2024-01-02 26 + 27 +accounts (4): 28 + assets:bank:checking 29 + directives: 0 30 + used: 1 31 + usages: 1 32 + file 0: assets:bank:checking 33 + last-used: 2024-01-01 34 + assets:cash 35 + directives: 0 36 + used: 1 37 + usages: 1 38 + file 0: assets:cash 39 + last-used: 2024-01-02 40 + expenses:food:groceries 41 + directives: 0 42 + used: 1 43 + usages: 1 44 + file 0: expenses:food:groceries 45 + last-used: 2024-01-01 46 + expenses:transport:fuel 47 + directives: 0 48 + used: 2 49 + usages: 2 50 + file 0: expenses:transport:fuel 51 + file 0: expenses:transport:fuel 52 + last-used: 2024-01-02 53 + 54 +payees (2): 55 + groceries 56 + directives: 0 57 + used: 1 58 + usages: 1 59 + file 0: groceries 60 + transport 61 + directives: 0 62 + used: 1 63 + usages: 1 64 + file 0: transport 65 + 66 +prefixes (5): 67 + assets: 68 + assets:bank:checking 69 + assets:cash 70 + assets:bank: 71 + assets:bank:checking 72 + expenses: 73 + expenses:food:groceries 74 + expenses:transport:fuel 75 + expenses:food: 76 + expenses:food:groceries 77 + expenses:transport: 78 + expenses:transport:fuel 79 + 80 +transactions: 2 81 +periodic transactions: 0 82 +automated transactions: 0 83 + 84 +directives (0): 85 + 86 +transaction keys (2): 87 + 2024-01-01|groceries|expenses:food:groceries,expenses:transport:fuel,assets:bank:checking, 88 + 2024-01-02|transport|expenses:transport:fuel,assets:cash, 89 + 90 +payee templates (2): 91 + groceries 92 + expenses:food:groceries $10 93 + expenses:transport:fuel $5 94 + assets:bank:checking $15 95 + transport 96 + expenses:transport:fuel $20 97 + assets:cash $-20 98 + 99 +dates (2): 100 + 2024-01-01 101 + 2024-01-02 102 + 103 +tag names (0): 104 + 105 +commodity decimal marks (1): 106 + $: .
A
internal/analyzer/testdata/account-names-sorted.txtar
··· 1 +-- in.journal -- 2 +2024-01-01 groceries 3 + expenses:food $10 4 + assets:cash $-10 5 + 6 +2024-01-02 transport 7 + expenses:transport:fuel $20 8 + assets:cash $-20 9 + 10 +-- expect -- 11 +files (1): 12 + 0: in.journal 13 + 14 +commodities (1): 15 + $ 16 + directives: 0 17 + used: 4 18 + usages: 4 19 + file 0: $ ("10") 20 + file 0: $ ("-10") 21 + file 0: $ ("20") 22 + file 0: $ ("-20") 23 + last-used: 2024-01-02 24 + 25 +accounts (3): 26 + assets:cash 27 + directives: 0 28 + used: 2 29 + usages: 2 30 + file 0: assets:cash 31 + file 0: assets:cash 32 + last-used: 2024-01-02 33 + expenses:food 34 + directives: 0 35 + used: 1 36 + usages: 1 37 + file 0: expenses:food 38 + last-used: 2024-01-01 39 + expenses:transport:fuel 40 + directives: 0 41 + used: 1 42 + usages: 1 43 + file 0: expenses:transport:fuel 44 + last-used: 2024-01-02 45 + 46 +payees (2): 47 + groceries 48 + directives: 0 49 + used: 1 50 + usages: 1 51 + file 0: groceries 52 + transport 53 + directives: 0 54 + used: 1 55 + usages: 1 56 + file 0: transport 57 + 58 +prefixes (3): 59 + assets: 60 + assets:cash 61 + expenses: 62 + expenses:food 63 + expenses:transport:fuel 64 + expenses:transport: 65 + expenses:transport:fuel 66 + 67 +transactions: 2 68 +periodic transactions: 0 69 +automated transactions: 0 70 + 71 +directives (0): 72 + 73 +transaction keys (2): 74 + 2024-01-01|groceries|expenses:food,assets:cash, 75 + 2024-01-02|transport|expenses:transport:fuel,assets:cash, 76 + 77 +payee templates (2): 78 + groceries 79 + expenses:food $10 80 + assets:cash $-10 81 + transport 82 + expenses:transport:fuel $20 83 + assets:cash $-20 84 + 85 +dates (2): 86 + 2024-01-01 87 + 2024-01-02 88 + 89 +tag names (0): 90 + 91 +commodity decimal marks (1): 92 + $: .
A
internal/analyzer/testdata/account-usage.txtar
··· 1 +-- in.journal -- 2 +2024-01-01 groceries 3 + expenses:food $10 4 + assets:cash 5 + 6 +2024-01-02 lunch 7 + expenses:food $5 8 + assets:cash 9 + 10 +2024-01-15 dinner 11 + expenses:food $20 12 + assets:cash 13 + 14 +-- expect -- 15 +files (1): 16 + 0: in.journal 17 + 18 +commodities (1): 19 + $ 20 + directives: 0 21 + used: 3 22 + usages: 3 23 + file 0: $ ("10") 24 + file 0: $ ("5") 25 + file 0: $ ("20") 26 + last-used: 2024-01-15 27 + 28 +accounts (2): 29 + assets:cash 30 + directives: 0 31 + used: 3 32 + usages: 3 33 + file 0: assets:cash 34 + file 0: assets:cash 35 + file 0: assets:cash 36 + last-used: 2024-01-15 37 + expenses:food 38 + directives: 0 39 + used: 3 40 + usages: 3 41 + file 0: expenses:food 42 + file 0: expenses:food 43 + file 0: expenses:food 44 + last-used: 2024-01-15 45 + 46 +payees (3): 47 + dinner 48 + directives: 0 49 + used: 1 50 + usages: 1 51 + file 0: dinner 52 + groceries 53 + directives: 0 54 + used: 1 55 + usages: 1 56 + file 0: groceries 57 + lunch 58 + directives: 0 59 + used: 1 60 + usages: 1 61 + file 0: lunch 62 + 63 +prefixes (2): 64 + assets: 65 + assets:cash 66 + expenses: 67 + expenses:food 68 + 69 +transactions: 3 70 +periodic transactions: 0 71 +automated transactions: 0 72 + 73 +directives (0): 74 + 75 +transaction keys (3): 76 + 2024-01-01|groceries|expenses:food,assets:cash, 77 + 2024-01-02|lunch|expenses:food,assets:cash, 78 + 2024-01-15|dinner|expenses:food,assets:cash, 79 + 80 +payee templates (3): 81 + dinner 82 + expenses:food $20 83 + assets:cash (inferred) 84 + groceries 85 + expenses:food $10 86 + assets:cash (inferred) 87 + lunch 88 + expenses:food $5 89 + assets:cash (inferred) 90 + 91 +dates (3): 92 + 2024-01-01 93 + 2024-01-02 94 + 2024-01-15 95 + 96 +tag names (0): 97 + 98 +commodity decimal marks (1): 99 + $: .
A
internal/analyzer/testdata/commodity-usage.txtar
··· 1 +-- in.journal -- 2 +2024-01-01 groceries 3 + expenses:food $10 4 + assets:cash 5 + 6 +2024-01-02 exchange 7 + expenses:food EUR 5 8 + assets:cash 9 + 10 +2024-01-03 market 11 + expenses:food 10 AAPL 12 + assets:cash 13 + 14 +-- expect -- 15 +files (1): 16 + 0: in.journal 17 + 18 +commodities (3): 19 + $ 20 + directives: 0 21 + used: 1 22 + usages: 1 23 + file 0: $ ("10") 24 + last-used: 2024-01-01 25 + AAPL 26 + directives: 0 27 + used: 1 28 + usages: 1 29 + file 0: AAPL ("10") 30 + last-used: 2024-01-03 31 + EUR 32 + directives: 0 33 + used: 1 34 + usages: 1 35 + file 0: EUR ("5") 36 + last-used: 2024-01-02 37 + 38 +accounts (2): 39 + assets:cash 40 + directives: 0 41 + used: 3 42 + usages: 3 43 + file 0: assets:cash 44 + file 0: assets:cash 45 + file 0: assets:cash 46 + last-used: 2024-01-03 47 + expenses:food 48 + directives: 0 49 + used: 3 50 + usages: 3 51 + file 0: expenses:food 52 + file 0: expenses:food 53 + file 0: expenses:food 54 + last-used: 2024-01-03 55 + 56 +payees (3): 57 + exchange 58 + directives: 0 59 + used: 1 60 + usages: 1 61 + file 0: exchange 62 + groceries 63 + directives: 0 64 + used: 1 65 + usages: 1 66 + file 0: groceries 67 + market 68 + directives: 0 69 + used: 1 70 + usages: 1 71 + file 0: market 72 + 73 +prefixes (2): 74 + assets: 75 + assets:cash 76 + expenses: 77 + expenses:food 78 + 79 +transactions: 3 80 +periodic transactions: 0 81 +automated transactions: 0 82 + 83 +directives (0): 84 + 85 +transaction keys (3): 86 + 2024-01-01|groceries|expenses:food,assets:cash, 87 + 2024-01-02|exchange|expenses:food,assets:cash, 88 + 2024-01-03|market|expenses:food,assets:cash, 89 + 90 +payee templates (3): 91 + exchange 92 + expenses:food EUR5 93 + assets:cash (inferred) 94 + groceries 95 + expenses:food $10 96 + assets:cash (inferred) 97 + market 98 + expenses:food AAPL10 99 + assets:cash (inferred) 100 + 101 +dates (3): 102 + 2024-01-01 103 + 2024-01-02 104 + 2024-01-03 105 + 106 +tag names (0): 107 + 108 +commodity decimal marks (3): 109 + $: . 110 + AAPL: . 111 + EUR: .
A
internal/analyzer/testdata/cost-balance.txtar
··· 1 +-- in.journal -- 2 +2024-01-01 * buy stock 3 + assets:stocks 10 AAPL @ $150.00 4 + assets:bank $-1500.00 5 + assets:stocks = 100 AAPL 6 + 7 +-- expect -- 8 +files (1): 9 + 0: in.journal 10 + 11 +commodities (2): 12 + $ 13 + directives: 0 14 + used: 2 15 + usages: 2 16 + file 0: $ ("150") 17 + file 0: $ ("-1500") 18 + last-used: 2024-01-01 19 + AAPL 20 + directives: 0 21 + used: 2 22 + usages: 2 23 + file 0: AAPL ("10") 24 + file 0: AAPL ("100") 25 + last-used: 2024-01-01 26 + 27 +accounts (2): 28 + assets:bank 29 + directives: 0 30 + used: 1 31 + usages: 1 32 + file 0: assets:bank 33 + last-used: 2024-01-01 34 + assets:stocks 35 + directives: 0 36 + used: 2 37 + usages: 2 38 + file 0: assets:stocks 39 + file 0: assets:stocks 40 + last-used: 2024-01-01 41 + 42 +payees (1): 43 + buy stock 44 + directives: 0 45 + used: 1 46 + usages: 1 47 + file 0: buy stock 48 + 49 +prefixes (1): 50 + assets: 51 + assets:bank 52 + assets:stocks 53 + 54 +transactions: 1 55 +periodic transactions: 0 56 +automated transactions: 0 57 + 58 +directives (0): 59 + 60 +transaction keys (1): 61 + 2024-01-01|buy stock|assets:stocks,assets:bank,assets:stocks, 62 + 63 +payee templates (1): 64 + buy stock 65 + assets:stocks AAPL10 66 + assets:bank $-1500 67 + assets:stocks (inferred) 68 + 69 +dates (1): 70 + 2024-01-01 71 + 72 +tag names (0): 73 + 74 +commodity decimal marks (2): 75 + $: . 76 + AAPL: .
A
internal/analyzer/testdata/dates-dedup.txtar
··· 1 +-- in.journal -- 2 +2024-01-01 groceries 3 + expenses:food $10 4 + assets:cash 5 + 6 +2024-01-01 lunch 7 + expenses:food $5 8 + assets:cash 9 + 10 +-- expect -- 11 +files (1): 12 + 0: in.journal 13 + 14 +commodities (1): 15 + $ 16 + directives: 0 17 + used: 2 18 + usages: 2 19 + file 0: $ ("10") 20 + file 0: $ ("5") 21 + last-used: 2024-01-01 22 + 23 +accounts (2): 24 + assets:cash 25 + directives: 0 26 + used: 2 27 + usages: 2 28 + file 0: assets:cash 29 + file 0: assets:cash 30 + last-used: 2024-01-01 31 + expenses:food 32 + directives: 0 33 + used: 2 34 + usages: 2 35 + file 0: expenses:food 36 + file 0: expenses:food 37 + last-used: 2024-01-01 38 + 39 +payees (2): 40 + groceries 41 + directives: 0 42 + used: 1 43 + usages: 1 44 + file 0: groceries 45 + lunch 46 + directives: 0 47 + used: 1 48 + usages: 1 49 + file 0: lunch 50 + 51 +prefixes (2): 52 + assets: 53 + assets:cash 54 + expenses: 55 + expenses:food 56 + 57 +transactions: 2 58 +periodic transactions: 0 59 +automated transactions: 0 60 + 61 +directives (0): 62 + 63 +transaction keys (2): 64 + 2024-01-01|groceries|expenses:food,assets:cash, 65 + 2024-01-01|lunch|expenses:food,assets:cash, 66 + 67 +payee templates (2): 68 + groceries 69 + expenses:food $10 70 + assets:cash (inferred) 71 + lunch 72 + expenses:food $5 73 + assets:cash (inferred) 74 + 75 +dates (1): 76 + 2024-01-01 77 + 78 +tag names (0): 79 + 80 +commodity decimal marks (1): 81 + $: .
A
internal/analyzer/testdata/dates.txtar
··· 1 +-- in.journal -- 2 +2024-01-03 third 3 + expenses:food $10 4 + assets:cash 5 + 6 +2024-01-01 first 7 + expenses:food $5 8 + assets:cash 9 + 10 +2024-01-01 also-first 11 + expenses:food $5 12 + assets:cash 13 + 14 +2024-01-02 second 15 + expenses:food $20 16 + assets:cash 17 + 18 +-- expect -- 19 +files (1): 20 + 0: in.journal 21 + 22 +commodities (1): 23 + $ 24 + directives: 0 25 + used: 4 26 + usages: 4 27 + file 0: $ ("10") 28 + file 0: $ ("5") 29 + file 0: $ ("5") 30 + file 0: $ ("20") 31 + last-used: 2024-01-03 32 + 33 +accounts (2): 34 + assets:cash 35 + directives: 0 36 + used: 4 37 + usages: 4 38 + file 0: assets:cash 39 + file 0: assets:cash 40 + file 0: assets:cash 41 + file 0: assets:cash 42 + last-used: 2024-01-03 43 + expenses:food 44 + directives: 0 45 + used: 4 46 + usages: 4 47 + file 0: expenses:food 48 + file 0: expenses:food 49 + file 0: expenses:food 50 + file 0: expenses:food 51 + last-used: 2024-01-03 52 + 53 +payees (4): 54 + also-first 55 + directives: 0 56 + used: 1 57 + usages: 1 58 + file 0: also-first 59 + first 60 + directives: 0 61 + used: 1 62 + usages: 1 63 + file 0: first 64 + second 65 + directives: 0 66 + used: 1 67 + usages: 1 68 + file 0: second 69 + third 70 + directives: 0 71 + used: 1 72 + usages: 1 73 + file 0: third 74 + 75 +prefixes (2): 76 + assets: 77 + assets:cash 78 + expenses: 79 + expenses:food 80 + 81 +transactions: 4 82 +periodic transactions: 0 83 +automated transactions: 0 84 + 85 +directives (0): 86 + 87 +transaction keys (4): 88 + 2024-01-01|also-first|expenses:food,assets:cash, 89 + 2024-01-01|first|expenses:food,assets:cash, 90 + 2024-01-02|second|expenses:food,assets:cash, 91 + 2024-01-03|third|expenses:food,assets:cash, 92 + 93 +payee templates (4): 94 + also-first 95 + expenses:food $5 96 + assets:cash (inferred) 97 + first 98 + expenses:food $5 99 + assets:cash (inferred) 100 + second 101 + expenses:food $20 102 + assets:cash (inferred) 103 + third 104 + expenses:food $10 105 + assets:cash (inferred) 106 + 107 +dates (3): 108 + 2024-01-01 109 + 2024-01-02 110 + 2024-01-03 111 + 112 +tag names (0): 113 + 114 +commodity decimal marks (1): 115 + $: .
A
internal/analyzer/testdata/decimal-marks.txtar
··· 1 +-- in.journal -- 2 +D $1,000.00 3 + 4 +2024-01-01 groceries 5 + expenses:food $10.50 6 + assets:cash 7 + 8 +-- expect -- 9 +files (1): 10 + 0: in.journal 11 + 12 +commodities (1): 13 + $ 14 + directives: 0 15 + used: 1 16 + usages: 1 17 + file 0: $ ("10.5") 18 + last-used: 2024-01-01 19 + 20 +accounts (2): 21 + assets:cash 22 + directives: 0 23 + used: 1 24 + usages: 1 25 + file 0: assets:cash 26 + last-used: 2024-01-01 27 + expenses:food 28 + directives: 0 29 + used: 1 30 + usages: 1 31 + file 0: expenses:food 32 + last-used: 2024-01-01 33 + 34 +payees (1): 35 + groceries 36 + directives: 0 37 + used: 1 38 + usages: 1 39 + file 0: groceries 40 + 41 +prefixes (2): 42 + assets: 43 + assets:cash 44 + expenses: 45 + expenses:food 46 + 47 +transactions: 1 48 +periodic transactions: 0 49 +automated transactions: 0 50 + 51 +directives (1): 52 + D $1000 53 + 54 +transaction keys (1): 55 + 2024-01-01|groceries|expenses:food,assets:cash, 56 + 57 +payee templates (1): 58 + groceries 59 + expenses:food $10.5 60 + assets:cash (inferred) 61 + 62 +dates (1): 63 + 2024-01-01 64 + 65 +tag names (0): 66 + 67 +commodity decimal marks (1): 68 + $: .
A
internal/analyzer/testdata/directives.txtar
··· 1 +-- in.journal -- 2 +account expenses:food 3 +commodity $ 4 +payee TestPayee 5 +tag mytag 6 +include other.journal 7 +alias expenses:food = expenses:food:old 8 +year 2024 9 +D $1,000.00 10 +P 2024-01-01 AAPL $150.00 11 +C $100.00 @@ EUR 90.00 12 +apply tag foo 13 +end tag 14 +comment 15 +some ignored text 16 +N ignored 17 + 18 +-- expect -- 19 +files (1): 20 + 0: in.journal 21 + 22 +commodities (1): 23 + $ 24 + directives: 1 25 + used: 0 26 + commodity $ 27 + usages: 0 28 + 29 +accounts (1): 30 + expenses:food 31 + directives: 1 32 + used: 0 33 + account expenses:food 34 + usages: 0 35 + 36 +payees (1): 37 + 38 + directives: 1 39 + used: 0 40 + payee 41 + usages: 0 42 + 43 +prefixes (1): 44 + expenses: 45 + expenses:food 46 + 47 +transactions: 0 48 +periodic transactions: 0 49 +automated transactions: 0 50 + 51 +directives (13): 52 + account expenses:food 53 + commodity $ 54 + payee 55 + tag mytag 56 + include other.journal 57 + alias expenses:food = expenses:food:old 58 + year 2024 59 + D $1000 60 + P AAPL 150 61 + C $100 @@ 0 62 + apply tag foo 63 + end tag 64 + comment 65 + 66 +transaction keys (0): 67 + 68 +payee templates (0): 69 + 70 +dates (0): 71 + 72 +tag names (1): 73 + mytag 74 + 75 +commodity decimal marks (1): 76 + $: .
A
internal/analyzer/testdata/duplicate-transactions.txtar
··· 1 +-- in.journal -- 2 +2024-01-01 groceries 3 + expenses:food $10 4 + assets:cash 5 + 6 +2024-01-01 groceries 7 + expenses:food $10 8 + assets:cash 9 + 10 +-- expect -- 11 +files (1): 12 + 0: in.journal 13 + 14 +commodities (1): 15 + $ 16 + directives: 0 17 + used: 2 18 + usages: 2 19 + file 0: $ ("10") 20 + file 0: $ ("10") 21 + last-used: 2024-01-01 22 + 23 +accounts (2): 24 + assets:cash 25 + directives: 0 26 + used: 2 27 + usages: 2 28 + file 0: assets:cash 29 + file 0: assets:cash 30 + last-used: 2024-01-01 31 + expenses:food 32 + directives: 0 33 + used: 2 34 + usages: 2 35 + file 0: expenses:food 36 + file 0: expenses:food 37 + last-used: 2024-01-01 38 + 39 +payees (1): 40 + groceries 41 + directives: 0 42 + used: 2 43 + usages: 2 44 + file 0: groceries 45 + file 0: groceries 46 + 47 +prefixes (2): 48 + assets: 49 + assets:cash 50 + expenses: 51 + expenses:food 52 + 53 +transactions: 2 54 +periodic transactions: 0 55 +automated transactions: 0 56 + 57 +directives (0): 58 + 59 +transaction keys (1): 60 + 2024-01-01|groceries|expenses:food,assets:cash, (dup: 2) 61 + 62 +payee templates (1): 63 + groceries 64 + expenses:food $10 65 + assets:cash (inferred) 66 + 67 +dates (1): 68 + 2024-01-01 69 + 70 +tag names (0): 71 + 72 +commodity decimal marks (1): 73 + $: .
A
internal/analyzer/testdata/empty.txtar
··· 1 +-- in.journal -- 2 + 3 +-- expect -- 4 +files (1): 5 + 0: in.journal 6 + 7 +commodities (0): 8 + 9 +accounts (0): 10 + 11 +payees (0): 12 + 13 +prefixes (0): 14 + 15 +transactions: 0 16 +periodic transactions: 0 17 +automated transactions: 0 18 + 19 +directives (0): 20 + 21 +transaction keys (0): 22 + 23 +payee templates (0): 24 + 25 +dates (0): 26 + 27 +tag names (0): 28 + 29 +commodity decimal marks (0):
A
internal/analyzer/testdata/include.txtar
··· 1 +-- in.journal -- 2 +include accounts.journal 3 + 4 +2024/01/01 ! test 5 + expenses:food $10 6 + assets:checking 7 + 8 +-- accounts.journal -- 9 +account expenses:food 10 + 11 +-- expect -- 12 +files (2): 13 + 0: accounts.journal 14 + 1: in.journal 15 + 16 +commodities (1): 17 + $ 18 + directives: 0 19 + used: 1 20 + usages: 1 21 + file 1: $ ("10") 22 + last-used: 2024-01-01 23 + 24 +accounts (2): 25 + assets:checking 26 + directives: 0 27 + used: 1 28 + usages: 1 29 + file 1: assets:checking 30 + last-used: 2024-01-01 31 + expenses:food 32 + directives: 1 33 + used: 1 34 + account expenses:food 35 + usages: 1 36 + file 1: expenses:food 37 + last-used: 2024-01-01 38 + 39 +payees (1): 40 + test 41 + directives: 0 42 + used: 1 43 + usages: 1 44 + file 1: test 45 + 46 +prefixes (2): 47 + assets: 48 + assets:checking 49 + expenses: 50 + expenses:food 51 + 52 +transactions: 1 53 +periodic transactions: 0 54 +automated transactions: 0 55 + 56 +directives (2): 57 + account expenses:food 58 + include accounts.journal 59 + 60 +transaction keys (1): 61 + 2024-01-01|test|expenses:food,assets:checking, 62 + 63 +payee templates (1): 64 + test 65 + expenses:food $10 66 + assets:checking (inferred) 67 + 68 +dates (1): 69 + 2024-01-01 70 + 71 +tag names (0): 72 + 73 +commodity decimal marks (1): 74 + $: .
A
internal/analyzer/testdata/journal.txtar
··· 1 +-- in.journal -- 2 +account expenses:food 3 +account expenses:food 4 +account expenses:transport 5 + 6 +commodity $ 7 +commodity $ 8 +commodity EUR 9 + 10 +~ monthly 11 + expenses:transport $50 12 + assets:checking 13 + 14 += expr:big 15 + expenses:food $100 16 + assets:credit 17 + 18 +2024/01/01 groceries 19 + expenses:food $10 20 + assets:checking 21 + 22 +2024/01/02 lunch 23 + expenses:food $5 24 + assets:cash 25 + 26 +2024/01/04 aws 27 + expenses:food 1000000.00 USD 28 + assets:cash 29 + 30 +-- expect -- 31 +files (1): 32 + 0: in.journal 33 + 34 +commodities (3): 35 + $ 36 + directives: 2 37 + used: 4 38 + commodity $ 39 + commodity $ 40 + usages: 4 41 + file 0: $ ("50") 42 + file 0: $ ("100") 43 + file 0: $ ("10") 44 + file 0: $ ("5") 45 + last-used: 2024-01-02 46 + EUR 47 + directives: 1 48 + used: 0 49 + commodity EUR 50 + usages: 0 51 + USD 52 + directives: 0 53 + used: 1 54 + usages: 1 55 + file 0: USD ("1000000") 56 + last-used: 2024-01-04 57 + 58 +accounts (5): 59 + assets:cash 60 + directives: 0 61 + used: 2 62 + usages: 2 63 + file 0: assets:cash 64 + file 0: assets:cash 65 + last-used: 2024-01-04 66 + assets:checking 67 + directives: 0 68 + used: 2 69 + usages: 2 70 + file 0: assets:checking 71 + file 0: assets:checking 72 + last-used: 2024-01-01 73 + assets:credit 74 + directives: 0 75 + used: 1 76 + usages: 1 77 + file 0: assets:credit 78 + expenses:food 79 + directives: 2 80 + used: 4 81 + account expenses:food 82 + account expenses:food 83 + usages: 4 84 + file 0: expenses:food 85 + file 0: expenses:food 86 + file 0: expenses:food 87 + file 0: expenses:food 88 + last-used: 2024-01-04 89 + expenses:transport 90 + directives: 1 91 + used: 1 92 + account expenses:transport 93 + usages: 1 94 + file 0: expenses:transport 95 + 96 +payees (3): 97 + aws 98 + directives: 0 99 + used: 1 100 + usages: 1 101 + file 0: aws 102 + groceries 103 + directives: 0 104 + used: 1 105 + usages: 1 106 + file 0: groceries 107 + lunch 108 + directives: 0 109 + used: 1 110 + usages: 1 111 + file 0: lunch 112 + 113 +prefixes (2): 114 + assets: 115 + assets:cash 116 + assets:checking 117 + assets:credit 118 + expenses: 119 + expenses:food 120 + expenses:transport 121 + 122 +transactions: 3 123 +periodic transactions: 1 124 +automated transactions: 1 125 + 126 +directives (6): 127 + account expenses:food 128 + account expenses:food 129 + account expenses:transport 130 + commodity $ 131 + commodity $ 132 + commodity EUR 133 + 134 +transaction keys (3): 135 + 2024-01-01|groceries|expenses:food,assets:checking, 136 + 2024-01-02|lunch|expenses:food,assets:cash, 137 + 2024-01-04|aws|expenses:food,assets:cash, 138 + 139 +payee templates (3): 140 + aws 141 + expenses:food USD1000000 142 + assets:cash (inferred) 143 + groceries 144 + expenses:food $10 145 + assets:checking (inferred) 146 + lunch 147 + expenses:food $5 148 + assets:cash (inferred) 149 + 150 +dates (3): 151 + 2024-01-01 152 + 2024-01-02 153 + 2024-01-04 154 + 155 +tag names (0): 156 + 157 +commodity decimal marks (2): 158 + $: . 159 + USD: .
A
internal/analyzer/testdata/last-used-dates.txtar
··· 1 +-- in.journal -- 2 +2024-01-01 groceries 3 + expenses:food $10 4 + assets:cash 5 + 6 +2024-01-15 lunch 7 + expenses:food $5 8 + assets:cash 9 + 10 +-- expect -- 11 +files (1): 12 + 0: in.journal 13 + 14 +commodities (1): 15 + $ 16 + directives: 0 17 + used: 2 18 + usages: 2 19 + file 0: $ ("10") 20 + file 0: $ ("5") 21 + last-used: 2024-01-15 22 + 23 +accounts (2): 24 + assets:cash 25 + directives: 0 26 + used: 2 27 + usages: 2 28 + file 0: assets:cash 29 + file 0: assets:cash 30 + last-used: 2024-01-15 31 + expenses:food 32 + directives: 0 33 + used: 2 34 + usages: 2 35 + file 0: expenses:food 36 + file 0: expenses:food 37 + last-used: 2024-01-15 38 + 39 +payees (2): 40 + groceries 41 + directives: 0 42 + used: 1 43 + usages: 1 44 + file 0: groceries 45 + lunch 46 + directives: 0 47 + used: 1 48 + usages: 1 49 + file 0: lunch 50 + 51 +prefixes (2): 52 + assets: 53 + assets:cash 54 + expenses: 55 + expenses:food 56 + 57 +transactions: 2 58 +periodic transactions: 0 59 +automated transactions: 0 60 + 61 +directives (0): 62 + 63 +transaction keys (2): 64 + 2024-01-01|groceries|expenses:food,assets:cash, 65 + 2024-01-15|lunch|expenses:food,assets:cash, 66 + 67 +payee templates (2): 68 + groceries 69 + expenses:food $10 70 + assets:cash (inferred) 71 + lunch 72 + expenses:food $5 73 + assets:cash (inferred) 74 + 75 +dates (2): 76 + 2024-01-01 77 + 2024-01-15 78 + 79 +tag names (0): 80 + 81 +commodity decimal marks (1): 82 + $: .
A
internal/analyzer/testdata/payee-templates.txtar
··· 1 +-- in.journal -- 2 +2024-01-01 groceries 3 + expenses:food $10 4 + assets:checking 5 + 6 +2024-01-02 lunch 7 + expenses:food $5 8 + assets:cash 9 + 10 +2024-01-03 groceries 11 + expenses:food $20 12 + assets:checking 13 + 14 +-- expect -- 15 +files (1): 16 + 0: in.journal 17 + 18 +commodities (1): 19 + $ 20 + directives: 0 21 + used: 3 22 + usages: 3 23 + file 0: $ ("10") 24 + file 0: $ ("5") 25 + file 0: $ ("20") 26 + last-used: 2024-01-03 27 + 28 +accounts (3): 29 + assets:cash 30 + directives: 0 31 + used: 1 32 + usages: 1 33 + file 0: assets:cash 34 + last-used: 2024-01-02 35 + assets:checking 36 + directives: 0 37 + used: 2 38 + usages: 2 39 + file 0: assets:checking 40 + file 0: assets:checking 41 + last-used: 2024-01-03 42 + expenses:food 43 + directives: 0 44 + used: 3 45 + usages: 3 46 + file 0: expenses:food 47 + file 0: expenses:food 48 + file 0: expenses:food 49 + last-used: 2024-01-03 50 + 51 +payees (2): 52 + groceries 53 + directives: 0 54 + used: 2 55 + usages: 2 56 + file 0: groceries 57 + file 0: groceries 58 + lunch 59 + directives: 0 60 + used: 1 61 + usages: 1 62 + file 0: lunch 63 + 64 +prefixes (2): 65 + assets: 66 + assets:cash 67 + assets:checking 68 + expenses: 69 + expenses:food 70 + 71 +transactions: 3 72 +periodic transactions: 0 73 +automated transactions: 0 74 + 75 +directives (0): 76 + 77 +transaction keys (3): 78 + 2024-01-01|groceries|expenses:food,assets:checking, 79 + 2024-01-02|lunch|expenses:food,assets:cash, 80 + 2024-01-03|groceries|expenses:food,assets:checking, 81 + 82 +payee templates (2): 83 + groceries 84 + expenses:food $20 85 + assets:checking (inferred) 86 + lunch 87 + expenses:food $5 88 + assets:cash (inferred) 89 + 90 +dates (3): 91 + 2024-01-01 92 + 2024-01-02 93 + 2024-01-03 94 + 95 +tag names (0): 96 + 97 +commodity decimal marks (1): 98 + $: .
A
internal/analyzer/testdata/payee-tracking.txtar
··· 1 +-- in.journal -- 2 +2024-01-01 groceries 3 + expenses:food $10 4 + assets:cash 5 + 6 +-- expect -- 7 +files (1): 8 + 0: in.journal 9 + 10 +commodities (1): 11 + $ 12 + directives: 0 13 + used: 1 14 + usages: 1 15 + file 0: $ ("10") 16 + last-used: 2024-01-01 17 + 18 +accounts (2): 19 + assets:cash 20 + directives: 0 21 + used: 1 22 + usages: 1 23 + file 0: assets:cash 24 + last-used: 2024-01-01 25 + expenses:food 26 + directives: 0 27 + used: 1 28 + usages: 1 29 + file 0: expenses:food 30 + last-used: 2024-01-01 31 + 32 +payees (1): 33 + groceries 34 + directives: 0 35 + used: 1 36 + usages: 1 37 + file 0: groceries 38 + 39 +prefixes (2): 40 + assets: 41 + assets:cash 42 + expenses: 43 + expenses:food 44 + 45 +transactions: 1 46 +periodic transactions: 0 47 +automated transactions: 0 48 + 49 +directives (0): 50 + 51 +transaction keys (1): 52 + 2024-01-01|groceries|expenses:food,assets:cash, 53 + 54 +payee templates (1): 55 + groceries 56 + expenses:food $10 57 + assets:cash (inferred) 58 + 59 +dates (1): 60 + 2024-01-01 61 + 62 +tag names (0): 63 + 64 +commodity decimal marks (1): 65 + $: .
A
internal/analyzer/testdata/payees.txtar
··· 1 +-- in.journal -- 2 +2024-01-01 groceries 3 + expenses:food $10 4 + assets:checking 5 + 6 +2024-01-02 lunch 7 + expenses:food $5 8 + assets:cash 9 + 10 +2024-01-03 groceries 11 + expenses:food $20 12 + assets:checking 13 + 14 +-- expect -- 15 +files (1): 16 + 0: in.journal 17 + 18 +commodities (1): 19 + $ 20 + directives: 0 21 + used: 3 22 + usages: 3 23 + file 0: $ ("10") 24 + file 0: $ ("5") 25 + file 0: $ ("20") 26 + last-used: 2024-01-03 27 + 28 +accounts (3): 29 + assets:cash 30 + directives: 0 31 + used: 1 32 + usages: 1 33 + file 0: assets:cash 34 + last-used: 2024-01-02 35 + assets:checking 36 + directives: 0 37 + used: 2 38 + usages: 2 39 + file 0: assets:checking 40 + file 0: assets:checking 41 + last-used: 2024-01-03 42 + expenses:food 43 + directives: 0 44 + used: 3 45 + usages: 3 46 + file 0: expenses:food 47 + file 0: expenses:food 48 + file 0: expenses:food 49 + last-used: 2024-01-03 50 + 51 +payees (2): 52 + groceries 53 + directives: 0 54 + used: 2 55 + usages: 2 56 + file 0: groceries 57 + file 0: groceries 58 + lunch 59 + directives: 0 60 + used: 1 61 + usages: 1 62 + file 0: lunch 63 + 64 +prefixes (2): 65 + assets: 66 + assets:cash 67 + assets:checking 68 + expenses: 69 + expenses:food 70 + 71 +transactions: 3 72 +periodic transactions: 0 73 +automated transactions: 0 74 + 75 +directives (0): 76 + 77 +transaction keys (3): 78 + 2024-01-01|groceries|expenses:food,assets:checking, 79 + 2024-01-02|lunch|expenses:food,assets:cash, 80 + 2024-01-03|groceries|expenses:food,assets:checking, 81 + 82 +payee templates (2): 83 + groceries 84 + expenses:food $20 85 + assets:checking (inferred) 86 + lunch 87 + expenses:food $5 88 + assets:cash (inferred) 89 + 90 +dates (3): 91 + 2024-01-01 92 + 2024-01-02 93 + 2024-01-03 94 + 95 +tag names (0): 96 + 97 +commodity decimal marks (1): 98 + $: .
A
internal/analyzer/testdata/periodic-automated.txtar
··· 1 +-- in.journal -- 2 +~ monthly 3 + expenses:food $100 4 + assets:cash 5 + 6 += expr:big 7 + expenses:food $200 8 + assets:cash 9 + 10 +-- expect -- 11 +files (1): 12 + 0: in.journal 13 + 14 +commodities (1): 15 + $ 16 + directives: 0 17 + used: 2 18 + usages: 2 19 + file 0: $ ("100") 20 + file 0: $ ("200") 21 + 22 +accounts (2): 23 + assets:cash 24 + directives: 0 25 + used: 2 26 + usages: 2 27 + file 0: assets:cash 28 + file 0: assets:cash 29 + expenses:food 30 + directives: 0 31 + used: 2 32 + usages: 2 33 + file 0: expenses:food 34 + file 0: expenses:food 35 + 36 +payees (0): 37 + 38 +prefixes (2): 39 + assets: 40 + assets:cash 41 + expenses: 42 + expenses:food 43 + 44 +transactions: 0 45 +periodic transactions: 1 46 +automated transactions: 1 47 + 48 +directives (0): 49 + 50 +transaction keys (0): 51 + 52 +payee templates (0): 53 + 54 +dates (0): 55 + 56 +tag names (0): 57 + 58 +commodity decimal marks (1): 59 + $: .
A
internal/analyzer/testdata/posting-types.txtar
··· 1 +-- in.journal -- 2 +2024-01-01 test 3 + (virtual:unbalanced) $10 4 + [virtual:balanced] $20 5 + expenses:food $30 6 + 7 +-- expect -- 8 +files (1): 9 + 0: in.journal 10 + 11 +commodities (1): 12 + $ 13 + directives: 0 14 + used: 3 15 + usages: 3 16 + file 0: $ ("10") 17 + file 0: $ ("20") 18 + file 0: $ ("30") 19 + last-used: 2024-01-01 20 + 21 +accounts (3): 22 + expenses:food 23 + directives: 0 24 + used: 1 25 + usages: 1 26 + file 0: expenses:food 27 + last-used: 2024-01-01 28 + virtual:balanced 29 + directives: 0 30 + used: 1 31 + usages: 1 32 + file 0: virtual:balanced 33 + last-used: 2024-01-01 34 + virtual:unbalanced 35 + directives: 0 36 + used: 1 37 + usages: 1 38 + file 0: virtual:unbalanced 39 + last-used: 2024-01-01 40 + 41 +payees (1): 42 + test 43 + directives: 0 44 + used: 1 45 + usages: 1 46 + file 0: test 47 + 48 +prefixes (2): 49 + expenses: 50 + expenses:food 51 + virtual: 52 + virtual:balanced 53 + virtual:unbalanced 54 + 55 +transactions: 1 56 +periodic transactions: 0 57 +automated transactions: 0 58 + 59 +directives (0): 60 + 61 +transaction keys (1): 62 + 2024-01-01|test|virtual:unbalanced,virtual:balanced,expenses:food, 63 + 64 +payee templates (1): 65 + test 66 + virtual:unbalanced $10 67 + virtual:balanced $20 68 + expenses:food $30 69 + 70 +dates (1): 71 + 2024-01-01 72 + 73 +tag names (0): 74 + 75 +commodity decimal marks (1): 76 + $: .
A
internal/analyzer/testdata/prefix-index.txtar
··· 1 +-- in.journal -- 2 +2024-01-01 groceries 3 + expenses:food:groceries $10 4 + expenses:transport:fuel $5 5 + assets:bank:checking $15 6 + 7 +-- expect -- 8 +files (1): 9 + 0: in.journal 10 + 11 +commodities (1): 12 + $ 13 + directives: 0 14 + used: 3 15 + usages: 3 16 + file 0: $ ("10") 17 + file 0: $ ("5") 18 + file 0: $ ("15") 19 + last-used: 2024-01-01 20 + 21 +accounts (3): 22 + assets:bank:checking 23 + directives: 0 24 + used: 1 25 + usages: 1 26 + file 0: assets:bank:checking 27 + last-used: 2024-01-01 28 + expenses:food:groceries 29 + directives: 0 30 + used: 1 31 + usages: 1 32 + file 0: expenses:food:groceries 33 + last-used: 2024-01-01 34 + expenses:transport:fuel 35 + directives: 0 36 + used: 1 37 + usages: 1 38 + file 0: expenses:transport:fuel 39 + last-used: 2024-01-01 40 + 41 +payees (1): 42 + groceries 43 + directives: 0 44 + used: 1 45 + usages: 1 46 + file 0: groceries 47 + 48 +prefixes (5): 49 + assets: 50 + assets:bank:checking 51 + assets:bank: 52 + assets:bank:checking 53 + expenses: 54 + expenses:food:groceries 55 + expenses:transport:fuel 56 + expenses:food: 57 + expenses:food:groceries 58 + expenses:transport: 59 + expenses:transport:fuel 60 + 61 +transactions: 1 62 +periodic transactions: 0 63 +automated transactions: 0 64 + 65 +directives (0): 66 + 67 +transaction keys (1): 68 + 2024-01-01|groceries|expenses:food:groceries,expenses:transport:fuel,assets:bank:checking, 69 + 70 +payee templates (1): 71 + groceries 72 + expenses:food:groceries $10 73 + expenses:transport:fuel $5 74 + assets:bank:checking $15 75 + 76 +dates (1): 77 + 2024-01-01 78 + 79 +tag names (0): 80 + 81 +commodity decimal marks (1): 82 + $: .
A
internal/analyzer/testdata/tag-names-dedup.txtar
··· 1 +-- in.journal -- 2 +tag project-xyz 3 +tag project-xyz 4 + 5 +-- expect -- 6 +files (1): 7 + 0: in.journal 8 + 9 +commodities (0): 10 + 11 +accounts (0): 12 + 13 +payees (0): 14 + 15 +prefixes (0): 16 + 17 +transactions: 0 18 +periodic transactions: 0 19 +automated transactions: 0 20 + 21 +directives (2): 22 + tag project-xyz 23 + tag project-xyz 24 + 25 +transaction keys (0): 26 + 27 +payee templates (0): 28 + 29 +dates (0): 30 + 31 +tag names (1): 32 + project-xyz 33 + 34 +commodity decimal marks (0):
A
internal/analyzer/testdata/tag-names.txtar
··· 1 +-- in.journal -- 2 +tag project-xyz 3 +tag personal 4 + 5 +-- expect -- 6 +files (1): 7 + 0: in.journal 8 + 9 +commodities (0): 10 + 11 +accounts (0): 12 + 13 +payees (0): 14 + 15 +prefixes (0): 16 + 17 +transactions: 0 18 +periodic transactions: 0 19 +automated transactions: 0 20 + 21 +directives (2): 22 + tag project-xyz 23 + tag personal 24 + 25 +transaction keys (0): 26 + 27 +payee templates (0): 28 + 29 +dates (0): 30 + 31 +tag names (2): 32 + personal 33 + project-xyz 34 + 35 +commodity decimal marks (0):
A
internal/analyzer/testdata/typed-entries.txtar
··· 1 +-- in.journal -- 2 +2024-01-01 groceries 3 + expenses:food $10 4 + assets:cash 5 + 6 +~ monthly 7 + expenses:transport $50 8 + assets:checking 9 + 10 += expr:big 11 + expenses:food $100 12 + assets:credit 13 + 14 +account expenses:food 15 + 16 +-- expect -- 17 +files (1): 18 + 0: in.journal 19 + 20 +commodities (1): 21 + $ 22 + directives: 0 23 + used: 3 24 + usages: 3 25 + file 0: $ ("10") 26 + file 0: $ ("50") 27 + file 0: $ ("100") 28 + last-used: 2024-01-01 29 + 30 +accounts (5): 31 + assets:cash 32 + directives: 0 33 + used: 1 34 + usages: 1 35 + file 0: assets:cash 36 + last-used: 2024-01-01 37 + assets:checking 38 + directives: 0 39 + used: 1 40 + usages: 1 41 + file 0: assets:checking 42 + assets:credit 43 + directives: 0 44 + used: 1 45 + usages: 1 46 + file 0: assets:credit 47 + expenses:food 48 + directives: 1 49 + used: 2 50 + account expenses:food 51 + usages: 2 52 + file 0: expenses:food 53 + file 0: expenses:food 54 + last-used: 2024-01-01 55 + expenses:transport 56 + directives: 0 57 + used: 1 58 + usages: 1 59 + file 0: expenses:transport 60 + 61 +payees (1): 62 + groceries 63 + directives: 0 64 + used: 1 65 + usages: 1 66 + file 0: groceries 67 + 68 +prefixes (2): 69 + assets: 70 + assets:cash 71 + assets:checking 72 + assets:credit 73 + expenses: 74 + expenses:food 75 + expenses:transport 76 + 77 +transactions: 1 78 +periodic transactions: 1 79 +automated transactions: 1 80 + 81 +directives (1): 82 + account expenses:food 83 + 84 +transaction keys (1): 85 + 2024-01-01|groceries|expenses:food,assets:cash, 86 + 87 +payee templates (1): 88 + groceries 89 + expenses:food $10 90 + assets:cash (inferred) 91 + 92 +dates (1): 93 + 2024-01-01 94 + 95 +tag names (0): 96 + 97 +commodity decimal marks (1): 98 + $: .
M
internal/cli/cmd_lint.go
··· 8 8 9 9 "github.com/urfave/cli/v3" 10 10 11 + "olexsmir.xyz/clerk/internal/analyzer" 11 12 "olexsmir.xyz/clerk/internal/linter" 12 13 "olexsmir.xyz/clerk/journal" 13 - "olexsmir.xyz/clerk/journal/semantic" 14 14 ) 15 15 16 16 func (c *Cli) lintAction(ctx context.Context, cmd *cli.Command) error { ··· 44 44 } 45 45 46 46 for _, root := range loader.Roots() { 47 - reporter.Collect(lint.Run(semantic.Build(journal.CollectFiles(root)))) 47 + reporter.Collect(lint.Run(analyzer.Build(journal.CollectFiles(root)))) 48 48 } 49 49 50 50 if err := reporter.Flush(format); err != nil { ··· 64 64 } 65 65 66 66 for _, root := range loader.Roots() { 67 - reporter.Collect(lint.Run(semantic.Build(journal.CollectFiles(root)))) 67 + reporter.Collect(lint.Run(analyzer.Build(journal.CollectFiles(root)))) 68 68 } 69 69 70 70 if err := reporter.Flush(format); err != nil {
M
internal/linter/linter.go
··· 1 1 package linter 2 2 3 3 import ( 4 + "olexsmir.xyz/clerk/internal/analyzer" 4 5 "olexsmir.xyz/clerk/journal/token" 5 - 6 - "olexsmir.xyz/clerk/journal/semantic" 7 6 ) 8 7 9 8 // A Find represents a single lint finding. ··· 24 23 return &Linter{rules: rules} 25 24 } 26 25 27 -// Run runs all rules against the semantic context. 28 -func (l *Linter) Run(ctx *semantic.Context) []Find { 26 +// Run runs all rules against the analysis context. 27 +func (l *Linter) Run(a *analyzer.Analysis) []Find { 29 28 var finds []Find 30 29 31 - // per-entry traversal 32 - for _, pf := range ctx.Files { 33 - for _, entry := range pf.Ast.Entries { 34 - for _, rule := range l.rules { 35 - if ec, ok := rule.(EntryChecker); ok { 36 - finds = append(finds, ec.CheckEntry(entry)...) 37 - } 38 - } 39 - } 40 - } 41 - 42 - // post-traversal 43 30 for _, rule := range l.rules { 44 - if jc, ok := rule.(JournalChecker); ok { 45 - finds = append(finds, jc.CheckJournal(ctx)...) 31 + if jc, ok := rule.(Rule); ok { 32 + finds = append(finds, jc.CheckJournal(a)...) 46 33 } 47 34 } 48 35
M
internal/linter/linter_test.go
··· 4 4 "strings" 5 5 "testing" 6 6 7 + "olexsmir.xyz/clerk/internal/analyzer" 7 8 "olexsmir.xyz/clerk/internal/testutil/golden" 8 9 "olexsmir.xyz/clerk/journal" 9 - "olexsmir.xyz/clerk/journal/semantic" 10 10 ) 11 11 12 12 var tests = map[string][]Rule{ ··· 42 42 t.Fatalf("failed to load test journal: %v", err) 43 43 } 44 44 45 - ctx := semantic.Build(l.Ordered()) 45 + ctx := analyzer.Build(l.Ordered()) 46 46 finds := NewLinter(trules).Run(ctx) 47 47 48 48 var b strings.Builder ··· 61 61 b.Fatalf("failed to load benchmark journal: %v", err) 62 62 } 63 63 64 - ctx := semantic.Build(ldr.Ordered()) 64 + ctx := analyzer.Build(ldr.Ordered()) 65 65 l := NewLinter(Rules) 66 66 67 67 b.ResetTimer()
M
internal/linter/rule_account_depth.go
··· 3 3 import ( 4 4 "fmt" 5 5 6 + "olexsmir.xyz/clerk/internal/analyzer" 6 7 "olexsmir.xyz/clerk/journal/ast" 7 8 ) 8 9 ··· 11 12 MaxDepth int 12 13 } 13 14 14 -func (AccountDepthLimit) ID() RuleID { return "account-depth" } 15 -func (AccountDepthLimit) Severity() Severity { return SeverityWarning } 16 -func (a *AccountDepthLimit) CheckEntry(entry ast.Entry) []Find { 15 +func (AccountDepthLimit) ID() RuleID { return "account-depth" } 16 +func (AccountDepthLimit) Severity() Severity { return SeverityWarning } 17 +func (a *AccountDepthLimit) CheckJournal(an *analyzer.Analysis) []Find { 17 18 var finds []Find 18 - switch e := entry.(type) { 19 - case *ast.Transaction: 20 - for _, p := range e.Postings { 19 + 20 + for _, txn := range an.Transactions { 21 + for _, p := range txn.Postings { 21 22 a.check(&finds, p.Account) 22 23 } 23 - case *ast.PeriodicTransaction: 24 - for _, p := range e.Postings { 24 + } 25 + for _, ptx := range an.PeriodicTransactions { 26 + for _, p := range ptx.Postings { 25 27 a.check(&finds, p.Account) 26 28 } 27 - case *ast.AutomatedTransaction: 28 - for _, p := range e.Postings { 29 + } 30 + for _, atx := range an.AutomatedTransactions { 31 + for _, p := range atx.Postings { 29 32 a.check(&finds, p.Account) 30 33 } 31 - case *ast.AccountDirective: 32 - a.check(&finds, e.Account) 33 - case *ast.AliasDirective: 34 - a.check(&finds, e.From) 35 - a.check(&finds, e.To) 34 + } 35 + 36 + for _, d := range an.Directives { 37 + switch e := d.(type) { 38 + case *ast.AccountDirective: 39 + a.check(&finds, e.Account) 40 + case *ast.AliasDirective: 41 + a.check(&finds, e.From) 42 + a.check(&finds, e.To) 43 + } 36 44 } 45 + 37 46 return finds 38 47 } 39 48
M
internal/linter/rule_duplicated_account.go
··· 3 3 import ( 4 4 "fmt" 5 5 6 - "olexsmir.xyz/clerk/journal/semantic" 6 + "olexsmir.xyz/clerk/internal/analyzer" 7 7 ) 8 8 9 9 // DuplicatedAccount flags account declarations that appear more than once. ··· 11 11 12 12 func (DuplicatedAccount) ID() RuleID { return "duplicated-account" } 13 13 func (DuplicatedAccount) Severity() Severity { return SeverityWarning } 14 -func (d *DuplicatedAccount) CheckJournal(ctx *semantic.Context) []Find { 14 +func (d *DuplicatedAccount) CheckJournal(an *analyzer.Analysis) []Find { 15 15 var finds []Find 16 - for _, info := range ctx.Accounts { 16 + for _, info := range an.Accounts { 17 17 if len(info.Directives) <= 1 { 18 18 continue 19 19 }
M
internal/linter/rule_duplicated_commodity.go
··· 3 3 import ( 4 4 "fmt" 5 5 6 - "olexsmir.xyz/clerk/journal/semantic" 6 + "olexsmir.xyz/clerk/internal/analyzer" 7 7 ) 8 8 9 9 // DuplicatedCommodity flags commodity declarations that appear more than once. ··· 11 11 12 12 func (DuplicatedCommodity) ID() RuleID { return "duplicated-commodity" } 13 13 func (DuplicatedCommodity) Severity() Severity { return SeverityWarning } 14 -func (d *DuplicatedCommodity) CheckJournal(ctx *semantic.Context) []Find { 14 +func (d *DuplicatedCommodity) CheckJournal(an *analyzer.Analysis) []Find { 15 15 var finds []Find 16 - for sym, info := range ctx.Commodities { 16 + for sym, info := range an.Commodities { 17 17 if len(info.Directives) <= 1 { 18 18 continue 19 19 }
M
internal/linter/rule_empty_postings.go
··· 1 1 package linter 2 2 3 -import "olexsmir.xyz/clerk/journal/ast" 3 +import "olexsmir.xyz/clerk/internal/analyzer" 4 4 5 5 // EmptyPostings flags transactions that have no postings. 6 6 type EmptyPostings struct{} 7 7 8 -func (EmptyPostings) ID() RuleID { return "empty-postings" } 9 -func (EmptyPostings) Severity() Severity { return SeverityError } 10 -func (e *EmptyPostings) CheckEntry(entry ast.Entry) []Find { 11 - txn, ok := entry.(*ast.Transaction) 12 - if !ok { 13 - return nil 8 +func (EmptyPostings) ID() RuleID { return "empty-postings" } 9 +func (EmptyPostings) Severity() Severity { return SeverityError } 10 +func (e *EmptyPostings) CheckJournal(an *analyzer.Analysis) []Find { 11 + var finds []Find 12 + for _, txn := range an.Transactions { 13 + if len(txn.Postings) == 0 { 14 + finds = append(finds, Find{ 15 + Code: e.ID(), 16 + Severity: e.Severity(), 17 + Message: "transaction has no postings", 18 + Span: txn.Span, 19 + }) 20 + } 14 21 } 15 - if len(txn.Postings) == 0 { 16 - return []Find{{ 17 - Code: e.ID(), 18 - Severity: e.Severity(), 19 - Message: "transaction has no postings", 20 - Span: txn.Span, 21 - }} 22 - } 23 - return nil 22 + return finds 24 23 }
M
internal/linter/rule_missing_commodity.go
··· 1 1 package linter 2 2 3 -import "olexsmir.xyz/clerk/journal/ast" 3 +import ( 4 + "olexsmir.xyz/clerk/internal/analyzer" 5 + "olexsmir.xyz/clerk/journal/ast" 6 +) 4 7 5 8 // MissingCommodity flags amounts with a missing commodity. 6 9 type MissingCommodity struct{} 7 10 8 11 func (MissingCommodity) ID() RuleID { return "missing-commodity" } 9 12 func (MissingCommodity) Severity() Severity { return SeverityWarning } 10 -func (m *MissingCommodity) CheckEntry(entry ast.Entry) []Find { 13 +func (m *MissingCommodity) CheckJournal(an *analyzer.Analysis) []Find { 11 14 var finds []Find 12 - switch e := entry.(type) { 13 - case *ast.Transaction: 14 - m.checkPostings(&finds, e.Postings) 15 - case *ast.PeriodicTransaction: 16 - m.checkPostings(&finds, e.Postings) 17 - case *ast.AutomatedTransaction: 18 - m.checkPostings(&finds, e.Postings) 19 - case *ast.ConversionDirective: 20 - m.check(&finds, e.From) 21 - m.check(&finds, e.To) 22 - case *ast.CommodityDirective: 23 - if e.Format.Commodity != "" { 24 - m.check(&finds, e.Format) 15 + 16 + for _, txn := range an.Transactions { 17 + m.checkPostings(&finds, txn.Postings) 18 + } 19 + for _, ptx := range an.PeriodicTransactions { 20 + m.checkPostings(&finds, ptx.Postings) 21 + } 22 + for _, atx := range an.AutomatedTransactions { 23 + m.checkPostings(&finds, atx.Postings) 24 + } 25 + 26 + for _, d := range an.Directives { 27 + switch e := d.(type) { 28 + case *ast.ConversionDirective: 29 + m.check(&finds, e.From) 30 + m.check(&finds, e.To) 31 + case *ast.CommodityDirective: 32 + if e.Format.Commodity != "" { 33 + m.check(&finds, e.Format) 34 + } 35 + case *ast.DefaultCommodityDirective: 36 + m.check(&finds, e.Amount) 37 + case *ast.MarketPriceDirective: 38 + m.check(&finds, e.Amount) 25 39 } 26 - case *ast.DefaultCommodityDirective: 27 - m.check(&finds, e.Amount) 28 - case *ast.MarketPriceDirective: 29 - m.check(&finds, e.Amount) 30 40 } 41 + 31 42 return finds 32 43 } 33 44
M
internal/linter/rule_missing_payee.go
··· 1 1 package linter 2 2 3 -import "olexsmir.xyz/clerk/journal/ast" 3 +import "olexsmir.xyz/clerk/internal/analyzer" 4 4 5 5 // MissingPayee flags transactions with missing payee. 6 6 type MissingPayee struct{} 7 7 8 8 func (MissingPayee) ID() RuleID { return "missing-payee" } 9 9 func (MissingPayee) Severity() Severity { return SeverityWarning } 10 -func (m *MissingPayee) CheckEntry(entry ast.Entry) []Find { 11 - txn, ok := entry.(*ast.Transaction) 12 - if !ok { 13 - return nil 10 +func (m *MissingPayee) CheckJournal(an *analyzer.Analysis) []Find { 11 + var finds []Find 12 + for _, txn := range an.Transactions { 13 + if txn.Payee == nil { 14 + finds = append(finds, Find{ 15 + Code: m.ID(), 16 + Severity: m.Severity(), 17 + Message: "transaction has no payee", 18 + Span: txn.Date.Span, 19 + }) 20 + } 14 21 } 15 - if txn.Payee == nil { 16 - return []Find{{ 17 - Code: m.ID(), 18 - Severity: m.Severity(), 19 - Message: "transaction has no payee", 20 - Span: txn.Date.Span, 21 - }} 22 - } 23 - return nil 22 + return finds 24 23 }
M
internal/linter/rule_missing_status.go
··· 1 1 package linter 2 2 3 -import "olexsmir.xyz/clerk/journal/ast" 3 +import ( 4 + "olexsmir.xyz/clerk/internal/analyzer" 5 + "olexsmir.xyz/clerk/journal/ast" 6 +) 4 7 5 8 // MissingStatus flags transactions with missing status. 6 9 type MissingStatus struct{} 7 10 8 11 func (MissingStatus) ID() RuleID { return "missing-status" } 9 12 func (MissingStatus) Severity() Severity { return SeverityWarning } 10 -func (m *MissingStatus) CheckEntry(entry ast.Entry) []Find { 11 - tnx, ok := entry.(*ast.Transaction) 12 - if !ok { 13 - return nil 13 +func (m *MissingStatus) CheckJournal(an *analyzer.Analysis) []Find { 14 + var finds []Find 15 + for _, txn := range an.Transactions { 16 + if txn.Status.Value == ast.StatusNone { 17 + finds = append(finds, Find{ 18 + Code: m.ID(), 19 + Severity: m.Severity(), 20 + Message: "transaction has no status", 21 + Span: txn.Status.Span, 22 + }) 23 + } 14 24 } 15 - 16 - if tnx.Status.Value == ast.StatusNone { 17 - return []Find{{ 18 - Code: m.ID(), 19 - Severity: m.Severity(), 20 - Message: "transaction has no status", 21 - Span: tnx.Status.Span, 22 - }} 23 - } 24 - return nil 25 + return finds 25 26 }
M
internal/linter/rule_multiple_omitted_amounts.go
··· 1 1 package linter 2 2 3 -import "olexsmir.xyz/clerk/journal/ast" 3 +import ( 4 + "olexsmir.xyz/clerk/internal/analyzer" 5 + "olexsmir.xyz/clerk/journal/ast" 6 +) 4 7 5 8 // MultipleOmittedAmounts flags entries where more than one posting has an ommited amount. 6 9 type MultipleOmittedAmounts struct{} 7 10 8 11 func (MultipleOmittedAmounts) ID() RuleID { return "multiple-omitted-amounts" } 9 12 func (MultipleOmittedAmounts) Severity() Severity { return SeverityError } 10 -func (m *MultipleOmittedAmounts) CheckEntry(entry ast.Entry) []Find { 11 - switch e := entry.(type) { 12 - case *ast.Transaction: 13 - return m.check(e.Postings) 14 - case *ast.PeriodicTransaction: 15 - return m.check(e.Postings) 16 - case *ast.AutomatedTransaction: 17 - return m.check(e.Postings) 18 - default: 19 - return nil 13 +func (m *MultipleOmittedAmounts) CheckJournal(an *analyzer.Analysis) []Find { 14 + var finds []Find 15 + for _, txn := range an.Transactions { 16 + finds = append(finds, m.check(txn.Postings)...) 17 + } 18 + for _, ptx := range an.PeriodicTransactions { 19 + finds = append(finds, m.check(ptx.Postings)...) 20 + } 21 + for _, atx := range an.AutomatedTransactions { 22 + finds = append(finds, m.check(atx.Postings)...) 20 23 } 24 + return finds 21 25 } 22 26 23 27 func (m *MultipleOmittedAmounts) check(postings []*ast.Posting) []Find { 24 28 var finds []Find 25 29 for _, p := range postings { 26 - // skipping postings with balance assertion, since those are often used as reconciliation entries 27 30 if p.Amount == nil && p.Balance == nil { 28 31 finds = append(finds, Find{ 29 32 Code: m.ID(),
M
internal/linter/rule_omitted_precision.go
··· 1 1 package linter 2 2 3 -import "olexsmir.xyz/clerk/journal/ast" 3 +import "olexsmir.xyz/clerk/internal/analyzer" 4 4 5 5 // OmittedPrecision flags amounts with insufficient decimal precision (<2 digits). 6 6 type OmittedPrecision struct{} 7 7 8 8 func (OmittedPrecision) ID() RuleID { return "omitted-precision" } 9 9 func (OmittedPrecision) Severity() Severity { return SeverityWarning } 10 -func (o *OmittedPrecision) CheckEntry(entry ast.Entry) []Find { 11 - txn, ok := entry.(*ast.Transaction) 12 - if !ok || (txn.Postings != nil && len(txn.Postings) == 0) { 13 - return nil 14 - } 15 - 10 +func (o *OmittedPrecision) CheckJournal(an *analyzer.Analysis) []Find { 16 11 var finds []Find 17 - for _, posting := range txn.Postings { 18 - if posting.Amount == nil { 19 - continue 20 - } 21 - if posting.Amount.QuantityFmt.Precision < 2 { 22 - finds = append(finds, Find{ 23 - Code: o.ID(), 24 - Severity: o.Severity(), 25 - Message: "amount has insufficient precision", 26 - Span: posting.Amount.Span, 27 - }) 12 + for _, txn := range an.Transactions { 13 + for _, posting := range txn.Postings { 14 + if posting.Amount == nil { 15 + continue 16 + } 17 + if posting.Amount.QuantityFmt.Precision < 2 { 18 + finds = append(finds, Find{ 19 + Code: o.ID(), 20 + Severity: o.Severity(), 21 + Message: "amount has insufficient precision", 22 + Span: posting.Amount.Span, 23 + }) 24 + } 28 25 } 29 26 } 30 27 return finds
M
internal/linter/rule_orderdate.go
··· 3 3 import ( 4 4 "fmt" 5 5 6 + "olexsmir.xyz/clerk/internal/analyzer" 6 7 "olexsmir.xyz/clerk/journal/ast" 7 - "olexsmir.xyz/clerk/journal/semantic" 8 8 ) 9 9 10 10 // OrderDate checks that transactions are in chronological order by date. ··· 12 12 13 13 func (OrderDate) ID() RuleID { return "orderdate" } 14 14 func (OrderDate) Severity() Severity { return SeverityWarning } 15 -func (r *OrderDate) CheckJournal(ctx *semantic.Context) []Find { 15 +func (r *OrderDate) CheckJournal(an *analyzer.Analysis) []Find { 16 16 var finds []Find 17 17 var anchor *ast.Date 18 - for _, pf := range ctx.Files { 18 + for _, pf := range an.Files { 19 19 for _, entry := range pf.Ast.Entries { 20 20 txn, ok := entry.(*ast.Transaction) 21 21 if !ok {
M
internal/linter/rule_parse_error.go
··· 1 1 package linter 2 2 3 -import "olexsmir.xyz/clerk/journal/semantic" 3 +import "olexsmir.xyz/clerk/internal/analyzer" 4 4 5 5 // ParseError wraps parser errors into lint findings. 6 6 type ParseError struct{} 7 7 8 8 func (ParseError) ID() RuleID { return "parse-error" } 9 9 func (ParseError) Severity() Severity { return SeverityError } 10 -func (e *ParseError) CheckJournal(ctx *semantic.Context) []Find { 10 +func (e *ParseError) CheckJournal(an *analyzer.Analysis) []Find { 11 11 var finds []Find 12 - for _, pf := range ctx.Files { 12 + for _, pf := range an.Files { 13 13 for _, err := range pf.Errors { 14 14 finds = append(finds, Find{ 15 15 Code: e.ID(),
M
internal/linter/rule_unbalanced_transaction.go
··· 3 3 import ( 4 4 "fmt" 5 5 6 + "olexsmir.xyz/clerk/internal/analyzer" 6 7 "olexsmir.xyz/clerk/internal/decimal" 7 8 "olexsmir.xyz/clerk/journal/ast" 8 9 "olexsmir.xyz/clerk/journal/token" ··· 13 14 14 15 func (UnbalancedTransaction) ID() RuleID { return "unbalanced-transaction" } 15 16 func (UnbalancedTransaction) Severity() Severity { return SeverityError } 16 -func (r *UnbalancedTransaction) CheckEntry(entry ast.Entry) []Find { 17 - switch e := entry.(type) { 18 - case *ast.Transaction: 19 - return r.check(e.Postings, e.Span) 20 - case *ast.PeriodicTransaction: 21 - return r.check(e.Postings, e.Span) 22 - case *ast.AutomatedTransaction: 23 - return r.check(e.Postings, e.Span) 24 - default: 25 - return nil 17 +func (r *UnbalancedTransaction) CheckJournal(an *analyzer.Analysis) []Find { 18 + var finds []Find 19 + for _, txn := range an.Transactions { 20 + finds = append(finds, r.check(txn.Postings, txn.Span)...) 21 + } 22 + for _, ptx := range an.PeriodicTransactions { 23 + finds = append(finds, r.check(ptx.Postings, ptx.Span)...) 24 + } 25 + for _, atx := range an.AutomatedTransactions { 26 + finds = append(finds, r.check(atx.Postings, atx.Span)...) 26 27 } 28 + return finds 27 29 } 28 30 29 31 func (r *UnbalancedTransaction) check(postings []*ast.Posting, span token.Span) []Find { ··· 50 52 51 53 if len(realPostings) == 0 || 52 54 autoBalancingPostings == 1 || 53 - // multiple auto-balancing postings is alreayd handled by [MultipleOmittedAmounts] 54 55 autoBalancingPostings > 1 || 55 - // too complex too verify 56 56 hasExpr || hasCost { 57 57 return nil 58 58 } 59 59 60 - // sum amounts per commodity 61 60 sums := make(map[string]decimal.Decimal) 62 61 for _, posting := range realPostings { 63 62 if posting.Amount == nil {
M
internal/linter/rule_undeclared_account.go
··· 3 3 import ( 4 4 "fmt" 5 5 6 - "olexsmir.xyz/clerk/journal/semantic" 6 + "olexsmir.xyz/clerk/internal/analyzer" 7 7 ) 8 8 9 9 // UndeclaredAccount flags postings that reference an account not declared via `account` directive. ··· 11 11 12 12 func (UndeclaredAccount) ID() RuleID { return "undeclared-account" } 13 13 func (UndeclaredAccount) Severity() Severity { return SeverityWarning } 14 -func (r *UndeclaredAccount) CheckJournal(ctx *semantic.Context) []Find { 14 +func (r *UndeclaredAccount) CheckJournal(an *analyzer.Analysis) []Find { 15 15 var finds []Find 16 - for name, info := range ctx.Accounts { 16 + for name, info := range an.Accounts { 17 17 if len(info.Directives) > 0 { 18 18 continue 19 19 }
M
internal/linter/rule_undeclared_commodity.go
··· 3 3 import ( 4 4 "fmt" 5 5 6 - "olexsmir.xyz/clerk/journal/semantic" 6 + "olexsmir.xyz/clerk/internal/analyzer" 7 7 ) 8 8 9 9 // UndeclaredCommodity flags amounts that reference a commodity not declared via `commodity` directive. ··· 11 11 12 12 func (UndeclaredCommodity) ID() RuleID { return "undeclared-commodity" } 13 13 func (UndeclaredCommodity) Severity() Severity { return SeverityWarning } 14 -func (r *UndeclaredCommodity) CheckJournal(ctx *semantic.Context) []Find { 14 +func (r *UndeclaredCommodity) CheckJournal(an *analyzer.Analysis) []Find { 15 15 var finds []Find 16 - for name, info := range ctx.Commodities { 16 + for name, info := range an.Commodities { 17 17 if len(info.Directives) > 0 { 18 18 continue 19 19 }
M
internal/linter/rule_unused_account.go
··· 3 3 import ( 4 4 "fmt" 5 5 6 - "olexsmir.xyz/clerk/journal/semantic" 6 + "olexsmir.xyz/clerk/internal/analyzer" 7 7 ) 8 8 9 9 // UnusedAccount flags declared accounts that are not used. ··· 11 11 12 12 func (UnusedAccount) ID() RuleID { return "unused-account" } 13 13 func (UnusedAccount) Severity() Severity { return SeverityWarning } 14 -func (a *UnusedAccount) CheckJournal(ctx *semantic.Context) []Find { 14 +func (a *UnusedAccount) CheckJournal(an *analyzer.Analysis) []Find { 15 15 var finds []Find 16 - for name, info := range ctx.Accounts { 16 + for name, info := range an.Accounts { 17 17 if len(info.Directives) == 0 { 18 18 continue 19 19 }
M
internal/linter/rules.go
··· 1 1 package linter 2 2 3 -import ( 4 - "olexsmir.xyz/clerk/journal/ast" 5 - 6 - "olexsmir.xyz/clerk/journal/semantic" 7 -) 3 +import "olexsmir.xyz/clerk/internal/analyzer" 8 4 9 5 type RuleID string 10 6 ··· 12 8 type Rule interface { 13 9 ID() RuleID 14 10 Severity() Severity 15 -} 16 - 17 -// EntryChecker implements per-entry linting. 18 -type EntryChecker interface { 19 - Rule 20 - CheckEntry(entry ast.Entry) []Find 21 -} 22 - 23 -// JournalChecker implements whole-journal linting using the semantic context. 24 -type JournalChecker interface { 25 - Rule 26 - CheckJournal(ctx *semantic.Context) []Find 11 + CheckJournal(an *analyzer.Analysis) []Find 27 12 } 28 13 29 14 // Rules is list of all available rules. ··· 42 27 &UndeclaredCommodity{}, 43 28 &UndeclaredAccount{}, 44 29 &UnbalancedTransaction{}, 45 - &UnusedAccount{}, 30 + // &UnusedAccount{}, 46 31 }
M
internal/lsp/diagnostics.go
··· 8 8 "go.lsp.dev/jsonrpc2" 9 9 "go.lsp.dev/protocol" 10 10 "go.lsp.dev/uri" 11 + 12 + "olexsmir.xyz/clerk/internal/analyzer" 11 13 "olexsmir.xyz/clerk/internal/linter" 12 14 "olexsmir.xyz/clerk/journal" 13 - "olexsmir.xyz/clerk/journal/semantic" 14 15 "olexsmir.xyz/clerk/journal/token" 15 16 ) 16 17 17 -func (s *server) rebuildWorkspace(ctx context.Context) { 18 - s.log.Debug("rebuilding workspace") 18 +const diagDebounce = 200 * time.Millisecond 19 + 20 +func (s *server) scheduleDiagnostics(ctx context.Context) { 21 + s.diagMu.Lock() 22 + defer s.diagMu.Unlock() 23 + 24 + if s.diagCancel != nil { 25 + s.diagCancel() 26 + } 27 + 28 + ctx, cancel := context.WithCancel(context.Background()) 29 + s.diagCancel = cancel 30 + 31 + time.AfterFunc(diagDebounce, func() { 32 + if ctx.Err() != nil { 33 + return 34 + } 35 + s.publishDiagnostics(jsonrpc2.DetachContext(ctx)) 36 + }) 37 +} 38 + 39 +func (s *server) publishDiagnostics(ctx context.Context) { 40 + s.log.Debug("publishing diagnostics") 19 41 20 42 s.mu.Lock() 21 - defer s.mu.Unlock() 43 + openDocs := make(map[uri.URI]docState, len(s.openDocs)) 44 + for u, st := range s.openDocs { 45 + openDocs[u] = st 46 + } 47 + ordered := s.loader.Ordered() 48 + s.mu.Unlock() 22 49 23 - if len(s.openDocs) == 0 { 50 + if ctx.Err() != nil { 51 + return 52 + } 53 + 54 + if len(openDocs) == 0 { 24 55 return 25 56 } 26 57 27 - for uri, state := range s.openDocs { 58 + for uri, state := range openDocs { 28 59 path := uri.Path() 29 60 if _, err := s.loader.Reload(path, []byte(state.text)); err != nil { 30 - s.log.Warn("filed to reload open document", "uri", uri, "err", err) 61 + s.log.Warn("failed to reload open document", "uri", uri, "err", err) 31 62 } 32 63 } 33 64 34 - roots := s.loader.Roots() 35 - if len(roots) == 0 { 65 + if ctx.Err() != nil { 66 + return 67 + } 68 + 69 + ordered = s.loader.Ordered() 70 + if len(ordered) == 0 { 36 71 s.log.Debug("no files in workspace") 37 72 return 38 73 } 39 74 40 - var allFiles []*journal.ParsedFile 41 - var finds []linter.Find 42 - for _, root := range roots { 43 - files := journal.CollectFiles(root) 44 - allFiles = append(allFiles, files...) 45 - c := semantic.Build(files) 46 - finds = append(finds, s.linter.Run(c)...) 75 + if ctx.Err() != nil { 76 + return 77 + } 78 + 79 + a := analyzer.Build(ordered) 80 + finds := dedupFinds(s.linter.Run(a)) 81 + 82 + s.mu.Lock() 83 + s.current = a 84 + s.mu.Unlock() 47 85 86 + if ctx.Err() != nil { 87 + return 48 88 } 49 89 50 - finds = dedupFinds(finds) 51 - s.semanticCtx = semantic.Build(allFiles) 90 + diagsByFile := s.groupFindsByFile(finds) 91 + 92 + s.mu.Lock() 93 + activeFiles := s.activeFileSet(openDocs, ordered) 94 + s.mu.Unlock() 52 95 53 - diagsByFile := s.groupFindsByFile(finds) 54 - activeFiles := s.activeFileSet(s.openDocs, s.loader.Ordered()) 55 96 for fpath := range activeFiles { 97 + if ctx.Err() != nil { 98 + return 99 + } 56 100 if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{ 57 101 URI: uri.File(fpath), 58 102 Diagnostics: diagsByFile[fpath], ··· 61 105 } 62 106 } 63 107 64 - s.log.Debug("workspace rebuild complete", 65 - "files", len(s.loader.Ordered()), "findings", len(finds)) 66 -} 67 - 68 -func (s *server) scheduleWorkspaceRebuild(ctx context.Context) { 69 - s.mu.Lock() 70 - defer s.mu.Unlock() 71 - if s.debouncer != nil { 72 - s.debouncer.Stop() 73 - } 74 - s.debouncer = time.AfterFunc(200*time.Millisecond, func() { 75 - s.rebuildWorkspace(jsonrpc2.DetachContext(ctx)) 76 - }) 77 -} 78 - 79 -func (s *server) cancelWorkspaceRebuild() { 80 - s.mu.Lock() 81 - defer s.mu.Unlock() 82 - if s.debouncer != nil { 83 - s.debouncer.Stop() 84 - s.debouncer = nil 85 - } 108 + s.log.Debug("diagnostics published", "files", len(ordered), "findings", len(finds)) 86 109 } 87 110 88 111 func (s *server) activeFileSet(docs map[uri.URI]docState, ordered []*journal.ParsedFile) map[string]bool {
M
internal/lsp/server.go
··· 4 4 "context" 5 5 "log/slog" 6 6 "sync" 7 - "time" 8 7 9 8 "go.lsp.dev/protocol" 10 9 "go.lsp.dev/uri" 11 10 11 + "olexsmir.xyz/clerk/internal/analyzer" 12 12 "olexsmir.xyz/clerk/internal/linter" 13 13 "olexsmir.xyz/clerk/journal" 14 14 "olexsmir.xyz/clerk/journal/printer" 15 - "olexsmir.xyz/clerk/journal/semantic" 16 15 ) 17 16 18 17 type server struct { ··· 23 22 24 23 version, name string 25 24 26 - linter *linter.Linter 27 - loader *journal.Loader 28 - printer *printer.Config 29 - debouncer *time.Timer 25 + linter *linter.Linter 26 + loader *journal.Loader 27 + printer *printer.Config 30 28 31 - mu sync.Mutex 32 - openDocs map[uri.URI]docState 33 - semanticCtx *semantic.Context 29 + mu sync.Mutex 30 + openDocs map[uri.URI]docState 31 + current *analyzer.Analysis 32 + 33 + diagMu sync.Mutex 34 + diagCancel context.CancelFunc 34 35 } 35 36 36 37 type docState struct { ··· 56 57 } 57 58 58 59 func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error { 59 - s.scheduleWorkspaceRebuild(ctx) 60 + s.scheduleDiagnostics(ctx) 60 61 return nil 61 62 } 62 63 63 64 func (s *server) Shutdown(ctx context.Context) error { 64 - s.cancelWorkspaceRebuild() 65 65 return nil 66 66 } 67 67
M
internal/lsp/textdocument_sync.go
··· 9 9 10 10 func (s *server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error { 11 11 s.openDoc(params.TextDocument.URI, params.TextDocument.Text, params.TextDocument.Version, params.TextDocument.LanguageID) 12 - s.scheduleWorkspaceRebuild(ctx) 12 + s.scheduleDiagnostics(ctx) 13 13 return nil 14 14 } 15 15 16 16 func (s *server) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error { 17 17 s.updateDoc(params.TextDocument.URI, params.TextDocument.Version, params.ContentChanges) 18 - s.scheduleWorkspaceRebuild(ctx) 18 + s.scheduleDiagnostics(ctx) 19 19 return nil 20 20 } 21 21 22 22 func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error { 23 23 s.closeDoc(params.TextDocument.URI) 24 - s.scheduleWorkspaceRebuild(ctx) 24 + s.scheduleDiagnostics(ctx) 25 25 return nil 26 26 } 27 27
A
journal/parser/benchmark_test.go
··· 1 +package parser 2 + 3 +import ( 4 + "os" 5 + "testing" 6 + 7 + "olexsmir.xyz/clerk/journal/lexer" 8 + "olexsmir.xyz/clerk/journal/token" 9 +) 10 + 11 +func loadTestdata(path string) []byte { 12 + src, err := os.ReadFile(path) 13 + if err != nil { 14 + panic("loadTestdata: " + err.Error()) 15 + } 16 + return src 17 +} 18 + 19 +var ( 20 + basicSrc = loadTestdata("../../journal/testdata/journals/basic.journal") 21 + personalSrc = loadTestdata("../../journal/testdata/journals/actual-personal.journal") 22 + sampleSrc = loadTestdata("../../journal/testdata/journals/actual-sample.journal") 23 + standardSrc = loadTestdata("../../journal/testdata/journals/actual-ledger-input-standard.dat") 24 + wowSrc = loadTestdata("../../journal/testdata/journals/actual-ledger-input-wow.dat") 25 + benchmarkSrc = loadTestdata("../../journal/testdata/journals/actual-1ktxns-100accts.journal") 26 +) 27 + 28 +func BenchmarkLexer_Basic(b *testing.B) { 29 + for b.Loop() { 30 + l := lexer.New("bench", basicSrc) 31 + for l.Next().Type != token.EOF { 32 + } 33 + } 34 +} 35 + 36 +func BenchmarkLexer_Personal(b *testing.B) { 37 + for b.Loop() { 38 + l := lexer.New("bench", personalSrc) 39 + for l.Next().Type != token.EOF { 40 + } 41 + } 42 +} 43 + 44 +func BenchmarkLexer_Sample(b *testing.B) { 45 + for b.Loop() { 46 + l := lexer.New("bench", sampleSrc) 47 + for l.Next().Type != token.EOF { 48 + } 49 + } 50 +} 51 + 52 +func BenchmarkLexer_Standard(b *testing.B) { 53 + for b.Loop() { 54 + l := lexer.New("bench", standardSrc) 55 + for l.Next().Type != token.EOF { 56 + } 57 + } 58 +} 59 + 60 +func BenchmarkLexer_Wow(b *testing.B) { 61 + for b.Loop() { 62 + l := lexer.New("bench", wowSrc) 63 + for l.Next().Type != token.EOF { 64 + } 65 + } 66 +} 67 + 68 +func BenchmarkLexer_1kTxns(b *testing.B) { 69 + for b.Loop() { 70 + l := lexer.New("bench", benchmarkSrc) 71 + for l.Next().Type != token.EOF { 72 + } 73 + } 74 +} 75 + 76 +func BenchmarkParser_Basic(b *testing.B) { 77 + for b.Loop() { 78 + l := lexer.New("bench", basicSrc) 79 + New(l).ParseJournal() 80 + } 81 +} 82 + 83 +func BenchmarkParser_Personal(b *testing.B) { 84 + for b.Loop() { 85 + l := lexer.New("bench", personalSrc) 86 + New(l).ParseJournal() 87 + } 88 +} 89 + 90 +func BenchmarkParser_Sample(b *testing.B) { 91 + for b.Loop() { 92 + l := lexer.New("bench", sampleSrc) 93 + New(l).ParseJournal() 94 + } 95 +} 96 + 97 +func BenchmarkParser_Standard(b *testing.B) { 98 + for b.Loop() { 99 + l := lexer.New("bench", standardSrc) 100 + New(l).ParseJournal() 101 + } 102 +} 103 + 104 +func BenchmarkParser_Wow(b *testing.B) { 105 + for b.Loop() { 106 + l := lexer.New("bench", wowSrc) 107 + New(l).ParseJournal() 108 + } 109 +} 110 + 111 +func BenchmarkParser_1kTxns(b *testing.B) { 112 + for b.Loop() { 113 + l := lexer.New("bench", benchmarkSrc) 114 + New(l).ParseJournal() 115 + } 116 +} 117 + 118 +func BenchmarkParser_1kTxns_Allocs(b *testing.B) { 119 + b.ReportAllocs() 120 + for b.Loop() { 121 + l := lexer.New("bench", benchmarkSrc) 122 + New(l).ParseJournal() 123 + } 124 +} 125 + 126 +func BenchmarkParser_Parallel_1kTxns(b *testing.B) { 127 + b.RunParallel(func(pb *testing.PB) { 128 + for pb.Next() { 129 + l := lexer.New("bench", benchmarkSrc) 130 + New(l).ParseJournal() 131 + } 132 + }) 133 +}
D
journal/semantic/build.go
··· 1 -package semantic 2 - 3 -import ( 4 - "olexsmir.xyz/clerk/journal" 5 - "olexsmir.xyz/clerk/journal/ast" 6 -) 7 - 8 -// Build constructs [Context] from a list of parsed files. 9 -// Files should be in dependency order(includes before includers). 10 -func Build(files []*journal.ParsedFile) *Context { 11 - c := &Context{ 12 - Files: files, 13 - Accounts: make(map[string]*AccountInfo), 14 - Commodities: make(map[string]*CommodityInfo), 15 - Payees: make(map[string]*PayeeInfo), 16 - } 17 - for i, pf := range files { 18 - for _, entry := range pf.Ast.Entries { 19 - switch e := entry.(type) { 20 - case *ast.AccountDirective: 21 - c.addAccountDirective(e) 22 - case *ast.CommodityDirective: 23 - c.addCommodityDirective(e) 24 - case *ast.PayeeDirective: 25 - c.addPayeeDirective(e) 26 - case *ast.Transaction: 27 - c.addPostings(i, e.Postings) 28 - c.addPayee(i, e.Payee) 29 - case *ast.PeriodicTransaction: 30 - c.addPostings(i, e.Postings) 31 - case *ast.AutomatedTransaction: 32 - c.addPostings(i, e.Postings) 33 - } 34 - } 35 - } 36 - return c 37 -} 38 - 39 -func (c *Context) addAccountDirective(ad *ast.AccountDirective) { 40 - aname := ad.Account.String() 41 - info, ok := c.Accounts[aname] 42 - if !ok { 43 - info = &AccountInfo{} 44 - c.Accounts[aname] = info 45 - } 46 - info.Directives = append(info.Directives, ad) 47 -} 48 - 49 -func (c *Context) addPayeeDirective(pd *ast.PayeeDirective) { 50 - info, ok := c.Payees[pd.Name] 51 - if !ok { 52 - info = &PayeeInfo{} 53 - c.Payees[pd.Name] = info 54 - } 55 - info.Directives = append(info.Directives, &ast.Payee{ 56 - Name: pd.Name, 57 - Span: pd.Span, 58 - }) 59 -} 60 - 61 -func (c *Context) addCommodityDirective(cd *ast.CommodityDirective) { 62 - info, ok := c.Commodities[cd.Commodity] 63 - if !ok { 64 - info = &CommodityInfo{} 65 - c.Commodities[cd.Commodity] = info 66 - } 67 - info.Directives = append(info.Directives, cd) 68 -} 69 - 70 -func (c *Context) addPayee(fileIndex int, payee *ast.Payee) { 71 - if payee == nil { 72 - return 73 - } 74 - info, ok := c.Payees[payee.Name] 75 - if !ok { 76 - info = &PayeeInfo{} 77 - c.Payees[payee.Name] = info 78 - } 79 - info.Usage = append(info.Usage, PayeeUsage{ 80 - FileIndex: fileIndex, 81 - Payee: payee, 82 - }) 83 -} 84 - 85 -func (c *Context) addPostings(fileIndex int, postings []*ast.Posting) { 86 - for _, posting := range postings { 87 - // Account tracking 88 - aname := posting.Account.String() 89 - info, ok := c.Accounts[aname] 90 - if !ok { 91 - info = &AccountInfo{} 92 - c.Accounts[aname] = info 93 - } 94 - info.Usages = append(info.Usages, AccountUsage{ 95 - FileIndex: fileIndex, 96 - Posting: posting, 97 - }) 98 - 99 - // Commodity tracking 100 - c.addCommodityUsage(fileIndex, posting.Amount) 101 - if posting.Cost != nil { 102 - c.addCommodityUsage(fileIndex, &posting.Cost.Amount) 103 - } 104 - if posting.Balance != nil { 105 - c.addCommodityUsage(fileIndex, &posting.Balance.Amount) 106 - if posting.Balance.Cost != nil { 107 - c.addCommodityUsage(fileIndex, &posting.Balance.Cost.Amount) 108 - } 109 - } 110 - } 111 -} 112 - 113 -func (c *Context) addCommodityUsage(fileIndex int, am *ast.Amount) { 114 - if am == nil || am.Commodity == "" { 115 - return 116 - } 117 - info, ok := c.Commodities[am.Commodity] 118 - if !ok { 119 - info = &CommodityInfo{} 120 - c.Commodities[am.Commodity] = info 121 - } 122 - info.Usages = append(info.Usages, CommodityUsage{ 123 - FileIndex: fileIndex, 124 - Amount: am, 125 - }) 126 -}
M
journal/semantic/build_test.go
→ internal/analyzer/build_test.go
··· 1 -package semantic 1 +package analyzer 2 2 3 3 import ( 4 4 "bytes"
D
journal/semantic/context.go
··· 1 -package semantic 2 - 3 -import ( 4 - "olexsmir.xyz/clerk/journal" 5 - "olexsmir.xyz/clerk/journal/ast" 6 -) 7 - 8 -// Context holds workspace-level semantic data built from parsed journal files. 9 -type Context struct { 10 - // Files in dependency order (includes before includers). 11 - Files []*journal.ParsedFile 12 - 13 - Accounts map[string]*AccountInfo 14 - Commodities map[string]*CommodityInfo 15 - Payees map[string]*PayeeInfo 16 -} 17 - 18 -type AccountInfo struct { 19 - // len == 0 = account is used but never declared (undeclared) 20 - // len > 1 = account is declared more than once (duplicated) 21 - // len > 0 = account appears in a posting 22 - Directives []*ast.AccountDirective 23 - Usages []AccountUsage 24 -} 25 - 26 -// AccountUsage is a single posting that references an account. 27 -type AccountUsage struct { 28 - FileIndex int 29 - Posting *ast.Posting 30 -} 31 - 32 -// CommodityInfo tracks all declarations and usages for one commodity. 33 -type CommodityInfo struct { 34 - Directives []*ast.CommodityDirective 35 - Usages []CommodityUsage 36 -} 37 - 38 -// CommodityUsage is a single amount that references a commodity. 39 -type CommodityUsage struct { 40 - FileIndex int 41 - Amount *ast.Amount 42 -} 43 - 44 -type PayeeInfo struct { 45 - Directives []*ast.Payee 46 - Usage []PayeeUsage 47 -} 48 - 49 -type PayeeUsage struct { 50 - FileIndex int 51 - Payee *ast.Payee 52 -}
D
journal/semantic/dump.go
··· 1 -package semantic 2 - 3 -import ( 4 - "fmt" 5 - "io" 6 - "path/filepath" 7 - "sort" 8 -) 9 - 10 -func fprint(w io.Writer, ctx *Context) { 11 - // Files 12 - fmt.Fprintf(w, "files (%d):\n", len(ctx.Files)) 13 - for i, pf := range ctx.Files { 14 - fmt.Fprintf(w, " %d: %s\n", i, filepath.Base(pf.Path)) 15 - } 16 - 17 - // Commodities 18 - csyms := make([]string, 0, len(ctx.Commodities)) 19 - for sym := range ctx.Commodities { 20 - csyms = append(csyms, sym) 21 - } 22 - sort.Strings(csyms) 23 - 24 - fmt.Fprintf(w, "\ncommodities (%d):\n", len(ctx.Commodities)) 25 - for _, sym := range csyms { 26 - info := ctx.Commodities[sym] 27 - fmt.Fprintf(w, " %s\n", sym) 28 - fmt.Fprintf(w, " directives: %d\n", len(info.Directives)) 29 - for _, cd := range info.Directives { 30 - fmt.Fprintf(w, " commodity %s\n", cd.Commodity) 31 - } 32 - fmt.Fprintf(w, " usages: %d\n", len(info.Usages)) 33 - for _, u := range info.Usages { 34 - fmt.Fprintf(w, " file %d: %s (%q)\n", u.FileIndex, u.Amount.Commodity, u.Amount.Quantity.String()) 35 - } 36 - } 37 - 38 - // Accounts 39 - anames := make([]string, 0, len(ctx.Accounts)) 40 - for name := range ctx.Accounts { 41 - anames = append(anames, name) 42 - } 43 - sort.Strings(anames) 44 - 45 - fmt.Fprintf(w, "\naccounts (%d):\n", len(ctx.Accounts)) 46 - for _, name := range anames { 47 - info := ctx.Accounts[name] 48 - fmt.Fprintf(w, " %s\n", name) 49 - fmt.Fprintf(w, " directives: %d\n", len(info.Directives)) 50 - for _, d := range info.Directives { 51 - fmt.Fprintf(w, " account %s\n", d.Account.String()) 52 - } 53 - fmt.Fprintf(w, " usages: %d\n", len(info.Usages)) 54 - for _, u := range info.Usages { 55 - fmt.Fprintf(w, " file %d: %s\n", u.FileIndex, u.Posting.Account.String()) 56 - } 57 - } 58 - 59 - // Payees 60 - pnames := make([]string, 0, len(ctx.Payees)) 61 - for name := range ctx.Payees { 62 - pnames = append(pnames, name) 63 - } 64 - sort.Strings(pnames) 65 - 66 - fmt.Fprintf(w, "\npayees (%d):\n", len(ctx.Payees)) 67 - for _, name := range pnames { 68 - info := ctx.Payees[name] 69 - fmt.Fprintf(w, " %s\n", name) 70 - fmt.Fprintf(w, " directives: %d\n", len(info.Directives)) 71 - for _, d := range info.Directives { 72 - fmt.Fprintf(w, " payee %s\n", d.Name) 73 - } 74 - fmt.Fprintf(w, " usages: %d\n", len(info.Usage)) 75 - for _, u := range info.Usage { 76 - fmt.Fprintf(w, " file %d: %s\n", u.FileIndex, u.Payee.Name) 77 - } 78 - } 79 -}
D
journal/semantic/testdata/include.txtar
··· 1 --- accounts.journal -- 2 -account expenses:food 3 - 4 --- in.journal -- 5 -include accounts.journal 6 - 7 -2024/01/01 ! test 8 - expenses:food $10 9 - assets:checking 10 - 11 --- expect -- 12 -files (2): 13 - 0: accounts.journal 14 - 1: in.journal 15 - 16 -commodities (1): 17 - $ 18 - directives: 0 19 - usages: 1 20 - file 1: $ ("10") 21 - 22 -accounts (2): 23 - assets:checking 24 - directives: 0 25 - usages: 1 26 - file 1: assets:checking 27 - expenses:food 28 - directives: 1 29 - account expenses:food 30 - usages: 1 31 - file 1: expenses:food 32 - 33 -payees (1): 34 - test 35 - directives: 0 36 - usages: 1 37 - file 1: test
D
journal/semantic/testdata/journal.txtar
··· 1 --- in.journal -- 2 -account expenses:food 3 -account expenses:food 4 -account expenses:transport 5 - 6 -commodity $ 7 -commodity $ 8 -commodity EUR 9 - 10 -~ monthly 11 - expenses:transport $50 12 - assets:checking 13 - 14 -= expr:big 15 - expenses:food $100 16 - assets:credit 17 - 18 -2024/01/01 groceries 19 - expenses:food $10 20 - assets:checking 21 - 22 -2024/01/02 lunch 23 - expenses:food $5 24 - assets:cash 25 - 26 -2024/01/04 aws 27 - expenses:food 1000000.00 USD 28 - assets:cash 29 - 30 --- expect -- 31 -files (1): 32 - 0: in.journal 33 - 34 -commodities (3): 35 - $ 36 - directives: 2 37 - commodity $ 38 - commodity $ 39 - usages: 4 40 - file 0: $ ("50") 41 - file 0: $ ("100") 42 - file 0: $ ("10") 43 - file 0: $ ("5") 44 - EUR 45 - directives: 1 46 - commodity EUR 47 - usages: 0 48 - USD 49 - directives: 0 50 - usages: 1 51 - file 0: USD ("1000000") 52 - 53 -accounts (5): 54 - assets:cash 55 - directives: 0 56 - usages: 2 57 - file 0: assets:cash 58 - file 0: assets:cash 59 - assets:checking 60 - directives: 0 61 - usages: 2 62 - file 0: assets:checking 63 - file 0: assets:checking 64 - assets:credit 65 - directives: 0 66 - usages: 1 67 - file 0: assets:credit 68 - expenses:food 69 - directives: 2 70 - account expenses:food 71 - account expenses:food 72 - usages: 4 73 - file 0: expenses:food 74 - file 0: expenses:food 75 - file 0: expenses:food 76 - file 0: expenses:food 77 - expenses:transport 78 - directives: 1 79 - account expenses:transport 80 - usages: 1 81 - file 0: expenses:transport 82 - 83 -payees (3): 84 - aws 85 - directives: 0 86 - usages: 1 87 - file 0: aws 88 - groceries 89 - directives: 0 90 - usages: 1 91 - file 0: groceries 92 - lunch 93 - directives: 0 94 - usages: 1 95 - file 0: lunch