Compare commits
5 Commits
0eb0abf4a8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7262f5f69b | ||
|
|
b2954c0461 | ||
|
|
03b9a6d1f1 | ||
|
|
fa7bafa241 | ||
|
|
502e47d0bc |
@@ -18,4 +18,5 @@ type Config struct {
|
||||
Compact bool
|
||||
GroupDate string
|
||||
NoPercent bool
|
||||
CSV string
|
||||
}
|
||||
|
||||
163
cmd/cli/main.go
163
cmd/cli/main.go
@@ -3,14 +3,17 @@ package cli
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"maps"
|
||||
"math"
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -35,6 +38,7 @@ func Main() {
|
||||
fs.BoolVar(&config.Query.NoRounding, "no-rounding", false, "no rounding")
|
||||
fs.BoolVar(&config.Compact, "c", false, "reg entries oneline")
|
||||
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.BoolVar(&config.Query.Normalize, "n", false, "normalize with default normalizer")
|
||||
fs.BoolVar(&config.Query.USDOnly, "usd", false, "filter to usd")
|
||||
@@ -50,27 +54,27 @@ func Main() {
|
||||
|
||||
files := config.Files.Strings()
|
||||
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:]...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
positional := fs.Args()
|
||||
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]
|
||||
|
||||
q, err := BuildQuery(config, positional[1:])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
deltas, err := ledgerFiles.Deltas()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
if period := config.Query.Period; !period.Empty() {
|
||||
@@ -98,7 +102,7 @@ func Main() {
|
||||
if config.BPI != "" {
|
||||
b, err := ledger.NewBPIs(config.BPI)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
bpis = b
|
||||
if period := config.Query.Period; !period.Empty() {
|
||||
@@ -115,12 +119,12 @@ func Main() {
|
||||
if config.CPI != "" && config.CPIYear > 0 {
|
||||
c, err := ledger.NewBPIs(config.CPI)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
cpi := c["CPI"]
|
||||
cpiy := cpi.Lookup(fmt.Sprintf("%d-06-01", config.CPIYear))
|
||||
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 {
|
||||
@@ -261,7 +265,7 @@ func Main() {
|
||||
}
|
||||
fmt.Println(asciigraph.PlotMany(points, options...))
|
||||
case "rec": // reconcile via teller // DEAD
|
||||
panic("dead and bad")
|
||||
log.Fatalf("dead and bad")
|
||||
byDate := map[string]ledger.Deltas{}
|
||||
for _, delta := range deltas {
|
||||
delta := delta
|
||||
@@ -273,13 +277,13 @@ func Main() {
|
||||
|
||||
teller, err := teller.New()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
client := cache.New(teller)
|
||||
|
||||
accounts, err := client.Accounts(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
inDay := func(date string, transaction bank.Transaction) bool {
|
||||
@@ -294,7 +298,7 @@ func Main() {
|
||||
for _, acc := range accounts {
|
||||
transactions, err := client.Transactions(ctx, acc)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
for _, transaction := range transactions {
|
||||
@@ -306,7 +310,7 @@ func Main() {
|
||||
|
||||
ts, err := time.ParseInLocation("2006-01-02", transaction.Date, time.Local)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
dayBefore := ts.Add(-24 * time.Hour).Format("2006-01-02")
|
||||
dayAfter := ts.Add(24 * time.Hour).Format("2006-01-02")
|
||||
@@ -343,8 +347,127 @@ 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")
|
||||
}
|
||||
}
|
||||
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")
|
||||
})
|
||||
|
||||
csvDeltas := make(ledger.Deltas, 0)
|
||||
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]
|
||||
|
||||
amountF, err := strconv.ParseFloat(amount, 32)
|
||||
if err != nil {
|
||||
log.Fatalf("non-float amount %q: %v", amount, err)
|
||||
}
|
||||
csvDeltas = append(csvDeltas, ledger.Delta{
|
||||
Date: dates[0],
|
||||
OtherDates: dates[1:],
|
||||
Name: desc,
|
||||
Value: amountF,
|
||||
Currency: ledger.USD,
|
||||
Description: desc,
|
||||
Payee: true,
|
||||
})
|
||||
}
|
||||
|
||||
matchies := func(deltas ledger.Deltas, delta ledger.Delta) (ledger.Deltas, []string) {
|
||||
deltas = slices.Clone(deltas)
|
||||
dates := append([]string{delta.Date}, delta.OtherDates...)
|
||||
matches := deltas.Like(func(delta ledger.Delta) bool {
|
||||
return delta.Date >= slices.Min(dates)
|
||||
})
|
||||
matches = matches.Like(func(delta ledger.Delta) bool {
|
||||
return delta.Date <= slices.Max(dates)
|
||||
})
|
||||
matches = matches.Like(func(delta ledger.Delta) bool {
|
||||
return delta.Currency == ledger.USD
|
||||
})
|
||||
return matches.Like(func(d2 ledger.Delta) bool {
|
||||
return fmt.Sprintf("%.2f", d2.Value) == fmt.Sprintf("%.2f", delta.Value)
|
||||
}), dates
|
||||
}
|
||||
|
||||
datesMatched := []string{}
|
||||
namesMatched := []string{}
|
||||
for _, csvDelta := range csvDeltas {
|
||||
matches, dates := matchies(deltas, csvDelta)
|
||||
if len(matches) == 0 {
|
||||
fmt.Printf("unique to csv | %s %s %.2f\n", strings.Join(dates, "="), csvDelta.Name, csvDelta.Value)
|
||||
}
|
||||
for _, match := range matches {
|
||||
datesMatched = append(datesMatched, match.Date)
|
||||
datesMatched = append(datesMatched, match.OtherDates...)
|
||||
namesMatched = append(namesMatched, match.Name)
|
||||
}
|
||||
}
|
||||
datesMatched = slices.DeleteFunc(datesMatched, func(a string) bool { return strings.TrimSpace(a) == "" })
|
||||
datesMatched = slices.Compact(datesMatched)
|
||||
namesMatched = slices.Compact(namesMatched)
|
||||
|
||||
deltas = deltas.Like(func(delta ledger.Delta) bool {
|
||||
return delta.Date >= slices.Min(datesMatched)
|
||||
})
|
||||
deltas = deltas.Like(func(delta ledger.Delta) bool {
|
||||
return delta.Date <= slices.Max(datesMatched)
|
||||
})
|
||||
deltas = deltas.Like(func(delta ledger.Delta) bool {
|
||||
return slices.Contains(namesMatched, delta.Name)
|
||||
})
|
||||
|
||||
deltasSum := deltas.Group(ledger.GroupDate(""), ledger.GroupName("")).Balances()[""][ledger.USD]
|
||||
csvSum := csvDeltas.Group(ledger.GroupDate(""), ledger.GroupName("")).Balances()[""][ledger.USD]
|
||||
if deltasSum != csvSum {
|
||||
log.Printf("csv sum %.2f but deltas sum %.2f", csvSum, deltasSum)
|
||||
}
|
||||
|
||||
for _, delta := range deltas {
|
||||
matches, dates := matchies(csvDeltas, delta)
|
||||
if len(matches) == 0 {
|
||||
fmt.Printf("unique to ledger | %s %s %.2f\n", strings.Join(dates, "="), delta.Description, delta.Value)
|
||||
}
|
||||
}
|
||||
default:
|
||||
panic("unknown command " + positional[0])
|
||||
log.Fatalf("unknown command %q", positional[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,3 +573,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 ""
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ const (
|
||||
|
||||
type Delta struct {
|
||||
Date string
|
||||
OtherDates []string
|
||||
Name string
|
||||
Value float64
|
||||
Currency Currency
|
||||
@@ -23,9 +24,10 @@ type Delta struct {
|
||||
with []Delta
|
||||
}
|
||||
|
||||
func newDelta(transaction string, payee bool, d, desc, name string, v float64, c string, isSet bool, fileName string, lineNo int) Delta {
|
||||
func newDelta(transaction string, payee bool, d, desc, name string, v float64, c string, isSet bool, fileName string, lineNo int, otherDates []string) Delta {
|
||||
return Delta{
|
||||
Date: d,
|
||||
OtherDates: otherDates,
|
||||
Name: name,
|
||||
Value: v,
|
||||
Currency: Currency(c),
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
func TestDelta(t *testing.T) {
|
||||
d := "2099-08-07"
|
||||
delta := newDelta("x", true, d, "", "name", 34.56, "$", false, "", 0)
|
||||
delta := newDelta("x", true, d, "", "name", 34.56, "$", false, "", 0, []string{"d2"})
|
||||
|
||||
if delta.Transaction != "x" {
|
||||
t.Error(delta.Transaction)
|
||||
@@ -17,6 +17,9 @@ func TestDelta(t *testing.T) {
|
||||
if delta.Date != d {
|
||||
t.Error(delta.Date)
|
||||
}
|
||||
if delta.OtherDates[0] != "d2" {
|
||||
t.Error(delta.OtherDates)
|
||||
}
|
||||
if delta.Name != "name" {
|
||||
t.Error(delta.Name)
|
||||
}
|
||||
|
||||
@@ -99,8 +99,12 @@ func (files Files) add(payee string, delta Delta) error {
|
||||
if delta.Currency != USD {
|
||||
currencyValue = fmt.Sprintf("%.2f %s", delta.Value, delta.Currency)
|
||||
}
|
||||
date := delta.Date
|
||||
for _, otherDate := range delta.OtherDates {
|
||||
date += "=" + otherDate
|
||||
}
|
||||
return files.append(fmt.Sprintf("%s %s\n%s%s%s%s\n%s%s",
|
||||
delta.Date, delta.Description,
|
||||
date, delta.Description,
|
||||
filesAppendDelim, delta.Name, filesAppendDelim+filesAppendDelim+filesAppendDelim, currencyValue,
|
||||
filesAppendDelim, payee,
|
||||
))
|
||||
|
||||
@@ -73,12 +73,13 @@ func TestFileAmend(t *testing.T) {
|
||||
},
|
||||
"payee": {
|
||||
from: `
|
||||
2006-01-02 description
|
||||
2006-01-02=2006-01-03 description
|
||||
recipient $3.45
|
||||
payee
|
||||
`,
|
||||
old: Delta{
|
||||
Date: "2006-01-02",
|
||||
OtherDates: []string{"2006-01-03"},
|
||||
Name: "payee",
|
||||
Value: -3.45,
|
||||
Currency: "$",
|
||||
@@ -86,16 +87,17 @@ func TestFileAmend(t *testing.T) {
|
||||
},
|
||||
now: Delta{
|
||||
Date: "2106-11-12",
|
||||
OtherDates: []string{"2006-01-03"},
|
||||
Name: "1payee",
|
||||
Value: -13.45,
|
||||
Currency: "T",
|
||||
Description: "1description",
|
||||
},
|
||||
want: `
|
||||
2006-01-02 description
|
||||
2006-01-02=2006-01-03 description
|
||||
payee $3.45
|
||||
recipient
|
||||
2106-11-12 1description
|
||||
2106-11-12=2006-01-03 1description
|
||||
1payee -13.45 T
|
||||
recipient`,
|
||||
},
|
||||
|
||||
@@ -106,6 +106,7 @@ func (transaction Transaction) Payee() string {
|
||||
|
||||
type transaction struct {
|
||||
date string
|
||||
otherDates []string
|
||||
description string
|
||||
payee string
|
||||
recipients []transactionRecipient
|
||||
@@ -142,6 +143,7 @@ func (t transaction) deltas() Deltas {
|
||||
recipient.isSet,
|
||||
t.fileName,
|
||||
t.lineNo+i,
|
||||
t.otherDates,
|
||||
))
|
||||
}
|
||||
for currency, value := range sums {
|
||||
@@ -162,6 +164,7 @@ func (t transaction) deltas() Deltas {
|
||||
false,
|
||||
t.fileName,
|
||||
t.lineNo,
|
||||
t.otherDates,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -287,16 +290,17 @@ func _readTransaction(name string, r *bufio.Reader) (transaction, error) {
|
||||
return transaction{}, err
|
||||
}
|
||||
|
||||
dateDescriptionPattern := regexp.MustCompile(`^([0-9]+-[0-9]+-[0-9]+)\s+(.*)$`)
|
||||
dateDescriptionMatches := dateDescriptionPattern.FindAllSubmatch(firstLine, 4)
|
||||
dateDescriptionPattern := regexp.MustCompile(`^([0-9]+-[0-9]+-[0-9]+)((=[0-9]+-[0-9]+-[0-9]+)*)\s+(.*)$`)
|
||||
dateDescriptionMatches := dateDescriptionPattern.FindAllStringSubmatch(string(firstLine), 4)
|
||||
if len(dateDescriptionMatches) != 1 {
|
||||
return transaction{}, fmt.Errorf("bad first line: %v matches: %q", len(dateDescriptionMatches), firstLine)
|
||||
} else if len(dateDescriptionMatches[0]) != 3 {
|
||||
} else if len(dateDescriptionMatches[0]) != 5 {
|
||||
return transaction{}, fmt.Errorf("bad first line: %v submatches: %q", len(dateDescriptionMatches[0]), firstLine)
|
||||
}
|
||||
result := transaction{
|
||||
date: string(dateDescriptionMatches[0][1]),
|
||||
description: string(dateDescriptionMatches[0][2]),
|
||||
date: dateDescriptionMatches[0][1],
|
||||
otherDates: strings.Split(strings.Trim(dateDescriptionMatches[0][2], "="), "="),
|
||||
description: dateDescriptionMatches[0][4],
|
||||
name: name,
|
||||
}
|
||||
|
||||
|
||||
@@ -58,12 +58,13 @@ func TestReadTransaction(t *testing.T) {
|
||||
},
|
||||
"verbose": {
|
||||
input: `
|
||||
2003-04-05 Reasoning here
|
||||
2003-04-05=2003-04-06=2003-04-07 Reasoning here
|
||||
A:B $1.00
|
||||
C:D $-1.00
|
||||
`,
|
||||
want: transaction{
|
||||
date: "2003-04-05",
|
||||
otherDates: []string{"2003-04-06", "2003-04-07"},
|
||||
description: "Reasoning here",
|
||||
payee: "A:B",
|
||||
recipients: []transactionRecipient{
|
||||
|
||||
Reference in New Issue
Block a user