ana-ledger/cmd/cli/main.go

230 lines
6.3 KiB
Go

package cli
import (
"bufio"
"flag"
"fmt"
"io"
"math"
"os"
"slices"
"strings"
"time"
"gogs.inhome.blapointe.com/ana-ledger/src/ana"
"gogs.inhome.blapointe.com/ana-ledger/src/ledger"
)
func Main() {
var config Config
fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
fs.Var(&config.Files, "f", "paths to files")
fs.Var(&config.Query.Period, "period", "period")
fs.StringVar(&config.Query.Sort, "S", "", "sort ie date")
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.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")
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)
}
files := config.Files.Strings()
if len(files) == 0 {
panic("must specify at least one file")
}
ledgerFiles, err := ledger.NewFiles(files[0], files[1:]...)
if err != nil {
panic(err)
}
positional := fs.Args()
if len(positional) == 0 || len(positional[0]) < 3 {
panic("positional arguments required, ie bal|reg PATTERN MATCHING")
}
cmd := positional[0]
q, err := BuildQuery(config, positional[1:])
if err != nil {
panic(err)
}
deltas, err := ledgerFiles.Deltas()
if err != nil {
panic(err)
}
if period := config.Query.Period; !period.Empty() {
after := period.Start.Format("2006-01-02")
before := period.Stop.Format("2006-01-02")
deltas = deltas.Like(
ledger.LikeAfter(after),
ledger.LikeBefore(before),
)
}
pattern := ".*"
if depth := config.Query.Depth; depth > 0 {
patterns := []string{}
pattern = ""
for i := 0; i < depth; i++ {
pattern += "[^:]*:"
patterns = append([]string{strings.Trim(pattern, ":")}, patterns...)
}
pattern = strings.Join(patterns, "|")
}
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)
}
if config.Query.With != "" {
deltas = deltas.Like(ledger.LikeWith(config.Query.With))
}
w := bufio.NewWriter(os.Stdout)
defer w.Flush()
maxAccW := 0
for _, delta := range deltas.Like(q).Group(group) {
if accW := len(delta.Name); accW > maxAccW {
maxAccW = accW
}
}
switch cmd[:3] {
case "bal":
balances := deltas.Balances().
WithBPIs(bpis).
KindaLike(q).
KindaGroup(group).
Nonzero()
FPrintBalances(w, "", balances, nil, config.Query.USDOnly, config.Query.Normalize, time.Now().Format("2006-01-02"), false, maxAccW)
case "reg":
transactions := deltas.Transactions()
cumulative := make(ledger.Balances)
for _, transaction := range transactions {
balances := ledger.Deltas(transaction).Like(q).Group(group).Balances().WithBPIsAt(bpis, transaction[0].Date).Nonzero()
shouldPrint := false
shouldPrint = shouldPrint || len(balances) > 2
if config.Query.NoExchanging {
shouldPrint = shouldPrint || len(balances) > 1
for _, v := range balances {
shouldPrint = shouldPrint || len(v) == 1
}
} else {
shouldPrint = shouldPrint || len(balances) > 0
}
if shouldPrint {
cumulative.PushAll(balances)
cumulative = cumulative.Nonzero()
FPrintBalancesFor(transaction[0].Description, w, "\t\t", balances, cumulative, config.Query.USDOnly, config.Query.Normalize, transaction[0].Date, config.Compact, maxAccW)
}
}
default:
panic("unknown command " + positional[0])
}
}
func FPrintBalancesFor(description string, w io.Writer, linePrefix string, balances, cumulatives ledger.Balances, usdOnly, normalized bool, date string, compact bool, keyW int) {
if compact {
FPrintBalances(w, date+"\t", balances, cumulatives, usdOnly, normalized, date, compact, keyW)
} else {
fmt.Fprintf(w, "%s\t%s\n", date, description)
FPrintBalances(w, linePrefix, balances, cumulatives, usdOnly, normalized, date, compact, keyW)
}
}
func FPrintBalances(w io.Writer, linePrefix string, balances, cumulatives ledger.Balances, usdOnly, normalized bool, date string, fullKey bool, max int) {
maxes := map[ledger.Currency]float64{}
keys := []string{}
for k, v := range balances {
keys = append(keys, k)
for k2, v2 := range v {
if math.Abs(v2) > math.Abs(maxes[k2]) {
maxes[k2] = math.Abs(v2)
}
}
}
slices.Sort(keys)
normalizer := ana.NewDefaultNormalizer()
format := fmt.Sprintf("%s%%-%ds\t%%s%%.2f (%%s%%.2f)\n", linePrefix, max)
if normalized {
format = fmt.Sprintf("%s%%-%ds\t%%s%%.2f (%%s%%.2f (%%.2f @%%.2f (%%s%%.0f)))\n", linePrefix, max)
}
for i, key := range keys {
printableKey := key
if fullKey {
} else if i == 0 {
} else {
commonPrefixLen := func() int {
j := 0
n := len(keys[i])
if n2 := len(keys[i-1]); n2 < n {
n = n2
}
for j = 0; j < n; j++ {
if keys[i-1][j] != keys[i][j] {
break
}
}
for keys[i][j] != ':' && j > 0 {
j -= 1
}
return j
}()
printableKey = strings.Repeat(" ", commonPrefixLen) + keys[i][commonPrefixLen:]
}
currencies := []ledger.Currency{}
for currency := range balances[key] {
currencies = append(currencies, currency)
}
slices.Sort(currencies)
for _, currency := range currencies {
printableCurrency := currency
format := format
if printableCurrency != "$" {
printableCurrency += " "
format = strings.ReplaceAll(format, "%.2f", "%.3f")
}
if usdOnly && printableCurrency != "$" {
continue
}
cumulative := balances[key][currency]
if balance, ok := cumulatives[key]; !ok {
} else if value, ok := balance[currency]; !ok {
} else {
cumulative = value
}
if !normalized {
fmt.Fprintf(w, format, printableKey, printableCurrency, balances[key][currency], printableCurrency, cumulative)
} else {
factor := normalizer.NormalizeFactor(ledger.Delta{Name: key, Date: date})
trailingMax := maxes[currency] - math.Abs(balances[key][currency])
fmt.Fprintf(w, format, printableKey, printableCurrency, balances[key][currency], printableCurrency, cumulative, cumulative*factor, factor, printableCurrency, factor*trailingMax)
}
}
}
}