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 {
Files FileList
BPI string
Query struct {
Period Period
Sort string

View File

@@ -24,6 +24,7 @@ func Main() {
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.NoExchanging, "no-exchanging", true, "omit currency exchanges")
fs.StringVar(&config.BPI, "bpi", "", "path to bpi")
if err := fs.Parse(os.Args[1:]); err != nil {
panic(err)
}
@@ -72,28 +73,31 @@ func Main() {
}
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 {
deltas = ana.NewDefaultNormalizer().Normalize(deltas)
}
switch cmd[:3] {
case "bal":
/*
balances := deltas.Balances()
if likePattern != "" {
balances = balances.Like(likePattern)
}
if notLikePattern != "" {
balances = balances.NotLike(notLikePattern)
}
FPrintBalances(os.Stdout, "", balances, nil)
*/
balances := deltas.Balances().
WithBPIs(bpis).
KindaLike(q).
KindaGroup(group)
FPrintBalances(os.Stdout, "", balances, nil)
case "reg":
transactions := deltas.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 = shouldPrint || len(balances) > 2
if config.Query.NoExchanging {

View File

@@ -4,7 +4,9 @@ import (
"fmt"
"maps"
"regexp"
"sort"
"strings"
"time"
)
type Balances map[string]Balance
@@ -46,6 +48,34 @@ func (balances Balances) NotLike(pattern string) Balances {
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 {
result := make(Balances)
p := regexp.MustCompile(pattern)
@@ -80,22 +110,57 @@ func (balances Balances) Group(pattern string) Balances {
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 {
return balances.WithBPIsAt(bpis, "9")
}
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)
for k, v := range balances {
for _, k := range ks {
v := balances[k]
if _, ok := result[k]; !ok {
result[k] = make(Balance)
}
for k2, v2 := range v {
scalar := 1.0
if k2 != USD {
scalar = bpis[k2].Lookup(date)
if k2 == USD {
result[k][USD] = result[k][USD] + v2
} 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

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
for k := range bpi {
if k <= date && k > closestWithoutGoingOver {
@@ -58,7 +58,8 @@ func (bpi BPI) Lookup(date string) float64 {
}
}
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)
}
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)
}
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)
}
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)
}
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.Log(got)

View File

@@ -14,29 +14,43 @@ type Delta struct {
Value float64
Currency Currency
Description string
isSet bool
Transaction string
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{
Date: d,
Name: name,
Value: v,
Currency: Currency(c),
Description: desc,
isSet: isSet,
Transaction: transaction,
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 {
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 {
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) {
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" {
t.Error(delta.Transaction)

View File

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

View File

@@ -7,7 +7,9 @@ import (
"os"
"path"
"path/filepath"
"slices"
"sort"
"strings"
"unicode"
)
@@ -174,6 +176,12 @@ func (files Files) Deltas(like ...Like) (Deltas, error) {
for _, transaction := range transactions {
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)
for i := range result {

View File

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

View File

@@ -110,6 +110,9 @@ type transaction struct {
payee string
recipients []transactionRecipient
name string
fileName string
lineNo int
}
func (t transaction) empty() bool {
@@ -126,7 +129,7 @@ type transactionRecipient struct {
func (t transaction) deltas() Deltas {
result := []Delta{}
sums := map[string]float64{}
for _, recipient := range t.recipients {
for i, recipient := range t.recipients {
sums[recipient.currency] += recipient.value
result = append(result, newDelta(
t.name,
@@ -137,6 +140,8 @@ func (t transaction) deltas() Deltas {
recipient.value,
recipient.currency,
recipient.isSet,
t.fileName,
t.lineNo+i,
))
}
for currency, value := range sums {
@@ -155,6 +160,8 @@ func (t transaction) deltas() Deltas {
-1.0*value,
currency,
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))
one, err := readTransaction(name, r)
if !one.empty() {
one.fileName = file
one.lineNo = len(result)
result = append(result, one)
}
if err == io.EOF {