Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
  • Loading branch information
ldrrp committed Aug 30, 2019
1 parent b10faca commit 8e2010e
Show file tree
Hide file tree
Showing 3 changed files with 336 additions and 0 deletions.
252 changes: 252 additions & 0 deletions loanPay.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
// In this example we'll look at how to implement
// a _worker pool_ using goroutines and channels.

package main

import (
"fmt"
"io/ioutil"
"log"
"math"
"os"
"sync"

"gopkg.in/yaml.v2"
)

type loans 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"`
FastestMethod loanResult
CheapestMethod loanResult
}

type loanResult struct {
Order []int
Months int16
TotalPaid float32
}

var loanResults loans
var canPay float32
var jobs chan loanResult

func main() {

var waitgroup sync.WaitGroup
jobs = make(chan loanResult, 100)

//Load the loans.yaml file
loadFile()

// This starts up 8 workers, initially blocked
// because there are no jobs yet.
for w := 1; w <= 8; w++ {
waitgroup.Add(1)
go worker(w, &waitgroup)
}

// 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(loanResults.Loans)))
close(jobs)

// Finally we collect all the results of the work.
// This also ensures that the worker goroutines have
// finished.
waitgroup.Wait()

fmt.Println(loanResults)
}

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), &loanResults)
if err2 != nil {
log.Fatal(err2)
}
}

func worker(id int, waitgroup *sync.WaitGroup) {
for j := range jobs {
//fmt.Printf("Started job %v\n", j)
processLoanOrder(j)
}

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) {

var rc func([]int, int)
rc = func(a []int, k int) {
if k == len(a) {
loanorder := loanResult{}
loanorder.Order = append([]int{}, a...) // Important to keep order
jobs <- loanorder
} 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)
}

func processLoanOrder(loan loanResult) {
var balances []float32
canPayExtra := loanResults.Extra

//Insert balances
for _, l := range loan.Order {
balances = append(balances, loanResults.Loans[l].Balance)
}

//fmt.Print("Balances: ")
//fmt.Println(balances)

for {
//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 += loanResults.Loans[l].Min //Rollover method
//Complete loan
continue
}

//Make the minimum payment
balances[l] -= loanResults.Loans[l].Min
loan.TotalPaid += loanResults.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] * loanResults.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)

// One month has elapsed
loan.Months++

//If all balances are empty, we are done
var totalBalances float32
for _, bal := range balances {
totalBalances += bal
}
//fmt.Println(totalBalances)
if totalBalances == 0 {
break // We are done!!
}

//Impossible payment plan, 50 years +
if loan.Months >= 600 {
return
}
}

if loanResults.FastestMethod.Months == 0 {
loanResults.FastestMethod = loan
loanResults.CheapestMethod = loan
}

//Was it the fastest then cheapest
if loanResults.FastestMethod.Months >= loan.Months && loanResults.FastestMethod.TotalPaid > loan.TotalPaid {
loanResults.FastestMethod = loan
}

//was it cheapest then fastest
if loanResults.CheapestMethod.TotalPaid >= loan.TotalPaid && loanResults.CheapestMethod.Months > loan.Months {
loanResults.CheapestMethod = loan
}
}
38 changes: 38 additions & 0 deletions loans.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
extra: 10
loans:
-
apy: 0.01
balance: 1000.00
min: 400.01
name: Furnace
-
apy: 0.01
balance: 595.19
min: 198
name: Lowes
-
apy: 0.01
balance: 1113.74
min: 100
name: Camera
-
apy: 0
balance: 3456.60
min: 101.85
name: Windows
-
apy: 0.0696
balance: 4175.29
min: 68.64
name: Nelnet
-
apy: 0.0559
balance: 10941.08
min: 274.27
name: Elantra
-
apy: 0.03875
balance: 121874.83
min: 400.52 #subtract escrow
name: Mortage
46 changes: 46 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

# Loan Pay v1

The purpose of this project is to calculate the fastest and cheapest way to pay off your loans. Currently it is able to perform up to 11-14 loans withouth getting into insane times. This uses the premise of the [stacking method and snowball methods](https://www.thebalance.com/debt-snowball-vs-debt-stacking-453633). Inspiration came from this [stackoverflow question](https://money.stackexchange.com/questions/85187/algorithm-for-multiple-debt-payoff-to-minimize-time-in-debt).

#### Upcomming ideas:

v1.2 - Integrate the idea of balance transfers

v2 - Branch off main, Use remote workers on network to handoff calculations in bulk to spread the load and cut time.

v3 - Use golangjs package to port it over from same code base to a pwa github hosted site

#### How to use

Edit the loans.yaml file that has some examples and run it. The output will be the index array order of how to pay them off followed by the time in months and amount paid.


#### Benchmarks

Loans | Permutations | Time To calculate
-- | -- | --
1 | 1 | 5ms
2 | 2 | 5ms
3 | 6 | 5ms
4 | 24 | 5ms
5 | 120 | 5ms
6 | 720 | 5ms
7 | 5,040 | 5ms
8 | 40,320 | 5ms
9 | 362,880 | 5ms
10 | 3,628,800 | 7s
11 | 39,916,800 | 75s
12 | 479,001,600 | 15m
13 | 6,227,020,800 | 4h
14 | 87,178,291,200 | 46h
15 | 1,307,674,368,000 | 28d
16 | 20,922,789,888,000 | 1.25y
17 | 355,687,428,096,000 | 21y
18 | 6,402,373,705,728,000 | 382y
19 | 121,645,100,408,832,000 | 7248y
20 | 2,432,902,008,176,640,000 | 144952y

Time to calculate was run on a machine with a 5147 [benchmark score](https://www.cpubenchmark.net/). Your times may vary. Average memory usage was 105MB of ram throughout the process.


0 comments on commit 8e2010e

Please sign in to comment.