diff --git a/cmd/bpi.dat b/cmd/bpi.dat
deleted file mode 120000
index 0763cc5..0000000
--- a/cmd/bpi.dat
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../Sync/Core/ledger/bpi.dat
\ No newline at end of file
diff --git a/cmd/http/bpi.dat b/cmd/http/bpi.dat
new file mode 120000
index 0000000..f5da145
--- /dev/null
+++ b/cmd/http/bpi.dat
@@ -0,0 +1 @@
+../../../../../../Sync/Core/ledger/bpi.dat
\ No newline at end of file
diff --git a/cmd/draw.sh b/cmd/http/draw.sh
similarity index 72%
rename from cmd/draw.sh
rename to cmd/http/draw.sh
index 6e1c2e6..d165773 100644
--- a/cmd/draw.sh
+++ b/cmd/http/draw.sh
@@ -1,12 +1,13 @@
#! /bin/bash
cd "$(dirname "$(realpath "$BASH_SOURCE")")"
-go run . -http=:8081 \
+cd ..
+go run . http -http=:8081 \
-foo reg \
-like-after 1023-08 \
-group-date ^....-.. \
-group-name '^[^:]*:[^:]*' \
-like-name '(AssetAccount|Retirement)' \
- -bpi ./bpi.dat \
+ -bpi ./http/bpi.dat \
"$@" \
- macro.d/*
+ ./http/macro.d/*
diff --git a/cmd/http/macro.d b/cmd/http/macro.d
new file mode 120000
index 0000000..989c51c
--- /dev/null
+++ b/cmd/http/macro.d
@@ -0,0 +1 @@
+../../../../../../Sync/Core/ledger/eras/2022-/
\ No newline at end of file
diff --git a/cmd/http/main.go b/cmd/http/main.go
new file mode 100644
index 0000000..2bb3c77
--- /dev/null
+++ b/cmd/http/main.go
@@ -0,0 +1,475 @@
+package http
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "maps"
+ "net/http"
+ "os"
+ "path"
+ "slices"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/go-echarts/go-echarts/v2/charts"
+ "github.com/go-echarts/go-echarts/v2/opts"
+ "gogs.inhome.blapointe.com/ana-ledger/src/ana"
+ "gogs.inhome.blapointe.com/ana-ledger/src/ledger"
+)
+
+func Main() {
+ foo := flag.String("foo", "bal", "bal or reg")
+ likeName := flag.String("like-name", ".", "regexp to match")
+ likeBefore := flag.String("like-before", "9", "date str to compare")
+ likeAfter := flag.String("like-after", "0", "date str to compare")
+ likeLedger := flag.Bool("like-ledger", false, "limit data to these -like-* rather than zoom to these -like-*")
+ groupName := flag.String("group-name", ".*", "grouping to apply to names")
+ groupDate := flag.String("group-date", ".*", "grouping to apply to dates")
+ bpiPath := flag.String("bpi", "/dev/null", "bpi file")
+ jsonOutput := flag.Bool("json", false, "json output")
+ httpOutput := flag.String("http", "", "http output listen address, like :8080")
+ flag.Parse()
+
+ if flag.NArg() < 1 {
+ panic(fmt.Errorf("positional arguments for files required"))
+ }
+
+ f, err := ledger.NewFiles(flag.Args()[0], flag.Args()[1:]...)
+ if err != nil {
+ panic(err)
+ }
+
+ bpis := make(ledger.BPIs)
+ if *bpiPath != "" {
+ bpis, err = ledger.NewBPIs(*bpiPath)
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ if *httpOutput != "" {
+ foo := func(w http.ResponseWriter, r *http.Request) {
+ if !strings.HasPrefix(r.URL.Path, "/api") {
+ http.FileServer(http.Dir("./http/public")).ServeHTTP(w, r)
+ return
+ }
+
+ switch r.URL.Path {
+ case "/api/transactions":
+ reqF := f
+ if queryF := r.URL.Query().Get("f"); queryF != "" {
+ queryF = path.Join("http", queryF)
+ reqF, err = ledger.NewFiles(queryF)
+ if err != nil {
+ panic(err)
+ }
+ }
+ deltas, err := reqF.Deltas()
+ if err != nil {
+ panic(err)
+ }
+ json.NewEncoder(w).Encode(map[string]any{
+ "deltas": deltas.Like(ledger.LikeAfter(time.Now().Add(-1 * time.Hour * 24 * 365 / 2).Format("2006-01"))),
+ "balances": deltas.Balances().Like("^AssetAccount:").WithBPIs(bpis),
+ })
+ return
+ }
+
+ deltas, err := f.Deltas()
+ if err != nil {
+ panic(err)
+ }
+ deltas = deltas.Group(ledger.GroupName(*groupName), ledger.GroupDate(*groupDate))
+ like := ledger.Likes{
+ ledger.LikeName(*likeName),
+ ledger.LikeBefore(*likeBefore),
+ ledger.LikeAfter(*likeAfter),
+ }
+
+ foolike := make(ledger.Likes, 0)
+ for _, v := range r.URL.Query()["likeName"] {
+ foolike = append(foolike, ledger.LikeName(v))
+ }
+ for _, v := range r.URL.Query()["likeAfter"] {
+ foolike = append(foolike, ledger.LikeAfter(v))
+ }
+ for _, v := range r.URL.Query()["likeBefore"] {
+ foolike = append(foolike, ledger.LikeBefore(v))
+ }
+ if len(foolike) == 0 {
+ foolike = like
+ }
+ deltas = deltas.Like(foolike...)
+
+ // MODIFIERS
+ for i, whatIf := range r.URL.Query()["whatIf"] {
+ fields := strings.Fields(whatIf)
+ date := "2001-01"
+ name := fields[0]
+ currency := ledger.Currency(fields[1])
+ value, err := strconv.ParseFloat(fields[2], 64)
+ if err != nil {
+ panic(err)
+ }
+ deltas = append(deltas, ledger.Delta{
+ Date: date,
+ Name: name,
+ Value: value,
+ Currency: currency,
+ Description: fmt.Sprintf("?whatIf[%d]", i),
+ })
+ }
+
+ register := deltas.Register()
+ predicted := make(ledger.Register)
+ bpis := maps.Clone(bpis)
+
+ if predictionMonths, err := strconv.ParseInt(r.URL.Query().Get("predictionMonths"), 10, 16); err == nil && predictionMonths > 0 {
+ window := time.Hour * 24.0 * 365.0 / 12.0 * time.Duration(predictionMonths)
+ // TODO whatif
+ prediction := make(ana.Prediction, 0)
+ for _, spec := range r.URL.Query()["prediction"] {
+ idx := strings.Index(spec, "=")
+ k := spec[:idx]
+ fields := strings.Fields(spec[idx+1:])
+ switch k {
+ case "interest":
+ apy, err := strconv.ParseFloat(fields[2], 64)
+ if err != nil {
+ panic(err)
+ }
+ prediction = append(prediction, ana.NewInterestPredictor(fields[0], fields[1], apy))
+ case "autoContributions":
+ prediction = append(prediction, ana.NewAutoContributionPredictor(register))
+ case "contributions":
+ name := fields[0]
+ currency := ledger.Currency(fields[1])
+ value, err := strconv.ParseFloat(fields[2], 64)
+ if err != nil {
+ panic(err)
+ }
+ prediction = append(prediction, ana.NewContributionPredictor(ledger.Balances{name: ledger.Balance{currency: value}}))
+ default:
+ panic(k)
+ }
+ }
+ predicted = prediction.Predict(register, window)
+
+ for _, currencyRate := range r.URL.Query()["predictFixedGrowth"] {
+ currency := strings.Split(currencyRate, "=")[0]
+ rate, err := strconv.ParseFloat(strings.Split(currencyRate, "=")[1], 64)
+ if err != nil {
+ panic(err)
+ }
+ bpis, err = ana.BPIsWithFixedGrowthPrediction(bpis, window, currency, rate)
+ if err != nil {
+ panic(err)
+ }
+ }
+ }
+
+ if r.URL.Query().Get("bpi") == "true" {
+ register = register.WithBPIs(bpis)
+ predicted = predicted.WithBPIs(bpis)
+ }
+ if zoomStart, err := time.ParseInLocation("2006-01", r.URL.Query().Get("zoomStart"), time.Local); err == nil {
+ register = register.Between(zoomStart, time.Now().Add(time.Hour*24*365*100))
+ predicted = predicted.Between(zoomStart, time.Now().Add(time.Hour*24*365*100))
+ }
+ // /MODIFIERS
+
+ dates := register.Dates()
+ names := register.Names()
+ for _, date := range predicted.Dates() {
+ found := false
+ for i := range dates {
+ found = found || dates[i] == date
+ }
+ if !found {
+ dates = append(dates, date)
+ }
+ }
+ for _, name := range predicted.Names() {
+ found := false
+ for i := range names {
+ found = found || names[i] == name
+ }
+ if !found {
+ names = append(names, name)
+ }
+ }
+ instant := map[string]string{}
+ toChart := func(cumulative bool, display string, reg ledger.Register) Chart {
+ nameCurrencyDateValue := map[string]map[ledger.Currency]map[string]float64{}
+ for date, balances := range reg {
+ for name, balance := range balances {
+ for currency, value := range balance {
+ if _, ok := nameCurrencyDateValue[name]; !ok {
+ nameCurrencyDateValue[name] = make(map[ledger.Currency]map[string]float64)
+ }
+ if _, ok := nameCurrencyDateValue[name][currency]; !ok {
+ nameCurrencyDateValue[name][currency] = make(map[string]float64)
+ }
+ nameCurrencyDateValue[name][currency][date] += value
+ }
+ }
+ }
+
+ chart := NewChart("line")
+ if v := display; v != "" {
+ chart = NewChart(v)
+ }
+
+ chart.AddX(dates)
+
+ if cumulative {
+ for _, name := range names {
+ currencyDateValue := nameCurrencyDateValue[name]
+ for currency, dateValue := range currencyDateValue {
+ series := make([]int, len(dates))
+ for i := range dates {
+ var lastValue float64
+ for j := range dates[:i+1] {
+ if newLastValue, ok := dateValue[dates[j]]; ok {
+ lastValue = newLastValue
+ }
+ }
+ series[i] = int(lastValue)
+ }
+ key := fmt.Sprintf("%s (%s)", name, currency)
+ for i := range dates {
+ if !(reg.Dates()[0] <= dates[i] && dates[i] <= reg.Dates()[len(reg.Dates())-1]) {
+ series[i] = 0
+ } else {
+ instant[key] = fmt.Sprintf("@%s %v", dates[i], series[i])
+ }
+ }
+ if slices.Min(series) != 0 || slices.Max(series) != 0 {
+ chart.AddY(key, series)
+ }
+ }
+ }
+ } else {
+ for _, name := range names {
+ currencyDateValue := nameCurrencyDateValue[name]
+ for currency, dateValue := range currencyDateValue {
+ series := make([]int, len(dates))
+ for i := range dates {
+ var prevValue float64
+ var lastValue float64
+ for j := range dates[:i+1] {
+ if newLastValue, ok := dateValue[dates[j]]; ok {
+ prevValue = lastValue
+ lastValue = newLastValue
+ }
+ }
+ series[i] = int(lastValue - prevValue)
+ }
+ for i := range series { // TODO no prior so no delta
+ if series[i] != 0 {
+ series[i] = 0
+ break
+ }
+ }
+ key := fmt.Sprintf("%s (%s)", name, currency)
+ for i := range dates {
+ if !(reg.Dates()[0] <= dates[i] && dates[i] <= reg.Dates()[len(reg.Dates())-1]) {
+ series[i] = 0
+ } else {
+ instant[key] = fmt.Sprintf("@%s %v", dates[i], series[i])
+ }
+ }
+ if slices.Min(series) != 0 || slices.Max(series) != 0 {
+ chart.AddY(key, series)
+ }
+ }
+ }
+ }
+ return chart
+ }
+ primary := toChart(r.URL.Path == "/api/bal", r.URL.Query().Get("chart"), register)
+ if len(predicted) > 0 {
+ primary.Overlap(toChart(r.URL.Path == "/api/bal", "line", predicted))
+ }
+ if err := primary.Render(w); err != nil {
+ panic(err)
+ }
+ for k, v := range instant {
+ fmt.Fprintf(w, "
\n%s = %s", k, v)
+ }
+ }
+
+ log.Println("listening on", *httpOutput)
+ if err := http.ListenAndServe(*httpOutput, http.HandlerFunc(foo)); err != nil {
+ panic(err)
+ }
+ } else {
+ deltas, err := f.Deltas()
+ if err != nil {
+ panic(err)
+ }
+ deltas = deltas.Group(ledger.GroupName(*groupName), ledger.GroupDate(*groupDate))
+ like := ledger.Likes{ledger.LikeName(*likeName)}
+ if *likeLedger {
+ like = append(like, ledger.LikeBefore(*likeBefore))
+ like = append(like, ledger.LikeAfter(*likeAfter))
+ deltas = deltas.Like(like...)
+ } else {
+ deltas = deltas.Like(like...)
+ like = append(like, ledger.LikeBefore(*likeBefore))
+ like = append(like, ledger.LikeAfter(*likeAfter))
+ }
+
+ jsonResult := []any{}
+
+ switch *foo {
+ case "reg":
+ sort.Slice(deltas, func(i, j int) bool {
+ return deltas[i].Debug() < deltas[j].Debug()
+ })
+ register := deltas.Register()
+ for i := range deltas {
+ if like.All(deltas[i]) {
+ if !*jsonOutput {
+ fmt.Printf("%s (%+v)\n", deltas[i].Debug(), register[deltas[i].Date][deltas[i].Name].Debug())
+ } else {
+ jsonResult = append(jsonResult, map[string]any{
+ "name": deltas[i].Name,
+ "delta": deltas[i],
+ "balance": register[deltas[i].Date][deltas[i].Name],
+ })
+ }
+ }
+ }
+ case "bal":
+ deltas = deltas.Like(like...)
+ for k, v := range deltas.Balances() {
+ results := []string{}
+ for subk, subv := range v {
+ results = append(results, fmt.Sprintf("%s %.2f", subk, subv))
+ }
+ if len(results) > 0 {
+ if !*jsonOutput {
+ fmt.Printf("%s\t%s\n", k, strings.Join(results, " + "))
+ } else {
+ jsonResult = append(jsonResult, map[string]any{
+ "name": k,
+ "balance": v,
+ })
+ }
+ }
+ }
+ default:
+ panic(fmt.Errorf("not impl %q", *foo))
+ }
+
+ if *jsonOutput {
+ json.NewEncoder(os.Stdout).Encode(jsonResult)
+ }
+ }
+}
+
+type Chart interface {
+ AddX(interface{})
+ AddY(string, []int)
+ Render(io.Writer) error
+ Overlap(Chart)
+}
+
+func NewChart(name string) Chart {
+ switch name {
+ case "line":
+ return NewLine()
+ case "bar":
+ return NewBar()
+ case "stack":
+ return NewStack()
+ default:
+ panic("bad chart name " + name)
+ }
+}
+
+type Line struct {
+ *charts.Line
+}
+
+func NewLine() Line {
+ return Line{Line: charts.NewLine()}
+}
+
+func (line Line) AddX(v interface{}) {
+ line.SetXAxis(v)
+}
+
+func (line Line) AddY(name string, v []int) {
+ y := make([]opts.LineData, len(v))
+ for i := range y {
+ y[i].Value = v[i]
+ }
+ line.AddSeries(name, y).
+ SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{
+ Stack: "stackB",
+ }))
+}
+
+func (line Line) Overlap(other Chart) {
+ overlapper, ok := other.(charts.Overlaper)
+ if !ok {
+ panic(fmt.Sprintf("cannot overlap %T", other))
+ }
+ line.Line.Overlap(overlapper)
+}
+
+type Bar struct {
+ *charts.Bar
+}
+
+func NewBar() Bar {
+ return Bar{Bar: charts.NewBar()}
+}
+
+func (bar Bar) AddX(v interface{}) {
+ bar.SetXAxis(v)
+}
+
+func (bar Bar) AddY(name string, v []int) {
+ y := make([]opts.BarData, len(v))
+ for i := range v {
+ y[i].Value = v[i]
+ }
+ bar.AddSeries(name, y)
+}
+
+func (bar Bar) Overlap(other Chart) {
+ overlapper, ok := other.(charts.Overlaper)
+ if !ok {
+ panic(fmt.Sprintf("cannot overlap %T", other))
+ }
+ bar.Bar.Overlap(overlapper)
+}
+
+type Stack struct {
+ Bar
+}
+
+func NewStack() Stack {
+ bar := NewBar()
+ bar.SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{Stack: "x"}))
+ return Stack{Bar: bar}
+}
+
+func (stack Stack) AddY(name string, v []int) {
+ y := make([]opts.BarData, len(v))
+ for i := range v {
+ y[i].Value = v[i]
+ }
+ stack.AddSeries(name, y).
+ SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{
+ Stack: "stackA",
+ }))
+}
diff --git a/cmd/http/moolah.dat b/cmd/http/moolah.dat
new file mode 120000
index 0000000..f1b571b
--- /dev/null
+++ b/cmd/http/moolah.dat
@@ -0,0 +1 @@
+../../../../../../Sync/Core/tmp/moolah.dat
\ No newline at end of file
diff --git a/cmd/public/explore.html b/cmd/http/public/explore.html
similarity index 100%
rename from cmd/public/explore.html
rename to cmd/http/public/explore.html
diff --git a/cmd/public/index.html b/cmd/http/public/index.html
similarity index 100%
rename from cmd/public/index.html
rename to cmd/http/public/index.html
diff --git a/cmd/public/transactions.html b/cmd/http/public/transactions.html
similarity index 100%
rename from cmd/public/transactions.html
rename to cmd/http/public/transactions.html
diff --git a/cmd/macro.d b/cmd/macro.d
deleted file mode 120000
index 9783d22..0000000
--- a/cmd/macro.d
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../Sync/Core/ledger/eras/2022-
\ No newline at end of file
diff --git a/cmd/main.go b/cmd/main.go
index 8ab33f2..091ad26 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -1,473 +1,15 @@
package main
import (
- "encoding/json"
- "flag"
- "fmt"
- "io"
- "log"
- "maps"
- "net/http"
"os"
- "slices"
- "sort"
- "strconv"
- "strings"
- "time"
- "github.com/go-echarts/go-echarts/v2/charts"
- "github.com/go-echarts/go-echarts/v2/opts"
- "gogs.inhome.blapointe.com/ana-ledger/src/ana"
- "gogs.inhome.blapointe.com/ana-ledger/src/ledger"
+ "gogs.inhome.blapointe.com/ana-ledger/cmd/http"
)
func main() {
- foo := flag.String("foo", "bal", "bal or reg")
- likeName := flag.String("like-name", ".", "regexp to match")
- likeBefore := flag.String("like-before", "9", "date str to compare")
- likeAfter := flag.String("like-after", "0", "date str to compare")
- likeLedger := flag.Bool("like-ledger", false, "limit data to these -like-* rather than zoom to these -like-*")
- groupName := flag.String("group-name", ".*", "grouping to apply to names")
- groupDate := flag.String("group-date", ".*", "grouping to apply to dates")
- bpiPath := flag.String("bpi", "/dev/null", "bpi file")
- jsonOutput := flag.Bool("json", false, "json output")
- httpOutput := flag.String("http", "", "http output listen address, like :8080")
- flag.Parse()
-
- if flag.NArg() < 1 {
- panic(fmt.Errorf("positional arguments for files required"))
- }
-
- f, err := ledger.NewFiles(flag.Args()[0], flag.Args()[1:]...)
- if err != nil {
- panic(err)
- }
-
- bpis := make(ledger.BPIs)
- if *bpiPath != "" {
- bpis, err = ledger.NewBPIs(*bpiPath)
- if err != nil {
- panic(err)
- }
- }
-
- if *httpOutput != "" {
- foo := func(w http.ResponseWriter, r *http.Request) {
- if !strings.HasPrefix(r.URL.Path, "/api") {
- http.FileServer(http.Dir("./public")).ServeHTTP(w, r)
- return
- }
-
- switch r.URL.Path {
- case "/api/transactions":
- reqF := f
- if queryF := r.URL.Query().Get("f"); queryF != "" {
- reqF, err = ledger.NewFiles(queryF)
- if err != nil {
- panic(err)
- }
- }
- deltas, err := reqF.Deltas()
- if err != nil {
- panic(err)
- }
- json.NewEncoder(w).Encode(map[string]any{
- "deltas": deltas.Like(ledger.LikeAfter(time.Now().Add(-1 * time.Hour * 24 * 365 / 2).Format("2006-01"))),
- "balances": deltas.Balances().Like("^AssetAccount:").WithBPIs(bpis),
- })
- return
- }
-
- deltas, err := f.Deltas()
- if err != nil {
- panic(err)
- }
- deltas = deltas.Group(ledger.GroupName(*groupName), ledger.GroupDate(*groupDate))
- like := ledger.Likes{
- ledger.LikeName(*likeName),
- ledger.LikeBefore(*likeBefore),
- ledger.LikeAfter(*likeAfter),
- }
-
- foolike := make(ledger.Likes, 0)
- for _, v := range r.URL.Query()["likeName"] {
- foolike = append(foolike, ledger.LikeName(v))
- }
- for _, v := range r.URL.Query()["likeAfter"] {
- foolike = append(foolike, ledger.LikeAfter(v))
- }
- for _, v := range r.URL.Query()["likeBefore"] {
- foolike = append(foolike, ledger.LikeBefore(v))
- }
- if len(foolike) == 0 {
- foolike = like
- }
- deltas = deltas.Like(foolike...)
-
- // MODIFIERS
- for i, whatIf := range r.URL.Query()["whatIf"] {
- fields := strings.Fields(whatIf)
- date := "2001-01"
- name := fields[0]
- currency := ledger.Currency(fields[1])
- value, err := strconv.ParseFloat(fields[2], 64)
- if err != nil {
- panic(err)
- }
- deltas = append(deltas, ledger.Delta{
- Date: date,
- Name: name,
- Value: value,
- Currency: currency,
- Description: fmt.Sprintf("?whatIf[%d]", i),
- })
- }
-
- register := deltas.Register()
- predicted := make(ledger.Register)
- bpis := maps.Clone(bpis)
-
- if predictionMonths, err := strconv.ParseInt(r.URL.Query().Get("predictionMonths"), 10, 16); err == nil && predictionMonths > 0 {
- window := time.Hour * 24.0 * 365.0 / 12.0 * time.Duration(predictionMonths)
- // TODO whatif
- prediction := make(ana.Prediction, 0)
- for _, spec := range r.URL.Query()["prediction"] {
- idx := strings.Index(spec, "=")
- k := spec[:idx]
- fields := strings.Fields(spec[idx+1:])
- switch k {
- case "interest":
- apy, err := strconv.ParseFloat(fields[2], 64)
- if err != nil {
- panic(err)
- }
- prediction = append(prediction, ana.NewInterestPredictor(fields[0], fields[1], apy))
- case "autoContributions":
- prediction = append(prediction, ana.NewAutoContributionPredictor(register))
- case "contributions":
- name := fields[0]
- currency := ledger.Currency(fields[1])
- value, err := strconv.ParseFloat(fields[2], 64)
- if err != nil {
- panic(err)
- }
- prediction = append(prediction, ana.NewContributionPredictor(ledger.Balances{name: ledger.Balance{currency: value}}))
- default:
- panic(k)
- }
- }
- predicted = prediction.Predict(register, window)
-
- for _, currencyRate := range r.URL.Query()["predictFixedGrowth"] {
- currency := strings.Split(currencyRate, "=")[0]
- rate, err := strconv.ParseFloat(strings.Split(currencyRate, "=")[1], 64)
- if err != nil {
- panic(err)
- }
- bpis, err = ana.BPIsWithFixedGrowthPrediction(bpis, window, currency, rate)
- if err != nil {
- panic(err)
- }
- }
- }
-
- if r.URL.Query().Get("bpi") == "true" {
- register = register.WithBPIs(bpis)
- predicted = predicted.WithBPIs(bpis)
- }
- if zoomStart, err := time.ParseInLocation("2006-01", r.URL.Query().Get("zoomStart"), time.Local); err == nil {
- register = register.Between(zoomStart, time.Now().Add(time.Hour*24*365*100))
- predicted = predicted.Between(zoomStart, time.Now().Add(time.Hour*24*365*100))
- }
- // /MODIFIERS
-
- dates := register.Dates()
- names := register.Names()
- for _, date := range predicted.Dates() {
- found := false
- for i := range dates {
- found = found || dates[i] == date
- }
- if !found {
- dates = append(dates, date)
- }
- }
- for _, name := range predicted.Names() {
- found := false
- for i := range names {
- found = found || names[i] == name
- }
- if !found {
- names = append(names, name)
- }
- }
- instant := map[string]string{}
- toChart := func(cumulative bool, display string, reg ledger.Register) Chart {
- nameCurrencyDateValue := map[string]map[ledger.Currency]map[string]float64{}
- for date, balances := range reg {
- for name, balance := range balances {
- for currency, value := range balance {
- if _, ok := nameCurrencyDateValue[name]; !ok {
- nameCurrencyDateValue[name] = make(map[ledger.Currency]map[string]float64)
- }
- if _, ok := nameCurrencyDateValue[name][currency]; !ok {
- nameCurrencyDateValue[name][currency] = make(map[string]float64)
- }
- nameCurrencyDateValue[name][currency][date] += value
- }
- }
- }
-
- chart := NewChart("line")
- if v := display; v != "" {
- chart = NewChart(v)
- }
-
- chart.AddX(dates)
-
- if cumulative {
- for _, name := range names {
- currencyDateValue := nameCurrencyDateValue[name]
- for currency, dateValue := range currencyDateValue {
- series := make([]int, len(dates))
- for i := range dates {
- var lastValue float64
- for j := range dates[:i+1] {
- if newLastValue, ok := dateValue[dates[j]]; ok {
- lastValue = newLastValue
- }
- }
- series[i] = int(lastValue)
- }
- key := fmt.Sprintf("%s (%s)", name, currency)
- for i := range dates {
- if !(reg.Dates()[0] <= dates[i] && dates[i] <= reg.Dates()[len(reg.Dates())-1]) {
- series[i] = 0
- } else {
- instant[key] = fmt.Sprintf("@%s %v", dates[i], series[i])
- }
- }
- if slices.Min(series) != 0 || slices.Max(series) != 0 {
- chart.AddY(key, series)
- }
- }
- }
- } else {
- for _, name := range names {
- currencyDateValue := nameCurrencyDateValue[name]
- for currency, dateValue := range currencyDateValue {
- series := make([]int, len(dates))
- for i := range dates {
- var prevValue float64
- var lastValue float64
- for j := range dates[:i+1] {
- if newLastValue, ok := dateValue[dates[j]]; ok {
- prevValue = lastValue
- lastValue = newLastValue
- }
- }
- series[i] = int(lastValue - prevValue)
- }
- for i := range series { // TODO no prior so no delta
- if series[i] != 0 {
- series[i] = 0
- break
- }
- }
- key := fmt.Sprintf("%s (%s)", name, currency)
- for i := range dates {
- if !(reg.Dates()[0] <= dates[i] && dates[i] <= reg.Dates()[len(reg.Dates())-1]) {
- series[i] = 0
- } else {
- instant[key] = fmt.Sprintf("@%s %v", dates[i], series[i])
- }
- }
- if slices.Min(series) != 0 || slices.Max(series) != 0 {
- chart.AddY(key, series)
- }
- }
- }
- }
- return chart
- }
- primary := toChart(r.URL.Path == "/api/bal", r.URL.Query().Get("chart"), register)
- if len(predicted) > 0 {
- primary.Overlap(toChart(r.URL.Path == "/api/bal", "line", predicted))
- }
- if err := primary.Render(w); err != nil {
- panic(err)
- }
- for k, v := range instant {
- fmt.Fprintf(w, "
\n%s = %s", k, v)
- }
- }
-
- log.Println("listening on", *httpOutput)
- if err := http.ListenAndServe(*httpOutput, http.HandlerFunc(foo)); err != nil {
- panic(err)
- }
- } else {
- deltas, err := f.Deltas()
- if err != nil {
- panic(err)
- }
- deltas = deltas.Group(ledger.GroupName(*groupName), ledger.GroupDate(*groupDate))
- like := ledger.Likes{ledger.LikeName(*likeName)}
- if *likeLedger {
- like = append(like, ledger.LikeBefore(*likeBefore))
- like = append(like, ledger.LikeAfter(*likeAfter))
- deltas = deltas.Like(like...)
- } else {
- deltas = deltas.Like(like...)
- like = append(like, ledger.LikeBefore(*likeBefore))
- like = append(like, ledger.LikeAfter(*likeAfter))
- }
-
- jsonResult := []any{}
-
- switch *foo {
- case "reg":
- sort.Slice(deltas, func(i, j int) bool {
- return deltas[i].Debug() < deltas[j].Debug()
- })
- register := deltas.Register()
- for i := range deltas {
- if like.All(deltas[i]) {
- if !*jsonOutput {
- fmt.Printf("%s (%+v)\n", deltas[i].Debug(), register[deltas[i].Date][deltas[i].Name].Debug())
- } else {
- jsonResult = append(jsonResult, map[string]any{
- "name": deltas[i].Name,
- "delta": deltas[i],
- "balance": register[deltas[i].Date][deltas[i].Name],
- })
- }
- }
- }
- case "bal":
- deltas = deltas.Like(like...)
- for k, v := range deltas.Balances() {
- results := []string{}
- for subk, subv := range v {
- results = append(results, fmt.Sprintf("%s %.2f", subk, subv))
- }
- if len(results) > 0 {
- if !*jsonOutput {
- fmt.Printf("%s\t%s\n", k, strings.Join(results, " + "))
- } else {
- jsonResult = append(jsonResult, map[string]any{
- "name": k,
- "balance": v,
- })
- }
- }
- }
- default:
- panic(fmt.Errorf("not impl %q", *foo))
- }
-
- if *jsonOutput {
- json.NewEncoder(os.Stdout).Encode(jsonResult)
- }
+ switch os.Args[1] {
+ case "http":
+ os.Args = append([]string{os.Args[0]}, os.Args[2:]...)
+ http.Main()
}
}
-
-type Chart interface {
- AddX(interface{})
- AddY(string, []int)
- Render(io.Writer) error
- Overlap(Chart)
-}
-
-func NewChart(name string) Chart {
- switch name {
- case "line":
- return NewLine()
- case "bar":
- return NewBar()
- case "stack":
- return NewStack()
- default:
- panic("bad chart name " + name)
- }
-}
-
-type Line struct {
- *charts.Line
-}
-
-func NewLine() Line {
- return Line{Line: charts.NewLine()}
-}
-
-func (line Line) AddX(v interface{}) {
- line.SetXAxis(v)
-}
-
-func (line Line) AddY(name string, v []int) {
- y := make([]opts.LineData, len(v))
- for i := range y {
- y[i].Value = v[i]
- }
- line.AddSeries(name, y).
- SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{
- Stack: "stackB",
- }))
-}
-
-func (line Line) Overlap(other Chart) {
- overlapper, ok := other.(charts.Overlaper)
- if !ok {
- panic(fmt.Sprintf("cannot overlap %T", other))
- }
- line.Line.Overlap(overlapper)
-}
-
-type Bar struct {
- *charts.Bar
-}
-
-func NewBar() Bar {
- return Bar{Bar: charts.NewBar()}
-}
-
-func (bar Bar) AddX(v interface{}) {
- bar.SetXAxis(v)
-}
-
-func (bar Bar) AddY(name string, v []int) {
- y := make([]opts.BarData, len(v))
- for i := range v {
- y[i].Value = v[i]
- }
- bar.AddSeries(name, y)
-}
-
-func (bar Bar) Overlap(other Chart) {
- overlapper, ok := other.(charts.Overlaper)
- if !ok {
- panic(fmt.Sprintf("cannot overlap %T", other))
- }
- bar.Bar.Overlap(overlapper)
-}
-
-type Stack struct {
- Bar
-}
-
-func NewStack() Stack {
- bar := NewBar()
- bar.SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{Stack: "x"}))
- return Stack{Bar: bar}
-}
-
-func (stack Stack) AddY(name string, v []int) {
- y := make([]opts.BarData, len(v))
- for i := range v {
- y[i].Value = v[i]
- }
- stack.AddSeries(name, y).
- SetSeriesOptions(charts.WithBarChartOpts(opts.BarChart{
- Stack: "stackA",
- }))
-}
diff --git a/cmd/moolah.dat b/cmd/moolah.dat
deleted file mode 120000
index 2e7a52c..0000000
--- a/cmd/moolah.dat
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../Sync/Core/tmp/moolah.dat
\ No newline at end of file