From 7f7df35faed7c633636ceea001a2bf9659682645 Mon Sep 17 00:00:00 2001 From: Samuel Attwood Date: Mon, 3 Oct 2022 15:26:45 -0400 Subject: [PATCH] Significant fixes and improvements to scheduling logic and backtracking --- go.mod | 5 +- go.sum | 2 + main.go | 246 +++++++++++++++++++++++++++++++++----------------------- 3 files changed, 150 insertions(+), 103 deletions(-) diff --git a/go.mod b/go.mod index 9a4091a..02df840 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) -require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect +require ( + github.com/google/uuid v1.3.0 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect +) diff --git a/go.sum b/go.sum index 32aac57..fc92ce7 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= diff --git a/main.go b/main.go index 3e4eed3..c05b18e 100644 --- a/main.go +++ b/main.go @@ -67,26 +67,27 @@ type TransactionYaml struct { Date string `yaml:"date"` Delayable *bool `yaml:"delayable"` Frequency string `yaml:"frequency,omitempty"` - Shift string `yaml:"shift` + Shift string `yaml:"shift"` } type TransactionLog struct { - Transactions map[time.Time]*LogDay StartDate time.Time EndDate time.Time + Iteration int + LogDays map[time.Time]*LogDay + Transactions map[string]*Transaction } type LogDay struct { Date time.Time StartingBalance []Account EndingBalance []Account - Executed []*Transaction Transactions map[string]*Transaction } func (t TransactionLog) dates() timeSlice { dateSlice := make(timeSlice, 0) - for date, _ := range t.Transactions { + for date, _ := range t.LogDays { dateSlice = append(dateSlice, date) } @@ -104,7 +105,19 @@ func (t TransactionLog) last() time.Time { } -func nextLogDay(transactionLog *TransactionLog, currentDate time.Time) LogDay { +func (logDay LogDay) transactions() []*Transaction { + transactions := make([]*Transaction, 0) + + for _, t := range logDay.Transactions { + transactions = append(transactions, t) + } + + sortTransactions(transactions) + + return transactions +} + +func nextLogDay(transactionLog *TransactionLog, currentDate time.Time) *LogDay { var next time.Time for i, date := range transactionLog.dates() { if date == currentDate { @@ -113,10 +126,10 @@ func nextLogDay(transactionLog *TransactionLog, currentDate time.Time) LogDay { } } - return *transactionLog.Transactions[next] + return transactionLog.LogDays[next] } -func previousLogDay(transactionLog *TransactionLog, currentDate time.Time) LogDay { +func previousLogDay(transactionLog *TransactionLog, currentDate time.Time) *LogDay { var previous time.Time for _, date := range transactionLog.dates() { if date == currentDate { @@ -125,7 +138,7 @@ func previousLogDay(transactionLog *TransactionLog, currentDate time.Time) LogDa previous = date } - return *transactionLog.Transactions[previous] + return transactionLog.LogDays[previous] } func parseConfig(configYaml *ConfigYaml) Config { @@ -214,12 +227,12 @@ func parseDate(date string) (time.Time, bool) { func preFetchDates(transactions []Transaction, transactionLog *TransactionLog) { for _, transaction := range transactions { - if _, ok := transactionLog.Transactions[transaction.Date]; !ok && transaction.Amount != 0 { + if _, ok := transactionLog.LogDays[transaction.Date]; !ok && transaction.Amount != 0 { logDay := LogDay{ Date: transaction.Date, Transactions: make(map[string]*Transaction), } - transactionLog.Transactions[transaction.Date] = &logDay + transactionLog.LogDays[transaction.Date] = &logDay } } logDates := transactionLog.dates() @@ -254,27 +267,27 @@ func addTransaction(transactionLog *TransactionLog, transaction *Transaction) { dateList = []time.Time{transaction.Date} } for _, date := range dateList { - if _, ok := transactionLog.Transactions[date]; !ok { + if _, ok := transactionLog.LogDays[date]; !ok { logDay := LogDay{ Date: date, Transactions: make(map[string]*Transaction), } - transactionLog.Transactions[date] = &logDay + transactionLog.LogDays[date] = &logDay } - transactionID := uuid.NewString() - transactionLog.Transactions[date].Transactions[transactionID] = - &Transaction{ - Amount: transaction.Amount, - Date: date, - Delayable: transaction.Delayable, - Frequency: "", - HardDate: false, - ID: transactionID, - Late: false, - Name: transaction.Name, - OrigDate: date, - Shift: transaction.Shift, - } + t := Transaction{ + Amount: transaction.Amount, + Date: date, + Delayable: transaction.Delayable, + Frequency: "", + HardDate: false, + ID: uuid.NewString(), + Late: false, + Name: transaction.Name, + OrigDate: date, + Shift: transaction.Shift, + } + transactionLog.LogDays[date].Transactions[t.ID] = &t + transactionLog.Transactions[t.ID] = &t } } @@ -309,36 +322,47 @@ func delayTransaction(transactionLog *TransactionLog, transaction *Transaction) transaction.Delayable = false } - if _, ok := transactionLog.Transactions[newDate]; !ok { + if _, ok := transactionLog.LogDays[newDate]; !ok { logDay := LogDay{ Date: newDate, Transactions: map[string]*Transaction{}, } - transactionLog.Transactions[newDate] = &logDay + transactionLog.LogDays[newDate] = &logDay } - delete(transactionLog.Transactions[transaction.Date].Transactions, transaction.ID) + delete(transactionLog.LogDays[transaction.Date].Transactions, transaction.ID) transaction.Date = newDate - transactionLog.Transactions[newDate].Transactions[transaction.ID] = transaction + transactionLog.LogDays[newDate].Transactions[transaction.ID] = transaction } -func executeTransaction(transactionLog *TransactionLog, accounts []Account, transaction *Transaction) bool { +func executeTransaction(transactionLog *TransactionLog, transaction *Transaction) bool { + logDay := transactionLog.LogDays[transaction.Date] + accounts := make([]Account, len(logDay.EndingBalance)) + copy(accounts, logDay.EndingBalance) executed := false finalDate := transactionLog.EndDate if transaction.Date.Weekday() == 0 || transaction.Date.Weekday() == 6 && transaction.Delayable { + logrus.Debugf("Delaying %s due to weekend scheduling\n", transaction.Name) delayTransaction(transactionLog, transaction) } else if transaction.Amount > 0 { + logrus.Debugf("Executing Credit: %s %s (%f)\n", logDay.Date, transaction.Name, transaction.Amount) accounts[0].Balance = accounts[0].Balance + transaction.Amount executed = true } else if transaction.Amount < 0 && math.Abs(transaction.Amount) > (accounts[0].Balance-accounts[0].Minimum) && transaction.Date.Before(finalDate) && transaction.Delayable { + logrus.Debugf("Balance: %f | Minimum: %f\n", accounts[0].Balance, accounts[0].Minimum) + logrus.Debugf("Delaying %s: %f (%s) due to lack of funds\n", transaction.Name, transaction.Amount, transaction.Date) delayTransaction(transactionLog, transaction) } else if transaction.Amount < 0 { + logrus.Debugf("Executing Charge: %s %s (%f)\n", logDay.Date, transaction.Name, transaction.Amount) accounts[0].Balance = accounts[0].Balance + transaction.Amount executed = true } + logDay.EndingBalance = make([]Account, len(accounts)) + copy(logDay.EndingBalance, accounts) + return executed } @@ -379,65 +403,91 @@ func calculateFrequency(frequency string, startDate time.Time, endDate time.Time return dateList } -func processTransactions(transactionLog *TransactionLog, accounts []Account) bool { +func categorizeTransactions(transactions map[string]*Transaction) map[string][]string { + var categorized = map[string][]string{ + "credits": make([]string, 0), + "late": make([]string, 0), + "standard": make([]string, 0), + } + + for _, t := range transactions { + if t.Amount > 0 { + categorized["credits"] = append(categorized["credits"], t.ID) + } else if t.Late { + categorized["late"] = append(categorized["late"], t.ID) + } else { + categorized["standard"] = append(categorized["standard"], t.ID) + } + } + + return categorized +} + +func processTransactionLog(transactionLog *TransactionLog, accounts []Account) bool { shifted := false - for _, date := range transactionLog.dates() { - if _, ok := transactionLog.Transactions[date]; ok { - logDay := transactionLog.Transactions[date] - sortedCharges := make([]*Transaction, 0) - - for _, charge := range logDay.Transactions { - sortedCharges = append(sortedCharges, charge) - } - - sortTransactions(sortedCharges) - - startCount := len(sortedCharges) - endCount := startCount - logDay.Executed = make([]*Transaction, 0) - credits := make([]*Transaction, 0) - late := make([]*Transaction, 0) - charges := make([]*Transaction, 0) - logDay.StartingBalance = make([]Account, len(accounts)) - copy(logDay.StartingBalance, accounts) - for _, charge := range sortedCharges { - if charge.Amount > 0 { - credits = append(credits, charge) - } else if charge.Late { - late = append(late, charge) - } else { - charges = append(charges, charge) - } - } - for _, charge := range credits { - if executeTransaction(transactionLog, accounts, charge) { - logDay.Executed = append(logDay.Executed, charge) - } else { - endCount-- - } - } - for _, charge := range late { - if executeTransaction(transactionLog, accounts, charge) { - logDay.Executed = append(logDay.Executed, charge) - } else { - endCount-- - } - } - for _, charge := range charges { - if executeTransaction(transactionLog, accounts, charge) { - logDay.Executed = append(logDay.Executed, charge) - } else { - endCount-- - } - } - logDay.EndingBalance = make([]Account, len(accounts)) - copy(logDay.EndingBalance, accounts) - if len(logDay.Transactions) == 0 { - delete(transactionLog.Transactions, date) - } - if endCount != startCount { + endBalance := accounts + logrus.Debugf("\n\nITERATION %d\n\n", transactionLog.Iteration) + if transactionLog.Iteration == 0 { + for _, date := range transactionLog.dates() { + logDay := transactionLog.LogDays[date] + logDay.StartingBalance = make([]Account, len(endBalance)) + logDay.EndingBalance = make([]Account, len(endBalance)) + copy(logDay.StartingBalance, endBalance) + copy(logDay.EndingBalance, endBalance) + categorized := categorizeTransactions(logDay.Transactions) + logrus.Debugf("---\nStarting Balance %s: %f\n", date, logDay.StartingBalance[0].Balance) + logrus.Debugln(categorized) + if processTransactions(transactionLog, categorized["credits"]) { shifted = true } + logrus.Debugf("Starting Balance %s: %f\n", date, logDay.StartingBalance[0].Balance) + logrus.Debugf("Ending Balance %s: %f\n", date, logDay.EndingBalance[0].Balance) + endBalance = logDay.EndingBalance + } + } else { + for _, date := range transactionLog.dates() { + logDay := transactionLog.LogDays[date] + logDay.StartingBalance = make([]Account, len(endBalance)) + logDay.EndingBalance = make([]Account, len(endBalance)) + copy(logDay.StartingBalance, endBalance) + copy(logDay.EndingBalance, endBalance) + categorized := categorizeTransactions(logDay.Transactions) + logrus.Debugf("---\nStarting Balance %s: %f\n", date, logDay.StartingBalance[0].Balance) + logrus.Debugln(categorized) + if processTransactions(transactionLog, categorized["credits"]) { + shifted = true + } + if processTransactions(transactionLog, categorized["late"]) { + shifted = true + } + if processTransactions(transactionLog, categorized["standard"]) { + shifted = true + } + logrus.Debugf("Starting Balance %s: %f\n", date, logDay.StartingBalance[0].Balance) + logrus.Debugf("Ending Balance %s: %f\n", date, logDay.EndingBalance[0].Balance) + endBalance = logDay.EndingBalance + if shifted { + break + } + if len(logDay.Transactions) == 0 { + delete(transactionLog.LogDays, logDay.Date) + } + } + } + + transactionLog.Iteration++ + return shifted +} + +func processTransactions(transactionLog *TransactionLog, transactionIDs []string) bool { + shifted := false + for _, id := range transactionIDs { + transaction, ok := transactionLog.Transactions[id] + if !ok { + logrus.Fatalf("Transaction ID: %s not found in log\n", id) + } + if !executeTransaction(transactionLog, transaction) { + shifted = true } } @@ -460,7 +510,7 @@ func printOutput(logDay LogDay) { rear_spacer := strings.Repeat(" ", 65-len(outString)) fmt.Printf("%s%s |\n", outString, rear_spacer) } - for _, charge := range logDay.Executed { + for _, charge := range logDay.transactions() { spacer := strings.Repeat(" ", 18-len(fmt.Sprintf("%.2f", math.Abs(charge.Amount)))) outString := "" if charge.Amount <= 0 { @@ -494,7 +544,6 @@ func printOutput(logDay LogDay) { func main() { yfile, err := os.ReadFile(os.Args[1]) - dates := make([]time.Time, 0) if err != nil { log.Fatal(err) @@ -503,7 +552,8 @@ func main() { var configYaml ConfigYaml transactionLog := TransactionLog{ - Transactions: make(map[time.Time]*LogDay), + LogDays: make(map[time.Time]*LogDay), + Transactions: make(map[string]*Transaction), } err = yaml.Unmarshal(yfile, &configYaml) @@ -532,24 +582,16 @@ func main() { collectTransactions(config.Credit, &transactionLog) collectTransactions(config.Transactions, &transactionLog) - current := transactionLog.StartDate - for current.Before(transactionLog.EndDate.AddDate(0, 0, 7)) { - dates = append(dates, current) - current = current.AddDate(0, 0, 1) - } - - startingBalance := make([]Account, len(config.Accounts)) shifted := true - i := 0 for shifted { - copy(startingBalance, config.Accounts) + //startingBalance := make([]Account, len(config.Accounts)) + //copy(startingBalance, config.Accounts) shifted = false - shifted = processTransactions(&transactionLog, startingBalance) - i++ + shifted = processTransactionLog(&transactionLog, config.Accounts) } for _, date := range transactionLog.dates() { - logDay := transactionLog.Transactions[date] + logDay := transactionLog.LogDays[date] printOutput(*logDay) }