This commit is contained in:
@@ -18,4 +18,5 @@ type Config struct {
|
|||||||
Compact bool
|
Compact bool
|
||||||
GroupDate string
|
GroupDate string
|
||||||
NoPercent bool
|
NoPercent bool
|
||||||
|
CSV string
|
||||||
}
|
}
|
||||||
|
|||||||
111
cmd/cli/main.go
111
cmd/cli/main.go
@@ -3,9 +3,11 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/csv"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"maps"
|
"maps"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
@@ -35,6 +37,7 @@ func Main() {
|
|||||||
fs.BoolVar(&config.Query.NoRounding, "no-rounding", false, "no rounding")
|
fs.BoolVar(&config.Query.NoRounding, "no-rounding", false, "no rounding")
|
||||||
fs.BoolVar(&config.Compact, "c", false, "reg entries oneline")
|
fs.BoolVar(&config.Compact, "c", false, "reg entries oneline")
|
||||||
fs.StringVar(&config.Query.With, "w", "", "regexp for transactions")
|
fs.StringVar(&config.Query.With, "w", "", "regexp for transactions")
|
||||||
|
fs.StringVar(&config.CSV, "csv", "", "if csv then the csv")
|
||||||
fs.IntVar(&config.Query.Depth, "depth", 0, "depth grouping")
|
fs.IntVar(&config.Query.Depth, "depth", 0, "depth grouping")
|
||||||
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.USDOnly, "usd", false, "filter to usd")
|
fs.BoolVar(&config.Query.USDOnly, "usd", false, "filter to usd")
|
||||||
@@ -50,27 +53,27 @@ func Main() {
|
|||||||
|
|
||||||
files := config.Files.Strings()
|
files := config.Files.Strings()
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
panic("must specify at least one file")
|
log.Fatalf("must specify at least one file")
|
||||||
}
|
}
|
||||||
ledgerFiles, err := ledger.NewFiles(files[0], files[1:]...)
|
ledgerFiles, err := ledger.NewFiles(files[0], files[1:]...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Fatalf("%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
positional := fs.Args()
|
positional := fs.Args()
|
||||||
if len(positional) == 0 || len(positional[0]) < 3 {
|
if len(positional) == 0 || len(positional[0]) < 3 {
|
||||||
panic("positional arguments required, ie bal|reg PATTERN MATCHING")
|
log.Fatalf("positional arguments required, ie bal|reg PATTERN MATCHING")
|
||||||
}
|
}
|
||||||
cmd := positional[0]
|
cmd := positional[0]
|
||||||
|
|
||||||
q, err := BuildQuery(config, positional[1:])
|
q, err := BuildQuery(config, positional[1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Fatalf("%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
deltas, err := ledgerFiles.Deltas()
|
deltas, err := ledgerFiles.Deltas()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Fatalf("%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if period := config.Query.Period; !period.Empty() {
|
if period := config.Query.Period; !period.Empty() {
|
||||||
@@ -98,7 +101,7 @@ func Main() {
|
|||||||
if config.BPI != "" {
|
if config.BPI != "" {
|
||||||
b, err := ledger.NewBPIs(config.BPI)
|
b, err := ledger.NewBPIs(config.BPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Fatalf("%v", err)
|
||||||
}
|
}
|
||||||
bpis = b
|
bpis = b
|
||||||
if period := config.Query.Period; !period.Empty() {
|
if period := config.Query.Period; !period.Empty() {
|
||||||
@@ -115,12 +118,12 @@ func Main() {
|
|||||||
if config.CPI != "" && config.CPIYear > 0 {
|
if config.CPI != "" && config.CPIYear > 0 {
|
||||||
c, err := ledger.NewBPIs(config.CPI)
|
c, err := ledger.NewBPIs(config.CPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Fatalf("%v", err)
|
||||||
}
|
}
|
||||||
cpi := c["CPI"]
|
cpi := c["CPI"]
|
||||||
cpiy := cpi.Lookup(fmt.Sprintf("%d-06-01", config.CPIYear))
|
cpiy := cpi.Lookup(fmt.Sprintf("%d-06-01", config.CPIYear))
|
||||||
if cpiy == nil {
|
if cpiy == nil {
|
||||||
panic(fmt.Errorf("no cpi for year %d", config.CPIYear))
|
log.Fatalf("no cpi for year %d", config.CPIYear)
|
||||||
}
|
}
|
||||||
|
|
||||||
for date, value := range cpi {
|
for date, value := range cpi {
|
||||||
@@ -261,7 +264,7 @@ func Main() {
|
|||||||
}
|
}
|
||||||
fmt.Println(asciigraph.PlotMany(points, options...))
|
fmt.Println(asciigraph.PlotMany(points, options...))
|
||||||
case "rec": // reconcile via teller // DEAD
|
case "rec": // reconcile via teller // DEAD
|
||||||
panic("dead and bad")
|
log.Fatalf("dead and bad")
|
||||||
byDate := map[string]ledger.Deltas{}
|
byDate := map[string]ledger.Deltas{}
|
||||||
for _, delta := range deltas {
|
for _, delta := range deltas {
|
||||||
delta := delta
|
delta := delta
|
||||||
@@ -273,13 +276,13 @@ func Main() {
|
|||||||
|
|
||||||
teller, err := teller.New()
|
teller, err := teller.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Fatalf("%v", err)
|
||||||
}
|
}
|
||||||
client := cache.New(teller)
|
client := cache.New(teller)
|
||||||
|
|
||||||
accounts, err := client.Accounts(ctx)
|
accounts, err := client.Accounts(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Fatalf("%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
inDay := func(date string, transaction bank.Transaction) bool {
|
inDay := func(date string, transaction bank.Transaction) bool {
|
||||||
@@ -294,7 +297,7 @@ func Main() {
|
|||||||
for _, acc := range accounts {
|
for _, acc := range accounts {
|
||||||
transactions, err := client.Transactions(ctx, acc)
|
transactions, err := client.Transactions(ctx, acc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Fatalf("%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, transaction := range transactions {
|
for _, transaction := range transactions {
|
||||||
@@ -306,7 +309,7 @@ func Main() {
|
|||||||
|
|
||||||
ts, err := time.ParseInLocation("2006-01-02", transaction.Date, time.Local)
|
ts, err := time.ParseInLocation("2006-01-02", transaction.Date, time.Local)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Fatalf("%v", err)
|
||||||
}
|
}
|
||||||
dayBefore := ts.Add(-24 * time.Hour).Format("2006-01-02")
|
dayBefore := ts.Add(-24 * time.Hour).Format("2006-01-02")
|
||||||
dayAfter := ts.Add(24 * time.Hour).Format("2006-01-02")
|
dayAfter := ts.Add(24 * time.Hour).Format("2006-01-02")
|
||||||
@@ -343,8 +346,76 @@ func Main() {
|
|||||||
FPrintBalancesFor(transaction[0].Description, w, "\t\t", balances, cumulative, config.Query.USDOnly, config.Query.Normalize, transaction[0].Date, config.Compact, maxAccW, "%s%.2f")
|
FPrintBalancesFor(transaction[0].Description, w, "\t\t", balances, cumulative, config.Query.USDOnly, config.Query.Normalize, transaction[0].Date, config.Compact, maxAccW, "%s%.2f")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "csv": // reconcile with given csv
|
||||||
|
if config.CSV == "" {
|
||||||
|
log.Fatalf("missing required -csv")
|
||||||
|
}
|
||||||
|
f, err := os.Open(config.CSV)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("cannot open csv %q: %w", config.CSV, err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
reader := csv.NewReader(f)
|
||||||
|
fields, err := reader.Read()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dateIdxs := []int{}
|
||||||
|
for i := range fields {
|
||||||
|
if strings.Contains(strings.ToLower(fields[i]), "date") {
|
||||||
|
dateIdxs = append(dateIdxs, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
descIdx := slices.IndexFunc(fields, func(field string) bool {
|
||||||
|
return strings.Contains(strings.ToLower(field), "desc")
|
||||||
|
})
|
||||||
|
|
||||||
|
amountIdx := slices.IndexFunc(fields, func(field string) bool {
|
||||||
|
return strings.Contains(strings.ToLower(field), "amount")
|
||||||
|
})
|
||||||
|
|
||||||
|
for {
|
||||||
|
line, err := reader.Read()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to read csv line: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dates := []string{}
|
||||||
|
for _, dateIdx := range dateIdxs {
|
||||||
|
dates = append(dates, normalizeDate(line[dateIdx]))
|
||||||
|
}
|
||||||
|
dates = slices.Compact(dates)
|
||||||
|
desc := line[descIdx]
|
||||||
|
amount := line[amountIdx]
|
||||||
|
|
||||||
|
matches := deltas.Like(
|
||||||
|
func(delta ledger.Delta) bool {
|
||||||
|
if delta.Date < slices.Min(dates) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if delta.Date > slices.Max(dates) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if delta.Currency != ledger.USD {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if fmt.Sprintf("%.2f", -1*delta.Value) != amount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
log.Printf("%s %s %s", strings.Join(dates, "="), desc, amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
panic("unknown command " + positional[0])
|
log.Fatalf("unknown command %q", positional[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,3 +521,15 @@ func FPrintBalances(w io.Writer, linePrefix string, balances, cumulatives ledger
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeDate(date string) string {
|
||||||
|
for _, layout := range []string{
|
||||||
|
"01/02/2006",
|
||||||
|
} {
|
||||||
|
if t, err := time.Parse(layout, date); err == nil {
|
||||||
|
return t.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Fatalf("cannot normalize date %q", date)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user