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)
}