commit 8864c8399822be5d86dd8ec2bfad6e236d11e208 Author: Samuel Attwood Date: Mon Jul 18 00:09:38 2022 -0400 Initial Commit diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..228a343 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module cashflow + +go 1.17 + +require gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e387ff0 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..48ba9f5 --- /dev/null +++ b/main.go @@ -0,0 +1,350 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "math" + "os" + "sort" + "strings" + "time" + + "gopkg.in/yaml.v3" +) + +type timeSlice []time.Time + +func (s timeSlice) Less(i, j int) bool { return s[i].Before(s[j]) } +func (s timeSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s timeSlice) Len() int { return len(s) } + +type Account struct { + Name string `yaml:"name"` + Balance float64 `yaml:"balance"` + Minimum float64 `yaml:"minimum"` +} + +type Charge struct { + Name string + Late bool + Amount float64 + Date time.Time +} + +type Config struct { + Accounts []Account + Credit []Transaction + Transactions []Transaction +} + +type ConfigYaml struct { + Accounts []Account + Credit []TransactionYaml + Transactions []TransactionYaml +} + +type Transaction struct { + Amount float64 + Date time.Time + Frequency string + HardDate bool + Late bool + Name string + OrigDate time.Time +} + +type TransactionYaml struct { + Name string `yaml:"name"` + Amount float64 `yaml:"amount"` + Date string `yaml:"date"` + Frequency string `yaml:"frequency,omitempty"` +} + +type TransactionLog struct { + Transactions map[time.Time][]Transaction + Dates []time.Time + StartDate time.Time + EndDate time.Time +} + +func parseConfig(configYaml *ConfigYaml) Config { + config := Config{ + Accounts: configYaml.Accounts, + Credit: make([]Transaction, 0), + Transactions: make([]Transaction, 0), + } + + for _, transaction := range configYaml.Credit { + formalDate, hardDate := parseDate(transaction.Date) + newTransaction := Transaction{ + Amount: transaction.Amount, + Date: formalDate, + OrigDate: formalDate, + HardDate: hardDate, + Frequency: transaction.Frequency, + Late: false, + Name: transaction.Name, + } + config.Transactions = append(config.Transactions, newTransaction) + } + + for _, transaction := range configYaml.Transactions { + formalDate, hardDate := parseDate(transaction.Date) + newTransaction := Transaction{ + Amount: transaction.Amount, + Date: formalDate, + OrigDate: formalDate, + HardDate: hardDate, + Frequency: transaction.Frequency, + Late: false, + Name: transaction.Name, + } + config.Credit = append(config.Credit, newTransaction) + } + + return config +} + +func parseDate(date string) (time.Time, bool) { + hardDate := true + base := time.Now().AddDate(0, 0, -time.Now().Day()+1) + current := time.Date(base.Year(), + base.Month(), base.Day(), 0, 0, 0, 0, time.UTC) + dateString := string(date) + formalDate, err := time.Parse("2006-1-2", dateString) + if err != nil { + hardDate = false + if date == "first" { + formalDate = current + } else if date == "last" { + formalDate = current.AddDate(0, 1, -current.Day()) + } else { + dateString = fmt.Sprintf("%d-%d-%s", + time.Now().Year(), int(time.Now().Month()), date) + formalDate, _ = time.Parse("2006-1-2", dateString) + } + } + return formalDate, hardDate +} + +func preFetchDates(transactions []Transaction, transactionLog *TransactionLog) { + for _, transaction := range transactions { + if _, ok := transactionLog.Transactions[transaction.Date]; !ok { + transactionLog.Transactions[transaction.Date] = make([]Transaction, 0) + transactionLog.Dates = append(transactionLog.Dates, transaction.Date) + } + } + sort.Sort(timeSlice(transactionLog.Dates)) + transactionLog.StartDate = transactionLog.Dates[0] + transactionLog.EndDate = transactionLog.Dates[len(transactionLog.Dates)-1] +} + +func collectTransactions(transactions []Transaction, transactionLog *TransactionLog) { + for _, transaction := range transactions { + if math.Abs(transaction.Amount) != 0 { + addTransaction(transactionLog, &transaction) + } + } + sort.Sort(timeSlice(transactionLog.Dates)) + for _, date := range transactionLog.Dates { + sortTransactions(transactionLog.Transactions[date]) + } +} + +func sortTransactions(transactions []Transaction) { + sort.Slice(transactions, func(i, j int) bool { + if transactions[i].Amount < 0 && transactions[j].Amount < 0 { + return math.Abs(transactions[i].Amount) > math.Abs(transactions[j].Amount) + } else { + return transactions[i].Amount > transactions[j].Amount + } + }) +} + +func addTransaction(transactionLog *TransactionLog, transaction *Transaction) { + var dateList []time.Time + if transaction.Frequency != "" { + dateList = calculateFrequency(transaction.Frequency, + transaction.Date, transactionLog.EndDate, transaction.HardDate) + } else { + dateList = []time.Time{transaction.Date} + } + for _, date := range dateList { + if _, ok := transactionLog.Transactions[date]; !ok { + transactionLog.Dates = append(transactionLog.Dates, date) + } + transactionLog.Transactions[date] = append(transactionLog.Transactions[date], + Transaction{ + Name: transaction.Name, + Amount: transaction.Amount, + Date: date, + OrigDate: date, + HardDate: false, + Late: false, + Frequency: "", + }) + } +} + +func delayTransaction(transactionLog *TransactionLog, transaction *Transaction) { + transaction.Late = true + newDate := transaction.Date.AddDate(0, 0, 1) + _, ok := transactionLog.Transactions[newDate] + for !ok { + if newDate == transactionLog.EndDate { + break + } + newDate = newDate.AddDate(0, 0, 1) + _, ok = transactionLog.Transactions[newDate] + } + transaction.Date = newDate + transactionLog.Transactions[newDate] = append(transactionLog.Transactions[newDate], *transaction) +} + +func executeTransaction(transactionLog *TransactionLog, accounts []Account, transaction *Transaction) bool { + executed := false + finalDate := transactionLog.EndDate + if transaction.Amount > 0 { + accounts[0].Balance = accounts[0].Balance + transaction.Amount + executed = true + } + if transaction.Amount < 0 && math.Abs(transaction.Amount) > (accounts[0].Balance-accounts[0].Minimum) && transaction.Date.Before(finalDate) { + delayTransaction(transactionLog, transaction) + } else if transaction.Amount < 0 { + accounts[0].Balance = accounts[0].Balance + transaction.Amount + executed = true + } + + return executed +} + +func calculateFrequency(frequency string, startDate time.Time, endDate time.Time, hardDate bool) []time.Time { + base := time.Now() + current := time.Date(base.Year(), + base.Month(), base.Day(), 0, 0, 0, 0, time.UTC) + initDate := startDate + dateList := make([]time.Time, 0) + for initDate.Before(endDate) { + if initDate.Before(current) && !hardDate { + } else { + dateList = append(dateList, initDate) + } + if frequency == "daily" { + initDate = initDate.AddDate(0, 0, 1) + } else if frequency == "weekly" { + initDate = initDate.AddDate(0, 0, 7) + } else if frequency == "biweekly" { + initDate = initDate.AddDate(0, 0, 14) + } else if frequency == "monthly" { + initDate = initDate.AddDate(0, 1, 0) + } else { + break + } + } + return dateList +} + +func printAllTransactions(transactionLog *TransactionLog) { + for _, d := range transactionLog.Dates { + for _, t := range transactionLog.Transactions[d] { + fmt.Printf("Date: %s | Transaction: %s\n", t.Date, t.Name) + } + } +} + +func printOutput(date time.Time, initialBalances []Account, executedCharges []Transaction, accounts []Account) { + delim := strings.Repeat("=", 67) + divider := delim + delim2 := strings.Repeat("-", 66) + divider2 := delim2 + "|" + + fmt.Println(divider) + output := fmt.Sprintf(date.Format("Jan 02 2006")) + rear_spacer := strings.Repeat(" ", 65-len(output)) + fmt.Printf("%s%s |\n", output, rear_spacer) + for _, v := range initialBalances { + spacer := strings.Repeat(" ", 19-len(fmt.Sprintf("%.2f", v.Balance))) + output := fmt.Sprintf("$%.2f%s| %s", v.Balance, spacer, v.Name) + rear_spacer := strings.Repeat(" ", 65-len(output)) + fmt.Printf("%s%s |\n", output, rear_spacer) + } + for _, charge := range executedCharges { + spacer := strings.Repeat(" ", 18-len(fmt.Sprintf("%.2f", math.Abs(charge.Amount)))) + if charge.Amount <= 0 { + output := fmt.Sprintf("-$%.2f%s| %s", math.Abs(charge.Amount), spacer, charge.Name) + if charge.Late { + late_tag := fmt.Sprintf("(%s - %s)", "Late", charge.OrigDate.Format("Jan 02 2006")) + rear_spacer := strings.Repeat(" ", 65-len(late_tag)-len(output)) + fmt.Printf("%s%s%s |\n", output, rear_spacer, late_tag) + } else { + rear_spacer := strings.Repeat(" ", 65-len(output)) + fmt.Printf("%s%s |\n", output, rear_spacer) + } + } else { + output := fmt.Sprintf("+$%.2f%s| %s", charge.Amount, spacer, charge.Name) + if charge.Late { + late_tag := fmt.Sprintf("(%s - %s)", "Late", charge.OrigDate.Format("Jan 02 2006")) + rear_spacer := strings.Repeat(" ", 65-len(late_tag)-len(output)) + fmt.Printf("%s%s%s |\n", output, rear_spacer, late_tag) + } else { + rear_spacer := strings.Repeat(" ", 65-len(output)) + fmt.Printf("%s%s |\n", output, rear_spacer) + } + } + } + fmt.Println(divider2) + for _, v := range accounts { + spacer := strings.Repeat(" ", 19-len(fmt.Sprintf("%.2f", v.Balance))) + output := fmt.Sprintf("$%.2f%s| %s", v.Balance, spacer, v.Name) + rear_spacer := strings.Repeat(" ", 65-len(output)) + fmt.Printf("%s%s |\n", output, rear_spacer) + } + fmt.Println(divider + "\n") +} + +func main() { + yfile, err := ioutil.ReadFile(os.Args[1]) + + if err != nil { + log.Fatal(err) + } + + var configYaml ConfigYaml + + transactionLog := TransactionLog{ + Transactions: map[time.Time][]Transaction{}, + Dates: make([]time.Time, 0), + } + + err = yaml.Unmarshal(yfile, &configYaml) + if err != nil { + log.Fatal(err) + } + + config := parseConfig(&configYaml) + preFetchDates(config.Credit, &transactionLog) + preFetchDates(config.Transactions, &transactionLog) + collectTransactions(config.Credit, &transactionLog) + collectTransactions(config.Transactions, &transactionLog) + + for _, date := range transactionLog.Dates { + dateCharges := transactionLog.Transactions[date] + chargeCount := len(dateCharges) + executedCharges := make([]Transaction, 0) + initialBalances := make([]Account, len(config.Accounts)) + copy(initialBalances, config.Accounts) + for _, charge := range dateCharges { + executed := executeTransaction(&transactionLog, config.Accounts, &charge) + if executed { + executedCharges = append(executedCharges, charge) + } else { + chargeCount-- + } + } + if chargeCount > 0 { + printOutput(date, initialBalances, executedCharges, config.Accounts) + } + } + +}