From 36f54c5322ceac0d0560fcb072c831bd01c2eb89 Mon Sep 17 00:00:00 2001 From: oscar bernardini Date: Fri, 6 Aug 2021 15:25:41 +0200 Subject: [PATCH] stats function --- kakebo.go | 216 +++++++++++++++++++++++++++++-------------------- kakebo_test.go | 29 ++++++- 2 files changed, 154 insertions(+), 91 deletions(-) diff --git a/kakebo.go b/kakebo.go index 940071d..118d059 100644 --- a/kakebo.go +++ b/kakebo.go @@ -39,21 +39,18 @@ func FormatEntries(entryData string) (string, error) { return strings.Join(formattedEntries, ""), nil } -// CalcBalance -// -// Input example: -// -// -120 y foo -// -34.5 m bar -// -6 M baz -// 789 Y xyzzy -// -// Output example: -// -// 15.25 -// -func CalcBalance(dueData string) (decimal.Decimal, error) { - return sumValues(dueData, extractDueValue) +func formatEntry(fields []string) (string, error) { + if len(fields) < 2 { + return "", fmt.Errorf("at least 2 fields required") + } + + s := strings.Replace(fields[0], ",", ".", 1) + amount, err := decimal.NewFromString(s) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s\t%s\n", strings.Title(fields[1]), money(amount)), nil } // CalcMonth @@ -73,6 +70,57 @@ func CalcMonth(monthData string) (decimal.Decimal, error) { return sumValues(monthData, extractFormattedEntryValue) } +func extractFormattedEntryValue(fields []string) (decimal.Decimal, error) { + return decimal.NewFromString(fields[1]) +} + +// CalcBalance +// +// Input example: +// +// -120 y foo +// -34.5 m bar +// -6 M baz +// 789 Y xyzzy +// +// Output example: +// +// 15.25 +// +func CalcBalance(dueData string) (decimal.Decimal, error) { + return sumValues(dueData, extractDueValue) +} + +const ( + monthly int64 = 1 + yearly int64 = 12 +) + +var intervalDictionary = map[string]int64{ + "m": monthly, + "M": monthly, + "y": yearly, + "Y": yearly, +} + +func extractDueValue(fields []string) (decimal.Decimal, error) { + if len(fields) < 2 { + return decimal.Decimal{}, fmt.Errorf("at least 2 fields required") + } + + amount, err := decimal.NewFromString(fields[0]) + if err != nil { + return decimal.Decimal{}, err + } + + interval, ok := intervalDictionary[fields[1]] + if !ok { + return decimal.Decimal{}, fmt.Errorf("unknown interval '%s'", fields[1]) + } + + return amount.Div(decimal.NewFromInt(interval)), nil +} + // DisplayMonth // // Input example: @@ -85,116 +133,106 @@ func CalcMonth(monthData string) (decimal.Decimal, error) { // // January 2020 // -// Foo 1,2 +// Foo 1,20 // Bar 3,45 // Baz 6,00 // // Tot 10,65 // -func DisplayMonth(period time.Time, monthData string, monthTot decimal.Decimal) string { +func DisplayMonth(date time.Time, monthData string, monthTot decimal.Decimal) string { var lines []string - lines = append(lines, fmt.Sprintln(period.Month(), period.Year())) // header - lines = append(lines, monthData) // body - lines = append(lines, fmt.Sprintf("Tot\t%s\n", monthTot.StringFixed(2))) // footer + lines = append(lines, fmt.Sprintln(date.Month(), date.Year())) // header + lines = append(lines, monthData) // body + lines = append(lines, fmt.Sprintf("Tot\t%s\n", money(monthTot))) // footer - display := strings.Join(lines, "\n") + text := strings.Join(lines, "\n") - return strings.ReplaceAll(display, ".", ",") + return replaceDotsWithCommas(text) } // DisplayStats // // Input example: // -// TODO: proper input example -// CalcBalance(), CalcMonth(), 35 +// time.Time{2009/11/10}, decimal.Decimal{1000}, decimal.Decimal{100}, 10 // // Output example: // -// Save goal 100,00 +// 10 November 2009 // -// Monthly budget 500,00 -// Daily budget 5,00 +// Save goal 100,00 +// Monthly budget 900,00 +// Daily budget 30,00 // -// End of month 70% -// Amount spent 90% +// End of month 33% +// Amount spent 11% // -func DisplayStats(balance, monthTot decimal.Decimal, savePercentage int) (string, error) { - // check savePercentage - - // saveGoal := balance / 100 * savePercentage - - // monthlyBudget := balance - saveGoal - - // now := time.Now() - - // monthDays := ? - - // dailyBudget := monthlyBudget / monthDays +func DisplayStats(date time.Time, balance, monthTot decimal.Decimal, savePercentage int) string { + var lines []string - // endOfMonthPercentage := 100 * now.Day() / monthDays + hundred := decimal.NewFromInt(100) + savePercent := decimal.NewFromInt(int64(savePercentage)) + today := decimal.NewFromInt(int64(date.Day())) + + saveGoal := balance.Div(hundred).Mul(savePercent) + monthlyBudget := balance.Sub(saveGoal) + monthDays := daysOfMonth(date) + dailyBudget := monthlyBudget.DivRound(monthDays, 2) + endOfMonthPercentage := hundred.Mul(today).DivRound(monthDays, 0) + spentAmountPercentage := hundred.Mul(monthTot).DivRound(monthlyBudget, 0) + + y, m, d := date.Date() + body := [][]string{ + {"Save goal", money(saveGoal)}, + {"Monthly budget", money(monthlyBudget)}, + {"Daily budget", money(dailyBudget)}, + } + footer := [][]string{ + {"End of month", percentage(endOfMonthPercentage)}, + {"Amount spent", percentage(spentAmountPercentage)}, + } - // spentAmountPercentage := 100 * monthTot / monthlyBudget + lines = append(lines, fmt.Sprintln(d, m, y)) + lines = append(lines, formatStats(body)) + lines = append(lines, formatStats(footer)) - // stats := map[string]string{ - // "Save goal": saveGoal, - // "Monthly budget": monthlyBudget, - // "Daily budget": dailyBudget, - // "End of month": endOfMonthPercentage, - // "Amount spent": spentAmountPercentage, - // } - // formatStats(stats) + text := strings.Join(lines, "\n") - return "", nil + return replaceDotsWithCommas(text) } -const ( - monthly int64 = 1 - yearly int64 = 12 -) +func daysOfMonth(date time.Time) decimal.Decimal { + y, m, _ := date.Date() + endOfMonth := time.Date(y, m+1, 0, 0, 0, 0, 0, date.Location()) -var intervalDictionary = map[string]int64{ - "m": monthly, - "M": monthly, - "y": yearly, - "Y": yearly, + return decimal.NewFromInt(int64(endOfMonth.Day())) } -func extractDueValue(fields []string) (decimal.Decimal, error) { - if len(fields) < 2 { - return decimal.Decimal{}, fmt.Errorf("at least 2 fields required") - } - - amount, err := decimal.NewFromString(fields[0]) - if err != nil { - return decimal.Decimal{}, err - } +func formatStats(stats [][]string) string { + var text string - interval, ok := intervalDictionary[fields[1]] - if !ok { - return decimal.Decimal{}, fmt.Errorf("unknown interval '%s'", fields[1]) + for _, cols := range stats { + text += fmt.Sprintf("%s\t%s\n", cols[0], cols[1]) } - return amount.Div(decimal.NewFromInt(interval)), nil + return text } -func extractFormattedEntryValue(fields []string) (decimal.Decimal, error) { - return decimal.NewFromString(fields[1]) -} +// +// Common stuff +// -func formatEntry(fields []string) (string, error) { - if len(fields) < 2 { - return "", fmt.Errorf("at least 2 fields required") - } +func lines(data string) []string { + return strings.Split(strings.Trim(data, "\n"), "\n") +} - s := strings.Replace(fields[0], ",", ".", 1) - amount, err := decimal.NewFromString(s) - if err != nil { - return "", err - } +func money(amount decimal.Decimal) string { + return amount.StringFixed(2) +} - return fmt.Sprintf("%s\t%s\n", strings.Title(fields[1]), amount.StringFixed(2)), nil +func percentage(amount decimal.Decimal) string { + return amount.String() + "%" } func sumValues(data string, extractor func([]string) (decimal.Decimal, error)) (decimal.Decimal, error) { @@ -212,6 +250,6 @@ func sumValues(data string, extractor func([]string) (decimal.Decimal, error)) ( return tot, nil } -func lines(data string) []string { - return strings.Split(strings.Trim(data, "\n"), "\n") +func replaceDotsWithCommas(text string) string { + return strings.ReplaceAll(text, ".", ",") } diff --git a/kakebo_test.go b/kakebo_test.go index b8487f9..7983976 100644 --- a/kakebo_test.go +++ b/kakebo_test.go @@ -3,6 +3,8 @@ package kakebo import ( "testing" "time" + + "github.com/shopspring/decimal" ) // Test FormatEntries @@ -201,14 +203,37 @@ Bar 3,45 Baz 6,00 Xyzzy 78,09 -Tot 88,74 +Tot 100,00 ` date := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) - tot, _ := CalcMonth(monthData) + tot := decimal.NewFromInt(100) got := DisplayMonth(date, monthData, tot) if got != want { t.Fatalf("\n GOT: %#v\nWANT: %#v", got, want) } } + +// Test DisplayStats +// +func TestDisplayStats(t *testing.T) { + want := `10 November 2009 + +Save goal 100,00 +Monthly budget 900,00 +Daily budget 30,00 + +End of month 33% +Amount spent 11% +` + + date := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) + bal := decimal.NewFromInt(1000) + tot := decimal.NewFromInt(100) + got := DisplayStats(date, bal, tot, 10) + + if got != want { + t.Fatalf("\n GOT: %#v\nWANT: %#v", got, want) + } +}