Compare commits
4 Commits
0eaeeec359
...
d093db1a2b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d093db1a2b | ||
|
|
30a0414dcd | ||
|
|
1f9919a172 | ||
|
|
fb1ddc72c3 |
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -63,6 +63,5 @@ func (deltas Deltas) Balances() Balances {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user