-
Notifications
You must be signed in to change notification settings - Fork 0
/
loanPay.go
290 lines (239 loc) · 6.59 KB
/
loanPay.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
package main
import (
"fmt"
"io/ioutil"
"log"
"math"
"os"
"sync"
"time"
"gopkg.in/yaml.v2"
)
type loansStruct struct {
Extra float32 `yaml:"extra"`
Loans []struct {
Apy float32 `yaml:"apy"`
Balance float32 `yaml:"balance"`
Min float32 `yaml:"min"`
//Name string `yaml:"name"` // Dont need this right now yet
} `yaml:"loans"`
}
type result struct {
Order []int
Months int16
TotalPaid float32
}
var loans loansStruct
var jobLoan chan result
var jobResults chan result
var fastestResult result
var cheapestResult result
var threads = 8
func main() {
start := time.Now()
var wgp sync.WaitGroup // Permutation
var wgr sync.WaitGroup // Result
jobLoan = make(chan result, 1000)
jobResults = make(chan result, 100)
//Load the loans.yaml file
loadFile()
// This starts up 8 workers, initially blocked
// because there are no jobs yet.
for w := 1; w <= threads; w++ {
wgp.Add(1)
go worker(w, &wgp)
}
//Start processing the results while we wait
go comparator(&wgr)
// Here we send `jobs` and then `close` that
// channel to indicate that's all the work we have.
//Generate possible combinations and add them to queue
permutation(rangeSlice(0, len(loans.Loans)), &wgr)
// Finally we collect all the results of the work.
// This also ensures that the worker goroutines have
// finished.
wgp.Wait()
wgr.Wait()
close(jobResults)
fmt.Println(fastestResult)
fmt.Println(cheapestResult)
fmt.Println(time.Since(start))
}
func loadFile() {
file, err := os.Open("loans.yaml")
if err != nil {
log.Fatal(err)
}
defer file.Close()
b, err := ioutil.ReadAll(file)
err2 := yaml.Unmarshal([]byte(b), &loans)
if err2 != nil {
log.Fatal(err2)
}
}
func worker(id int, waitgroup *sync.WaitGroup) {
for j := range jobLoan {
//fmt.Printf("Started job %v\n", j)
processLoanOrder(j)
}
waitgroup.Done()
}
func comparator(waitgroup *sync.WaitGroup) {
for loan := range jobResults {
if fastestResult.Months == 0 {
fastestResult = loan
cheapestResult = loan
fmt.Printf("Winner 0 (by default): %v\n", loan)
}
//eliminate emmediately, shaves about 0.5% of time
if (loan.Months > fastestResult.Months) || (loan.TotalPaid > fastestResult.TotalPaid) {
waitgroup.Done()
continue
}
if fastestResult.Months >= loan.Months && fastestResult.TotalPaid > loan.TotalPaid {
fastestResult = loan
fmt.Printf("Replacement Fastest Winner: %v\n", loan)
}
if cheapestResult.TotalPaid > loan.TotalPaid && cheapestResult.Months >= loan.Months {
cheapestResult = loan
fmt.Printf("Replacement Cheapest Winner: %v\n", loan)
}
waitgroup.Done()
}
}
func rangeSlice(start, stop int) []int {
if start > stop {
panic("Slice ends before it started")
}
xs := make([]int, stop-start)
for i := 0; i < len(xs); i++ {
xs[i] = i + start
}
return xs
}
func permutation(xs []int, waitgroup *sync.WaitGroup) {
var rc func([]int, int)
rc = func(a []int, k int) {
if k == len(a) {
// append is important to keep order of array
waitgroup.Add(1)
jobLoan <- result{Order: append([]int{}, a...)}
} else {
for i := k; i < len(xs); i++ {
a[k], a[i] = a[i], a[k]
rc(a, k+1)
a[k], a[i] = a[i], a[k]
}
}
}
rc(xs, 0)
close(jobLoan)
}
func processLoanOrder(loan result) {
var balances []float32
canPayExtra := loans.Extra
//Insert balances
for _, l := range loan.Order {
balances = append(balances, loans.Loans[l].Balance)
}
//fmt.Print("Balances: ")
//fmt.Println(balances)
for {
// One month has elapsed
loan.Months++
//This permutation already lost time or money, get out. 5% faster calculations with elimination check
if (fastestResult.Months != 0 && fastestResult.Months < loan.Months && fastestResult.TotalPaid < loan.TotalPaid) ||
(fastestResult.Months != 0 && cheapestResult.TotalPaid < loan.TotalPaid && cheapestResult.Months < loan.Months) {
loan.Months = 9999
loan.TotalPaid = 99999999999999999999999 //who is this rich? send me some money lol
break
}
//reset the monthly extra payment counter
canPayMonth := canPayExtra
// fmt.Printf("Balances after %v month(s): ", loan.Months)
// fmt.Println(balances)
for _, l := range loan.Order {
if balances[l] == 0 {
canPayMonth += loans.Loans[l].Min //Rollover method
//Complete loan
continue
}
//Make the minimum payment
balances[l] -= loans.Loans[l].Min
loan.TotalPaid += loans.Loans[l].Min
// check if balance is overpaid
if balances[l] < 0 {
//add this to canpay extra
overpaid := (balances[l] * -1)
canPayMonth += overpaid
loan.TotalPaid -= overpaid
balances[l] = 0
}
}
// fmt.Printf("Balances after first payment: ")
// fmt.Println(balances)
//Pay each loan extra in order until we are out of money
for canPayMonth != 0 {
//Lets quickly see if they are all zero to break out
extraCanDoMore := false
for _, bal := range balances {
if bal != 0 {
extraCanDoMore = true //lets keep paying extra then
break
}
}
if extraCanDoMore == false {
loan.TotalPaid -= canPayMonth //lets not count that against our numbers
break
}
for _, l := range loan.Order {
if balances[l] == 0 {
//Complete loan
continue
}
//Pay whatever extra we can
balances[l] -= canPayMonth
loan.TotalPaid += canPayMonth
canPayMonth = 0
// check if balance is overpaid
if balances[l] < 0 {
//readd this to canpay
overpaid := (balances[l] * -1)
canPayMonth += overpaid
loan.TotalPaid -= overpaid
balances[l] = 0
}
}
}
// fmt.Printf("Balances after extra: ")
// fmt.Println(balances)
//Calculate interest for each loan
for _, l := range loan.Order {
if balances[l] == 0 {
//Complete loan
continue
}
interest := balances[l] * loans.Loans[l].Apy / 12
interest = float32(math.RoundToEven(float64(interest)*100) / 100) // Bank round?
balances[l] += interest
}
// fmt.Printf("Balances after interest: ")
// fmt.Println(balances)
//If all balances are empty, we are done
var totalBalances float32
for _, bal := range balances {
totalBalances += bal
}
if totalBalances == 0 {
break // We are done!!
}
//Impossible payment plan, 50 years +
if loan.Months >= 600 {
//Need to send something to prevent deadlock
loan.Months = 9999
loan.TotalPaid = 99999999999999999999999
break
}
}
jobResults <- loan
}