Initial Commit
This commit is contained in:
350
main.go
Normal file
350
main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user