Initial Commit

This commit is contained in:
2022-07-18 00:09:38 -04:00
commit 8864c83998
3 changed files with 359 additions and 0 deletions

350
main.go Normal file
View File

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