From 487aa89f180f69cdf8aa1976c92581ba5e57968d Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Sun, 6 Feb 2022 22:12:48 +0000 Subject: [PATCH] Use Numeric in JSON output --- http/autocomplete.go | 54 +++++++++++++++ http/budgeting.go | 113 ++++++++++---------------------- http/http.go | 1 + postgres/numeric.go | 91 ++++++++++++++++++++++++- web/src/components/Currency.vue | 12 ++++ web/src/pages/Account.vue | 8 +-- web/src/pages/BudgetSidebar.vue | 10 +-- web/src/pages/Budgeting.vue | 43 ++++++++---- web/src/store/action-types.ts | 1 + web/src/store/budget/index.ts | 33 +++++++++- web/src/store/index.ts | 10 --- 11 files changed, 258 insertions(+), 118 deletions(-) create mode 100644 http/autocomplete.go create mode 100644 web/src/components/Currency.vue diff --git a/http/autocomplete.go b/http/autocomplete.go new file mode 100644 index 0000000..2a510c1 --- /dev/null +++ b/http/autocomplete.go @@ -0,0 +1,54 @@ +package http + +import ( + "fmt" + "net/http" + + "git.javil.eu/jacob1123/budgeteer/postgres" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +func (h *Handler) autocompleteCategories(c *gin.Context) { + budgetID := c.Param("budgetid") + budgetUUID, err := uuid.Parse(budgetID) + if err != nil { + c.AbortWithError(http.StatusBadRequest, fmt.Errorf("budgetid missing from URL")) + return + } + + query := c.Request.URL.Query().Get("s") + searchParams := postgres.SearchCategoriesParams{ + BudgetID: budgetUUID, + Search: "%" + query + "%", + } + categories, err := h.Service.SearchCategories(c.Request.Context(), searchParams) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + c.JSON(http.StatusOK, categories) +} + +func (h *Handler) autocompletePayee(c *gin.Context) { + budgetID := c.Param("budgetid") + budgetUUID, err := uuid.Parse(budgetID) + if err != nil { + c.AbortWithError(http.StatusBadRequest, fmt.Errorf("budgetid missing from URL")) + return + } + + query := c.Request.URL.Query().Get("s") + searchParams := postgres.SearchPayeesParams{ + BudgetID: budgetUUID, + Search: query + "%", + } + payees, err := h.Service.SearchPayees(c.Request.Context(), searchParams) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + c.JSON(http.StatusOK, payees) +} diff --git a/http/budgeting.go b/http/budgeting.go index f666fec..c0bed4f 100644 --- a/http/budgeting.go +++ b/http/budgeting.go @@ -24,10 +24,10 @@ func getFirstOfMonthTime(date time.Time) time.Time { type CategoryWithBalance struct { *postgres.GetCategoriesRow - Available float64 - AvailableLastMonth float64 - Activity float64 - Assigned float64 + Available postgres.Numeric + AvailableLastMonth postgres.Numeric + Activity postgres.Numeric + Assigned postgres.Numeric } func getDate(c *gin.Context) (time.Time, error) { @@ -51,50 +51,6 @@ func getDate(c *gin.Context) (time.Time, error) { return getFirstOfMonth(year, month, time.Now().Location()), nil } -func (h *Handler) autocompleteCategories(c *gin.Context) { - budgetID := c.Param("budgetid") - budgetUUID, err := uuid.Parse(budgetID) - if err != nil { - c.AbortWithError(http.StatusBadRequest, fmt.Errorf("budgetid missing from URL")) - return - } - - query := c.Request.URL.Query().Get("s") - searchParams := postgres.SearchCategoriesParams{ - BudgetID: budgetUUID, - Search: "%" + query + "%", - } - categories, err := h.Service.SearchCategories(c.Request.Context(), searchParams) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - c.JSON(http.StatusOK, categories) -} - -func (h *Handler) autocompletePayee(c *gin.Context) { - budgetID := c.Param("budgetid") - budgetUUID, err := uuid.Parse(budgetID) - if err != nil { - c.AbortWithError(http.StatusBadRequest, fmt.Errorf("budgetid missing from URL")) - return - } - - query := c.Request.URL.Query().Get("s") - searchParams := postgres.SearchPayeesParams{ - BudgetID: budgetUUID, - Search: query + "%", - } - payees, err := h.Service.SearchPayees(c.Request.Context(), searchParams) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - c.JSON(http.StatusOK, payees) -} - func (h *Handler) budgetingForMonth(c *gin.Context) { budgetID := c.Param("budgetid") budgetUUID, err := uuid.Parse(budgetID) @@ -134,7 +90,7 @@ func (h *Handler) budgetingForMonth(c *gin.Context) { return } - var availableBalance float64 = 0 + availableBalance := postgres.NewZeroNumeric() for _, cat := range categories { if cat.ID != budget.IncomeCategoryID { continue @@ -150,13 +106,13 @@ func (h *Handler) budgetingForMonth(c *gin.Context) { continue } - availableBalance += bal.Transactions.GetFloat64() + availableBalance = availableBalance.Add(bal.Transactions) } } data := struct { Categories []CategoryWithBalance - AvailableBalance float64 + AvailableBalance postgres.Numeric }{categoriesWithBalance, availableBalance} c.JSON(http.StatusOK, data) @@ -182,39 +138,36 @@ func (h *Handler) budgeting(c *gin.Context) { return } - categories, err := h.Service.GetCategories(c.Request.Context(), budgetUUID) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - data := struct { - Budget postgres.Budget - Accounts []postgres.GetAccountsWithBalanceRow - Categories []postgres.GetCategoriesRow - }{ - Accounts: accounts, - Budget: budget, - Categories: categories, - } + Accounts []postgres.GetAccountsWithBalanceRow + Budget postgres.Budget + }{accounts, budget} c.JSON(http.StatusOK, data) } -func (h *Handler) calculateBalances(c *gin.Context, budget postgres.Budget, firstOfNextMonth time.Time, firstOfMonth time.Time, categories []postgres.GetCategoriesRow, cumultativeBalances []postgres.GetCumultativeBalancesRow) ([]CategoryWithBalance, float64, error) { +func (h *Handler) calculateBalances(c *gin.Context, budget postgres.Budget, firstOfNextMonth time.Time, firstOfMonth time.Time, categories []postgres.GetCategoriesRow, cumultativeBalances []postgres.GetCumultativeBalancesRow) ([]CategoryWithBalance, postgres.Numeric, error) { categoriesWithBalance := []CategoryWithBalance{} hiddenCategory := CategoryWithBalance{ GetCategoriesRow: &postgres.GetCategoriesRow{ Name: "", Group: "Hidden Categories", }, + Available: postgres.NewZeroNumeric(), + AvailableLastMonth: postgres.NewZeroNumeric(), + Activity: postgres.NewZeroNumeric(), + Assigned: postgres.NewZeroNumeric(), } - var moneyUsed float64 = 0 + moneyUsed := postgres.NewZeroNumeric() for i := range categories { cat := &categories[i] categoryWithBalance := CategoryWithBalance{ - GetCategoriesRow: cat, + GetCategoriesRow: cat, + Available: postgres.NewZeroNumeric(), + AvailableLastMonth: postgres.NewZeroNumeric(), + Activity: postgres.NewZeroNumeric(), + Assigned: postgres.NewZeroNumeric(), } for _, bal := range cumultativeBalances { if bal.CategoryID != cat.ID { @@ -225,29 +178,29 @@ func (h *Handler) calculateBalances(c *gin.Context, budget postgres.Budget, firs continue } - moneyUsed -= bal.Assignments.GetFloat64() - categoryWithBalance.Available += bal.Assignments.GetFloat64() - categoryWithBalance.Available += bal.Transactions.GetFloat64() - if categoryWithBalance.Available < 0 && bal.Date.Before(firstOfMonth) { - moneyUsed += categoryWithBalance.Available - categoryWithBalance.Available = 0 + moneyUsed = moneyUsed.Sub(bal.Assignments) + categoryWithBalance.Available = categoryWithBalance.Available.Add(bal.Assignments) + categoryWithBalance.Available = categoryWithBalance.Available.Add(bal.Transactions) + if !categoryWithBalance.Available.IsPositive() && bal.Date.Before(firstOfMonth) { + moneyUsed = moneyUsed.Add(categoryWithBalance.Available) + categoryWithBalance.Available = postgres.NewZeroNumeric() } if bal.Date.Before(firstOfMonth) { categoryWithBalance.AvailableLastMonth = categoryWithBalance.Available } else if bal.Date.Before(firstOfNextMonth) { - categoryWithBalance.Activity = bal.Transactions.GetFloat64() - categoryWithBalance.Assigned = bal.Assignments.GetFloat64() + categoryWithBalance.Activity = bal.Transactions + categoryWithBalance.Assigned = bal.Assignments } } // do not show hidden categories if cat.Group == "Hidden Categories" { - hiddenCategory.Available += categoryWithBalance.Available - hiddenCategory.AvailableLastMonth += categoryWithBalance.AvailableLastMonth - hiddenCategory.Activity += categoryWithBalance.Activity - hiddenCategory.Assigned += categoryWithBalance.Assigned + hiddenCategory.Available = hiddenCategory.Available.Add(categoryWithBalance.Available) + hiddenCategory.AvailableLastMonth = hiddenCategory.AvailableLastMonth.Add(categoryWithBalance.AvailableLastMonth) + hiddenCategory.Activity = hiddenCategory.Activity.Add(categoryWithBalance.Activity) + hiddenCategory.Assigned = hiddenCategory.Assigned.Add(categoryWithBalance.Assigned) continue } diff --git a/http/http.go b/http/http.go index 01a0742..745e60b 100644 --- a/http/http.go +++ b/http/http.go @@ -64,6 +64,7 @@ func (h *Handler) Serve() { authenticated.GET("/account/:accountid/transactions", h.transactionsForAccount) authenticated.GET("/admin/clear-database", h.clearDatabase) authenticated.GET("/budget/:budgetid", h.budgeting) + authenticated.GET("/budget/:budgetid/:year/:month", h.budgetingForMonth) authenticated.GET("/budget/:budgetid/autocomplete/payees", h.autocompletePayee) authenticated.GET("/budget/:budgetid/autocomplete/categories", h.autocompleteCategories) authenticated.DELETE("/budget/:budgetid", h.deleteBudget) diff --git a/postgres/numeric.go b/postgres/numeric.go index 5df7bba..38e75b1 100644 --- a/postgres/numeric.go +++ b/postgres/numeric.go @@ -1,11 +1,20 @@ package postgres -import "github.com/jackc/pgtype" +import ( + "fmt" + "math/big" + + "github.com/jackc/pgtype" +) type Numeric struct { pgtype.Numeric } +func NewZeroNumeric() Numeric { + return Numeric{pgtype.Numeric{Exp: 0, Int: big.NewInt(0), Status: pgtype.Present, NaN: false}} +} + func (n Numeric) GetFloat64() float64 { if n.Status != pgtype.Present { return 0 @@ -33,3 +42,83 @@ func (n Numeric) IsZero() bool { float := n.GetFloat64() return float == 0 } + +func (n Numeric) MatchExp(exp int32) Numeric { + diffExp := exp - n.Exp + factor := big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(diffExp)), nil) + return Numeric{pgtype.Numeric{ + Exp: exp, + Int: big.NewInt(0).Mul(n.Int, factor), + Status: n.Status, + NaN: n.NaN, + }} +} + +func (n Numeric) Sub(o Numeric) Numeric { + if n.Exp > o.Exp { + o = o.MatchExp(n.Exp) + } else if n.Exp < o.Exp { + n = n.MatchExp(o.Exp) + } + + if o.Exp == n.Exp { + return Numeric{pgtype.Numeric{ + Exp: n.Exp, + Int: big.NewInt(0).Sub(o.Int, n.Int), + }} + } + + panic("Cannot subtract with different exponents") +} +func (n Numeric) Add(o Numeric) Numeric { + fmt.Println("N", n, "O", o) + if n.Exp > o.Exp { + o = o.MatchExp(n.Exp) + } else if n.Exp < o.Exp { + n = n.MatchExp(o.Exp) + } + + fmt.Println("NM", n, "OM", o) + if o.Exp == n.Exp { + return Numeric{pgtype.Numeric{ + Exp: n.Exp, + Int: big.NewInt(0).Add(o.Int, n.Int), + }} + } + + panic("Cannot add with different exponents") +} + +func (n Numeric) MarshalJSON() ([]byte, error) { + if n.Int.Int64() == 0 { + return []byte("\"0\""), nil + } + + s := fmt.Sprintf("%d", n.Int) + bytes := []byte(s) + + exp := n.Exp + for exp > 0 { + bytes = append(bytes, byte('0')) + exp-- + } + + if exp == 0 { + return bytes, nil + } + + length := int32(len(bytes)) + var bytesWithSeparator []byte + + exp = -exp + for length <= exp { + bytes = append(bytes, byte('0')) + length++ + } + + split := length - exp + bytesWithSeparator = append(bytesWithSeparator, bytes[:split]...) + bytesWithSeparator = append(bytesWithSeparator, byte('.')) + bytesWithSeparator = append(bytesWithSeparator, bytes[split:]...) + return bytesWithSeparator, nil +} diff --git a/web/src/components/Currency.vue b/web/src/components/Currency.vue new file mode 100644 index 0000000..ca60b55 --- /dev/null +++ b/web/src/components/Currency.vue @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/web/src/pages/Account.vue b/web/src/pages/Account.vue index 963054c..91f5e2b 100644 --- a/web/src/pages/Account.vue +++ b/web/src/pages/Account.vue @@ -14,11 +14,6 @@ export default defineComponent({ }, components: { Autocomplete }, props: ["budgetid", "accountid"], - watch: { - Payee() { - console.log(this.$data.Payee); - } - }, methods: { saveTransaction(e : MouseEvent) { e.preventDefault(); @@ -34,8 +29,7 @@ export default defineComponent({ }), headers: this.$store.getters.AuthHeaders, }) - .then(x => x.json()) - .then(x => console.log(x)); + .then(x => x.json()); }, } }) diff --git a/web/src/pages/BudgetSidebar.vue b/web/src/pages/BudgetSidebar.vue index f087100..872e305 100644 --- a/web/src/pages/BudgetSidebar.vue +++ b/web/src/pages/BudgetSidebar.vue @@ -1,8 +1,10 @@ @@ -21,14 +23,14 @@ export default defineComponent({ On-Budget Accounts
{{account.Name}} - {{(account.Balance.Int / 100).toLocaleString(undefined, {minimumFractionDigits: 2,})}} € +
  • Off-Budget Accounts
    - {{account.Name}} - {{account.Balance.Int / 100}} + {{account.Name}} +
  • diff --git a/web/src/pages/Budgeting.vue b/web/src/pages/Budgeting.vue index 8143222..8c4fc19 100644 --- a/web/src/pages/Budgeting.vue +++ b/web/src/pages/Budgeting.vue @@ -1,38 +1,53 @@