Significant fixes and improvements to scheduling logic and backtracking

This commit is contained in:
2022-10-03 15:26:45 -04:00
parent b8baff3d5f
commit 7f7df35fae
3 changed files with 150 additions and 103 deletions

5
go.mod
View File

@@ -7,4 +7,7 @@ require (
gopkg.in/yaml.v3 v3.0.1 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
)

2
go.sum
View File

@@ -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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=

224
main.go
View File

@@ -67,26 +67,27 @@ type TransactionYaml struct {
Date string `yaml:"date"` Date string `yaml:"date"`
Delayable *bool `yaml:"delayable"` Delayable *bool `yaml:"delayable"`
Frequency string `yaml:"frequency,omitempty"` Frequency string `yaml:"frequency,omitempty"`
Shift string `yaml:"shift` Shift string `yaml:"shift"`
} }
type TransactionLog struct { type TransactionLog struct {
Transactions map[time.Time]*LogDay
StartDate time.Time StartDate time.Time
EndDate time.Time EndDate time.Time
Iteration int
LogDays map[time.Time]*LogDay
Transactions map[string]*Transaction
} }
type LogDay struct { type LogDay struct {
Date time.Time Date time.Time
StartingBalance []Account StartingBalance []Account
EndingBalance []Account EndingBalance []Account
Executed []*Transaction
Transactions map[string]*Transaction Transactions map[string]*Transaction
} }
func (t TransactionLog) dates() timeSlice { func (t TransactionLog) dates() timeSlice {
dateSlice := make(timeSlice, 0) dateSlice := make(timeSlice, 0)
for date, _ := range t.Transactions { for date, _ := range t.LogDays {
dateSlice = append(dateSlice, date) 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 var next time.Time
for i, date := range transactionLog.dates() { for i, date := range transactionLog.dates() {
if date == currentDate { 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 var previous time.Time
for _, date := range transactionLog.dates() { for _, date := range transactionLog.dates() {
if date == currentDate { if date == currentDate {
@@ -125,7 +138,7 @@ func previousLogDay(transactionLog *TransactionLog, currentDate time.Time) LogDa
previous = date previous = date
} }
return *transactionLog.Transactions[previous] return transactionLog.LogDays[previous]
} }
func parseConfig(configYaml *ConfigYaml) Config { func parseConfig(configYaml *ConfigYaml) Config {
@@ -214,12 +227,12 @@ func parseDate(date string) (time.Time, bool) {
func preFetchDates(transactions []Transaction, transactionLog *TransactionLog) { func preFetchDates(transactions []Transaction, transactionLog *TransactionLog) {
for _, transaction := range transactions { 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{ logDay := LogDay{
Date: transaction.Date, Date: transaction.Date,
Transactions: make(map[string]*Transaction), Transactions: make(map[string]*Transaction),
} }
transactionLog.Transactions[transaction.Date] = &logDay transactionLog.LogDays[transaction.Date] = &logDay
} }
} }
logDates := transactionLog.dates() logDates := transactionLog.dates()
@@ -254,27 +267,27 @@ func addTransaction(transactionLog *TransactionLog, transaction *Transaction) {
dateList = []time.Time{transaction.Date} dateList = []time.Time{transaction.Date}
} }
for _, date := range dateList { for _, date := range dateList {
if _, ok := transactionLog.Transactions[date]; !ok { if _, ok := transactionLog.LogDays[date]; !ok {
logDay := LogDay{ logDay := LogDay{
Date: date, Date: date,
Transactions: make(map[string]*Transaction), Transactions: make(map[string]*Transaction),
} }
transactionLog.Transactions[date] = &logDay transactionLog.LogDays[date] = &logDay
} }
transactionID := uuid.NewString() t := Transaction{
transactionLog.Transactions[date].Transactions[transactionID] =
&Transaction{
Amount: transaction.Amount, Amount: transaction.Amount,
Date: date, Date: date,
Delayable: transaction.Delayable, Delayable: transaction.Delayable,
Frequency: "", Frequency: "",
HardDate: false, HardDate: false,
ID: transactionID, ID: uuid.NewString(),
Late: false, Late: false,
Name: transaction.Name, Name: transaction.Name,
OrigDate: date, OrigDate: date,
Shift: transaction.Shift, 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 transaction.Delayable = false
} }
if _, ok := transactionLog.Transactions[newDate]; !ok { if _, ok := transactionLog.LogDays[newDate]; !ok {
logDay := LogDay{ logDay := LogDay{
Date: newDate, Date: newDate,
Transactions: map[string]*Transaction{}, 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 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 executed := false
finalDate := transactionLog.EndDate finalDate := transactionLog.EndDate
if transaction.Date.Weekday() == 0 || transaction.Date.Weekday() == 6 && transaction.Delayable { 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) delayTransaction(transactionLog, transaction)
} else if transaction.Amount > 0 { } 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 accounts[0].Balance = accounts[0].Balance + transaction.Amount
executed = true executed = true
} else if transaction.Amount < 0 && math.Abs(transaction.Amount) > (accounts[0].Balance-accounts[0].Minimum) && transaction.Date.Before(finalDate) && transaction.Delayable { } 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) delayTransaction(transactionLog, transaction)
} else if transaction.Amount < 0 { } 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 accounts[0].Balance = accounts[0].Balance + transaction.Amount
executed = true executed = true
} }
logDay.EndingBalance = make([]Account, len(accounts))
copy(logDay.EndingBalance, accounts)
return executed return executed
} }
@@ -379,65 +403,91 @@ func calculateFrequency(frequency string, startDate time.Time, endDate time.Time
return dateList 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 shifted := false
endBalance := accounts
logrus.Debugf("\n\nITERATION %d\n\n", transactionLog.Iteration)
if transactionLog.Iteration == 0 {
for _, date := range transactionLog.dates() { for _, date := range transactionLog.dates() {
if _, ok := transactionLog.Transactions[date]; ok { logDay := transactionLog.LogDays[date]
logDay := transactionLog.Transactions[date] logDay.StartingBalance = make([]Account, len(endBalance))
sortedCharges := make([]*Transaction, 0) logDay.EndingBalance = make([]Account, len(endBalance))
copy(logDay.StartingBalance, endBalance)
for _, charge := range logDay.Transactions { copy(logDay.EndingBalance, endBalance)
sortedCharges = append(sortedCharges, charge) categorized := categorizeTransactions(logDay.Transactions)
} logrus.Debugf("---\nStarting Balance %s: %f\n", date, logDay.StartingBalance[0].Balance)
logrus.Debugln(categorized)
sortTransactions(sortedCharges) if processTransactions(transactionLog, categorized["credits"]) {
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 {
shifted = true 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)) rear_spacer := strings.Repeat(" ", 65-len(outString))
fmt.Printf("%s%s |\n", outString, rear_spacer) 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)))) spacer := strings.Repeat(" ", 18-len(fmt.Sprintf("%.2f", math.Abs(charge.Amount))))
outString := "" outString := ""
if charge.Amount <= 0 { if charge.Amount <= 0 {
@@ -494,7 +544,6 @@ func printOutput(logDay LogDay) {
func main() { func main() {
yfile, err := os.ReadFile(os.Args[1]) yfile, err := os.ReadFile(os.Args[1])
dates := make([]time.Time, 0)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@@ -503,7 +552,8 @@ func main() {
var configYaml ConfigYaml var configYaml ConfigYaml
transactionLog := TransactionLog{ transactionLog := TransactionLog{
Transactions: make(map[time.Time]*LogDay), LogDays: make(map[time.Time]*LogDay),
Transactions: make(map[string]*Transaction),
} }
err = yaml.Unmarshal(yfile, &configYaml) err = yaml.Unmarshal(yfile, &configYaml)
@@ -532,24 +582,16 @@ func main() {
collectTransactions(config.Credit, &transactionLog) collectTransactions(config.Credit, &transactionLog)
collectTransactions(config.Transactions, &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 shifted := true
i := 0
for shifted { for shifted {
copy(startingBalance, config.Accounts) //startingBalance := make([]Account, len(config.Accounts))
//copy(startingBalance, config.Accounts)
shifted = false shifted = false
shifted = processTransactions(&transactionLog, startingBalance) shifted = processTransactionLog(&transactionLog, config.Accounts)
i++
} }
for _, date := range transactionLog.dates() { for _, date := range transactionLog.dates() {
logDay := transactionLog.Transactions[date] logDay := transactionLog.LogDays[date]
printOutput(*logDay) printOutput(*logDay)
} }