Compare commits

...

4 Commits

Author SHA1 Message Date
Bel LaPointe
d093db1a2b cli supports bal again!
All checks were successful
cicd / ci (push) Successful in 1m35s
2025-04-03 11:55:19 -06:00
Bel LaPointe
30a0414dcd evalulate acc = VALUE in date, filename, lineno order 2025-04-03 11:55:06 -06:00
Bel LaPointe
1f9919a172 deltas try to remember filename, lineno 2025-04-03 11:54:29 -06:00
Bel LaPointe
fb1ddc72c3 ledger.Balances accept Like, Group kinda 2025-04-03 11:53:33 -06:00
11 changed files with 150 additions and 34 deletions

View File

@@ -2,6 +2,7 @@ package cli
type Config struct { type Config struct {
Files FileList Files FileList
BPI string
Query struct { Query struct {
Period Period Period Period
Sort string Sort string

View File

@@ -24,6 +24,7 @@ func Main() {
fs.BoolVar(&config.Query.Reverse, "r", false, "reverse printed accounts") fs.BoolVar(&config.Query.Reverse, "r", false, "reverse printed accounts")
fs.BoolVar(&config.Query.Normalize, "n", false, "normalize with default normalizer") fs.BoolVar(&config.Query.Normalize, "n", false, "normalize with default normalizer")
fs.BoolVar(&config.Query.NoExchanging, "no-exchanging", true, "omit currency exchanges") fs.BoolVar(&config.Query.NoExchanging, "no-exchanging", true, "omit currency exchanges")
fs.StringVar(&config.BPI, "bpi", "", "path to bpi")
if err := fs.Parse(os.Args[1:]); err != nil { if err := fs.Parse(os.Args[1:]); err != nil {
panic(err) panic(err)
} }
@@ -72,28 +73,31 @@ func Main() {
} }
group := ledger.GroupName(pattern) group := ledger.GroupName(pattern)
bpis := make(ledger.BPIs)
if config.BPI != "" {
b, err := ledger.NewBPIs(config.BPI)
if err != nil {
panic(err)
}
bpis = b
}
if config.Query.Normalize { if config.Query.Normalize {
deltas = ana.NewDefaultNormalizer().Normalize(deltas) deltas = ana.NewDefaultNormalizer().Normalize(deltas)
} }
switch cmd[:3] { switch cmd[:3] {
case "bal": case "bal":
/* balances := deltas.Balances().
balances := deltas.Balances() WithBPIs(bpis).
if likePattern != "" { KindaLike(q).
balances = balances.Like(likePattern) KindaGroup(group)
}
if notLikePattern != "" {
balances = balances.NotLike(notLikePattern)
}
FPrintBalances(os.Stdout, "", balances, nil) FPrintBalances(os.Stdout, "", balances, nil)
*/
case "reg": case "reg":
transactions := deltas.Transactions() transactions := deltas.Transactions()
for i, transaction := range transactions { for i, transaction := range transactions {
balances := ledger.Deltas(transaction).Like(q).Group(group).Balances() balances := ledger.Deltas(transaction).Like(q).Group(group).Balances().WithBPIs(bpis)
shouldPrint := false shouldPrint := false
shouldPrint = shouldPrint || len(balances) > 2 shouldPrint = shouldPrint || len(balances) > 2
if config.Query.NoExchanging { if config.Query.NoExchanging {

View File

@@ -4,7 +4,9 @@ import (
"fmt" "fmt"
"maps" "maps"
"regexp" "regexp"
"sort"
"strings" "strings"
"time"
) )
type Balances map[string]Balance type Balances map[string]Balance
@@ -46,6 +48,34 @@ func (balances Balances) NotLike(pattern string) Balances {
return result return result
} }
func (balances Balances) KindaLike(like Like) Balances {
ks := []string{}
for k := range balances {
ks = append(ks, k)
}
sort.Strings(ks)
result := make(Balances)
for _, k := range ks {
v := balances[k]
if like(v.kinda(k)) {
result[k] = maps.Clone(v)
}
}
return result
}
func (b Balance) kinda(k string) Delta {
return Delta{
Date: time.Now().Format("2006-01-02"),
Name: k,
Value: b[USD],
Description: k,
Transaction: k,
Payee: false,
}
}
func (balances Balances) Like(pattern string) Balances { func (balances Balances) Like(pattern string) Balances {
result := make(Balances) result := make(Balances)
p := regexp.MustCompile(pattern) p := regexp.MustCompile(pattern)
@@ -80,22 +110,57 @@ func (balances Balances) Group(pattern string) Balances {
return result return result
} }
func (balances Balances) KindaGroup(group Group) Balances {
ks := []string{}
for k := range balances {
ks = append(ks, k)
}
sort.Strings(ks)
result := make(Balances)
for _, k := range ks {
v := balances[k]
k2 := k
if k3 := group(v.kinda(k)).Name; k2 != k3 {
k2 = k3
}
if _, ok := result[k2]; !ok {
result[k2] = make(Balance)
}
for k3, v3 := range v {
result[k2][k3] += v3
}
}
return result
}
func (balances Balances) WithBPIs(bpis BPIs) Balances { func (balances Balances) WithBPIs(bpis BPIs) Balances {
return balances.WithBPIsAt(bpis, "9") return balances.WithBPIsAt(bpis, "9")
} }
func (balances Balances) WithBPIsAt(bpis BPIs, date string) Balances { func (balances Balances) WithBPIsAt(bpis BPIs, date string) Balances {
ks := []string{}
for k := range balances {
ks = append(ks, k)
}
sort.Strings(ks)
result := make(Balances) result := make(Balances)
for k, v := range balances { for _, k := range ks {
v := balances[k]
if _, ok := result[k]; !ok { if _, ok := result[k]; !ok {
result[k] = make(Balance) result[k] = make(Balance)
} }
for k2, v2 := range v { for k2, v2 := range v {
scalar := 1.0 if k2 == USD {
if k2 != USD { result[k][USD] = result[k][USD] + v2
scalar = bpis[k2].Lookup(date) } else if scalar := bpis[k2].Lookup(date); scalar != nil {
result[k][USD] = result[k][USD] + *scalar*v2
} else {
result[k][k2] = result[k][k2] + v2
} }
result[k][USD] += v2 * scalar
} }
} }
return result return result

View File

@@ -50,7 +50,7 @@ func NewBPIs(p string) (BPIs, error) {
} }
} }
func (bpi BPI) Lookup(date string) float64 { func (bpi BPI) Lookup(date string) *float64 {
var closestWithoutGoingOver string var closestWithoutGoingOver string
for k := range bpi { for k := range bpi {
if k <= date && k > closestWithoutGoingOver { if k <= date && k > closestWithoutGoingOver {
@@ -58,7 +58,8 @@ func (bpi BPI) Lookup(date string) float64 {
} }
} }
if closestWithoutGoingOver == "" { if closestWithoutGoingOver == "" {
return 0 return nil
} }
return bpi[closestWithoutGoingOver] f := bpi[closestWithoutGoingOver]
return &f
} }

View File

@@ -157,16 +157,16 @@ P 2023-10-22 07:33:56 GME $17.18
t.Fatal(err) t.Fatal(err)
} }
if got := got["USDC"].Lookup("2099-01-01"); got != 0 { if got := got["USDC"].Lookup("2099-01-01"); got != nil {
t.Error("default got != 0:", got) t.Error("default got != 0:", got)
} }
if got := got["GME"].Lookup("2099-01-01"); got != 17.18 { if got := got["GME"].Lookup("2099-01-01"); got == nil || *got != 17.18 {
t.Errorf("shouldve returned latest but got %v", got) t.Errorf("shouldve returned latest but got %v", got)
} }
if got := got["GME"].Lookup("2023-09-19"); got != 18.22 { if got := got["GME"].Lookup("2023-09-19"); got == nil || *got != 18.22 {
t.Errorf("shouldve returned one day before but got %v", got) t.Errorf("shouldve returned one day before but got %v", got)
} }
if got := got["GME"].Lookup("2023-09-18"); got != 18.22 { if got := got["GME"].Lookup("2023-09-18"); got == nil || *got != 18.22 {
t.Errorf("shouldve returned day of but got %v", got) t.Errorf("shouldve returned day of but got %v", got)
} }
t.Log(got) t.Log(got)

View File

@@ -14,29 +14,43 @@ type Delta struct {
Value float64 Value float64
Currency Currency Currency Currency
Description string Description string
isSet bool
Transaction string Transaction string
Payee bool Payee bool
isSet bool
fileName string
lineNo int
} }
func newDelta(transaction string, payee bool, d, desc, name string, v float64, c string, isSet bool) Delta { func newDelta(transaction string, payee bool, d, desc, name string, v float64, c string, isSet bool, fileName string, lineNo int) Delta {
return Delta{ return Delta{
Date: d, Date: d,
Name: name, Name: name,
Value: v, Value: v,
Currency: Currency(c), Currency: Currency(c),
Description: desc, Description: desc,
isSet: isSet,
Transaction: transaction, Transaction: transaction,
Payee: payee, Payee: payee,
isSet: isSet,
fileName: fileName,
lineNo: lineNo,
} }
} }
func (delta Delta) equivalent(other Delta) bool {
delta.fileName = ""
delta.lineNo = 0
other.fileName = ""
other.lineNo = 0
return delta == other
}
func (delta Delta) Debug() string { func (delta Delta) Debug() string {
return fmt.Sprintf("{@%s %s(payee=%v):\"%s\" %s%.2f %s}", delta.Date, delta.Name, delta.Payee, delta.Description, func() string { return fmt.Sprintf("{@%s %s(payee=%v):\"%s\" %s%.2f %s @%s#%d}", delta.Date, delta.Name, delta.Payee, delta.Description, func() string {
if !delta.isSet { if !delta.isSet {
return "" return ""
} }
return "= " return "= "
}(), delta.Value, delta.Currency) }(), delta.Value, delta.Currency, delta.fileName, delta.lineNo)
} }

View File

@@ -6,7 +6,7 @@ import (
func TestDelta(t *testing.T) { func TestDelta(t *testing.T) {
d := "2099-08-07" d := "2099-08-07"
delta := newDelta("x", true, d, "", "name", 34.56, "$", false) delta := newDelta("x", true, d, "", "name", 34.56, "$", false, "", 0)
if delta.Transaction != "x" { if delta.Transaction != "x" {
t.Error(delta.Transaction) t.Error(delta.Transaction)

View File

@@ -63,6 +63,5 @@ func (deltas Deltas) Balances() Balances {
} }
} }
} }
return result return result
} }

View File

@@ -7,7 +7,9 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"slices"
"sort" "sort"
"strings"
"unicode" "unicode"
) )
@@ -174,6 +176,12 @@ func (files Files) Deltas(like ...Like) (Deltas, error) {
for _, transaction := range transactions { for _, transaction := range transactions {
result = append(result, transaction.deltas()...) result = append(result, transaction.deltas()...)
} }
slices.SortFunc(result, func(a, b Delta) int {
if str := strings.Compare(a.Date+a.fileName, b.Date+b.fileName); str != 0 {
return str
}
return a.lineNo - b.lineNo
})
balances := make(Balances) balances := make(Balances)
for i := range result { for i := range result {

View File

@@ -145,7 +145,7 @@ func TestFileAmend(t *testing.T) {
} else if filtered := deltas.Like(func(d Delta) bool { } else if filtered := deltas.Like(func(d Delta) bool {
c.old.Transaction = d.Transaction c.old.Transaction = d.Transaction
c.old.Payee = d.Payee c.old.Payee = d.Payee
return d == c.old return d.equivalent(c.old)
}); len(filtered) != 1 { }); len(filtered) != 1 {
t.Fatalf("input \n\t%s \ndidnt include old \n\t%+v \nin \n\t%+v: \n\t%+v", c.from, c.old, deltas, filtered) t.Fatalf("input \n\t%s \ndidnt include old \n\t%+v \nin \n\t%+v: \n\t%+v", c.from, c.old, deltas, filtered)
} }
@@ -373,6 +373,9 @@ func TestFileDeltas(t *testing.T) {
Currency: USD, Currency: USD,
Description: "Electricity / Power Bill TG2PJ-2PLP5", Description: "Electricity / Power Bill TG2PJ-2PLP5",
Payee: true, Payee: true,
fileName: "",
lineNo: 0,
}, },
{ {
Date: "2022-12-12", Date: "2022-12-12",
@@ -380,6 +383,9 @@ func TestFileDeltas(t *testing.T) {
Value: 97.92, Value: 97.92,
Currency: USD, Currency: USD,
Description: "Electricity / Power Bill TG2PJ-2PLP5", Description: "Electricity / Power Bill TG2PJ-2PLP5",
fileName: "",
lineNo: 0,
}, },
{ {
Date: "2022-12-12", Date: "2022-12-12",
@@ -388,6 +394,9 @@ func TestFileDeltas(t *testing.T) {
Currency: USD, Currency: USD,
Description: "Test pay chase TG32S-BT2FF", Description: "Test pay chase TG32S-BT2FF",
Payee: true, Payee: true,
fileName: "",
lineNo: 0,
}, },
{ {
Date: "2022-12-12", Date: "2022-12-12",
@@ -395,6 +404,9 @@ func TestFileDeltas(t *testing.T) {
Value: 1.00, Value: 1.00,
Currency: USD, Currency: USD,
Description: "Test pay chase TG32S-BT2FF", Description: "Test pay chase TG32S-BT2FF",
fileName: "",
lineNo: 0,
}, },
} }
@@ -407,7 +419,8 @@ func TestFileDeltas(t *testing.T) {
for name, d := range cases { for name, d := range cases {
want := d want := d
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
f, err := NewFiles("./testdata/" + name + ".dat") fileName := "./testdata/" + name + ".dat"
f, err := NewFiles(fileName)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -421,6 +434,8 @@ func TestFileDeltas(t *testing.T) {
t.Error(len(deltas)) t.Error(len(deltas))
} }
for i := range want { for i := range want {
want[i].fileName = fileName
deltas[i].lineNo = 0
if i >= len(deltas) { if i >= len(deltas) {
break break
} }

View File

@@ -110,6 +110,9 @@ type transaction struct {
payee string payee string
recipients []transactionRecipient recipients []transactionRecipient
name string name string
fileName string
lineNo int
} }
func (t transaction) empty() bool { func (t transaction) empty() bool {
@@ -126,7 +129,7 @@ type transactionRecipient struct {
func (t transaction) deltas() Deltas { func (t transaction) deltas() Deltas {
result := []Delta{} result := []Delta{}
sums := map[string]float64{} sums := map[string]float64{}
for _, recipient := range t.recipients { for i, recipient := range t.recipients {
sums[recipient.currency] += recipient.value sums[recipient.currency] += recipient.value
result = append(result, newDelta( result = append(result, newDelta(
t.name, t.name,
@@ -137,6 +140,8 @@ func (t transaction) deltas() Deltas {
recipient.value, recipient.value,
recipient.currency, recipient.currency,
recipient.isSet, recipient.isSet,
t.fileName,
t.lineNo+i,
)) ))
} }
for currency, value := range sums { for currency, value := range sums {
@@ -155,6 +160,8 @@ func (t transaction) deltas() Deltas {
-1.0*value, -1.0*value,
currency, currency,
false, false,
t.fileName,
t.lineNo,
)) ))
} }
} }
@@ -194,6 +201,8 @@ func (files Files) _transactions(file string) ([]transaction, error) {
name := fmt.Sprintf("%s/%d", file, len(result)) name := fmt.Sprintf("%s/%d", file, len(result))
one, err := readTransaction(name, r) one, err := readTransaction(name, r)
if !one.empty() { if !one.empty() {
one.fileName = file
one.lineNo = len(result)
result = append(result, one) result = append(result, one)
} }
if err == io.EOF { if err == io.EOF {