package main import ( "fmt" "log" "os" "sort" "strconv" "strings" "time" "git.sr.ht/~ajwh/bud/expense" "github.com/fatih/color" "github.com/rodaine/table" ) func fail(n int) { log.Fatalf("invalid expense file (line %d)", n+1) } func main() { h, err := os.UserHomeDir() if err != nil { log.Fatal("could not get user home dir") } f, err := os.ReadFile(h + "/.config/bud/expenses") if err != nil { log.Fatal("no expense file") } // load expenses var x []expense.Expense c := expense.Expense{} for n, l := range strings.Split(string(f), "\n") { if len(l) > 0 && l[0] == '#' { continue } p := strings.Split(l, " = ") if len(p) == 1 { if c.IsValid() { x = append(x, c) c = expense.Expense{} continue } else { fail(n) } } // all lines should be like // Property=Value // with exactly one blank line separating each // (comments ignored) if len(p) != 2 { fail(n) } switch p[0] { case "Name": c.Name = p[1] case "Amount": if e := c.A(p[1]); e != nil { fail(n) } case "Months": if e := c.M(p[1]); e != nil { fail(n) } case "Day": if e := c.D(p[1]); e != nil { fail(n) } case "Category": c.Category = &p[1] case "Method": c.Method = &p[1] default: fail(n) } } args := os.Args if len(args) == 1 { showDaily(x, time.Now().Month()) return } switch args[1] { case "show": if len(args) >= 3 { m, err := strconv.Atoi(args[2]) if err != nil || m < 1 || m > 12 { log.Fatal("invalid month") } showMonth(x, time.Month(m)) } else { showMonth(x, time.Now().Month()) } case "daily": if len(args) >= 3 { m, err := strconv.Atoi(args[2]) if err != nil || m < 1 || m > 12 { log.Fatal("invalid month") } showDaily(x, time.Month(m)) } else { showDaily(x, time.Now().Month()) } case "overview": showOverview(x) default: showDaily(x, time.Now().Month()) } } func showOverview(x []expense.Expense) { base := 0 var byMonth [12]int for _, e := range x { if e.IsAll() { base += e.Amount } for i, m := range e.Months { if m { byMonth[i] += e.Amount } } } headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc() columnFmt := color.New(color.FgYellow).SprintfFunc() tbl := table.New("Month", "Amount") tbl.WithHeaderFormatter(headerFmt).WithFirstColumnFormatter(columnFmt) tbl.AddRow("Base", fmt.Sprintf("$%.2f", float64(base)/100.0)) tbl.AddRow("---------", "") for i, a := range byMonth { if a == base { tbl.AddRow(time.Month(i+1).String(), "-") } else { tbl.AddRow(time.Month(i+1).String(), fmt.Sprintf("$%.2f", float64(a)/100.0)) } } tbl.Print() } func showDaily(x []expense.Expense, m time.Month) { days := make([]int, 31) // right now this is just 1st-14th and 15th-EOM segments := []int{0, 0} for _, e := range x { if !e.Months[m-1] { continue } if e.Day == nil { log.Fatal("daily requires all relevant expenses to have days") } d := *e.Day days[d-1] += e.Amount if d < 15 { segments[0] += e.Amount } else { segments[1] += e.Amount } } headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc() columnFmt := color.New(color.FgYellow).SprintfFunc() tbl := table.New("Day", "Total") tbl.WithHeaderFormatter(headerFmt).WithFirstColumnFormatter(columnFmt) for i, t := range days { if t > 0 { tbl.AddRow(i+1, fmt.Sprintf("$%.2f", float64(t)/100.0)) } } fmt.Print("\n") tbl.Print() fmt.Print("\n") fmt.Printf("1st Half: $%.2f\n", float64(segments[0])/100.0) fmt.Printf("2nd Half: $%.2f\n\n", float64(segments[1])/100.0) } func showMonth(x []expense.Expense, m time.Month) { total := 0 var a []expense.Expense for _, e := range x { if e.Months[m-1] { total += e.Amount a = append(a, e) } } sort.Slice(a, func(i, j int) bool { if a[i].Day == nil && a[j].Day != nil { return true } if a[i].Day != nil && a[j].Day == nil { return false } if a[i].Day == nil && a[j].Day == nil { return a[i].Amount > a[j].Amount } if *a[i].Day == *a[j].Day { return a[i].Amount > a[j].Amount } return *a[i].Day < *a[j].Day }) headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc() columnFmt := color.New(color.FgYellow).SprintfFunc() tbl := table.New("Name", "Amount", "Category", "Day", "Method") tbl.WithHeaderFormatter(headerFmt).WithFirstColumnFormatter(columnFmt) for _, e := range a { c, m := "", "" if e.Category != nil { c = *e.Category } if e.Method != nil { m = *e.Method } if e.Day != nil { tbl.AddRow(e.Name, fmt.Sprintf("$%.2f", float64(e.Amount)/100.0), c, *e.Day, m) } else { tbl.AddRow(e.Name, fmt.Sprintf("$%.2f", float64(e.Amount)/100.0), c, "", m) } } fmt.Print("\n") tbl.Print() fmt.Print("\n") fmt.Printf("Total: $%.2f\n\n", float64(total)/100.0) }