package main import ( "fmt" "io/ioutil" "log" "math" "os" "sort" "strings" "time" "github.com/sirupsen/logrus" "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 { Start string End string 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 if formalDate.Before(time.Now()) { formalDate = formalDate.AddDate(0, 1, 0) } } else if date == "last" { formalDate = lastOfMonth(current) } else { dateString = fmt.Sprintf("%d-%d-%s", time.Now().Year(), int(time.Now().Month()), date) formalDate, _ = time.Parse("2006-1-2", dateString) if formalDate.Before(time.Now()) { formalDate = formalDate.AddDate(0, 1, 0) } } } 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 firstOfMonth(t time.Time) time.Time { return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, time.UTC) } func lastOfMonth(t time.Time) time.Time { return time.Date(t.Year(), t.Month()+1, 0, 0, 0, 0, 0, time.UTC) } 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.AddDate(0, 0, 1)) { if !initDate.Before(current) || hardDate { 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" { if initDate == lastOfMonth(initDate) { initDate = lastOfMonth(firstOfMonth(initDate).AddDate(0, 1, 0)) } else { 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.Sprint(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) if configYaml.Start != "" { transactionLog.StartDate, err = time.Parse("2006-1-2", configYaml.Start) if err != nil { logrus.Error(err) } } if configYaml.End != "" { transactionLog.EndDate, err = time.Parse("2006-1-2", configYaml.End) if err != nil { logrus.Error(err) } } collectTransactions(config.Credit, &transactionLog) collectTransactions(config.Transactions, &transactionLog) for _, date := range transactionLog.Dates { dateCharges := transactionLog.Transactions[date] chargeCount := len(dateCharges) executedCharges := make([]Transaction, 0) credits := make([]Transaction, 0) late := make([]Transaction, 0) charges := make([]Transaction, 0) initialBalances := make([]Account, len(config.Accounts)) copy(initialBalances, config.Accounts) for _, charge := range dateCharges { 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, config.Accounts, &charge) { executedCharges = append(executedCharges, charge) } else { chargeCount-- } } for _, charge := range late { if executeTransaction(&transactionLog, config.Accounts, &charge) { executedCharges = append(executedCharges, charge) } else { chargeCount-- } } for _, charge := range charges { if executeTransaction(&transactionLog, config.Accounts, &charge) { executedCharges = append(executedCharges, charge) } else { chargeCount-- } } if chargeCount > 0 { printOutput(date, initialBalances, executedCharges, config.Accounts) } } }