moneyfunx 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.circleci/config.yml +24 -0
- package/.eslintignore +1 -0
- package/.eslintrc.json +35 -0
- package/.github/workflows/release-package.yml +23 -0
- package/LICENSE +7 -0
- package/README.md +9 -0
- package/package.json +21 -0
- package/src/lib/loan.js +122 -0
- package/src/lib/payments.js +107 -0
- package/src/lib/sorting.js +26 -0
- package/tests/loan.test.js +35 -0
- package/tests/payments.test.js +37 -0
- package/tests/sorting.test.js +42 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# This config is equivalent to both the ".circleci/extended/orb-free.yml" and the base ".circleci/config.yml"
|
|
2
|
+
version: 2.1
|
|
3
|
+
|
|
4
|
+
# Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects.
|
|
5
|
+
# See: https://circleci.com/docs/2.0/orb-intro/
|
|
6
|
+
orbs:
|
|
7
|
+
node: circleci/node@4.7
|
|
8
|
+
|
|
9
|
+
# Invoke jobs via workflows
|
|
10
|
+
# See: https://circleci.com/docs/2.0/configuration-reference/#workflows
|
|
11
|
+
workflows:
|
|
12
|
+
test_lib: # This is the name of the workflow, feel free to change it to better match your workflow.
|
|
13
|
+
# Inside the workflow, you define the jobs you want to run.
|
|
14
|
+
jobs:
|
|
15
|
+
- node/test:
|
|
16
|
+
# This is the node version to use for the `cimg/node` tag
|
|
17
|
+
# Relevant tags can be found on the CircleCI Developer Hub
|
|
18
|
+
# https://circleci.com/developer/images/image/cimg/node
|
|
19
|
+
version: "17.5"
|
|
20
|
+
# If you are using yarn, change the line below from "npm" to "yarn"
|
|
21
|
+
pkg-manager: npm
|
|
22
|
+
filters:
|
|
23
|
+
tags:
|
|
24
|
+
only: /d+/.\d+/.\d+/
|
package/.eslintignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
**/scratch.js
|
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"env": {
|
|
3
|
+
"browser": true,
|
|
4
|
+
"es2021": true
|
|
5
|
+
},
|
|
6
|
+
"extends": [
|
|
7
|
+
"eslint:recommended",
|
|
8
|
+
"plugin:vue/essential"
|
|
9
|
+
],
|
|
10
|
+
"parserOptions": {
|
|
11
|
+
"ecmaVersion": "latest",
|
|
12
|
+
"sourceType": "module"
|
|
13
|
+
},
|
|
14
|
+
"plugins": [
|
|
15
|
+
"vue"
|
|
16
|
+
],
|
|
17
|
+
"rules": {
|
|
18
|
+
"indent": [
|
|
19
|
+
"error",
|
|
20
|
+
4
|
|
21
|
+
],
|
|
22
|
+
"linebreak-style": [
|
|
23
|
+
"error",
|
|
24
|
+
"unix"
|
|
25
|
+
],
|
|
26
|
+
"quotes": [
|
|
27
|
+
"error",
|
|
28
|
+
"double"
|
|
29
|
+
],
|
|
30
|
+
"semi": [
|
|
31
|
+
"error",
|
|
32
|
+
"always"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Node.js Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [created]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
|
|
9
|
+
publish-gpr:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
packages: write
|
|
13
|
+
contents: read
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v3
|
|
16
|
+
- uses: actions/setup-node@v3
|
|
17
|
+
with:
|
|
18
|
+
node-version: 17
|
|
19
|
+
registry-url: https://npm.pkg.github.com/
|
|
20
|
+
- run: npm ci
|
|
21
|
+
- run: npm publish
|
|
22
|
+
env:
|
|
23
|
+
NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2022 Kyle Pekosh
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# MoneyFunx
|
|
2
|
+
[](https://circleci.com/gh/Kylep342/moneyfunx/tree/main)
|
|
3
|
+
|
|
4
|
+
MoneyFunx is a small library of functions for financial computations, with a focus on personal finance
|
|
5
|
+
|
|
6
|
+
## TODO
|
|
7
|
+
- Fix interest calculation/accrual for the `lifetimeInterest` attribute from the `payLoans` function
|
|
8
|
+
- Add tests
|
|
9
|
+
- Figure out how imports/exports work in JavaScript
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "moneyfunx",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.2",
|
|
5
|
+
"description": "MoneyFunx is a small library of functions for financial computations, with a focus on personal finance",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
9
|
+
"lint": "eslint"
|
|
10
|
+
},
|
|
11
|
+
"author": "Kyle Pekosh",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"eslint": "^8.11.0",
|
|
15
|
+
"eslint-plugin-vue": "^8.5.0",
|
|
16
|
+
"jest": "^27.5.1"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"@Kylep342:registry": "https://npm.pkg.github.com"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/lib/loan.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
*****************
|
|
4
|
+
*** MoneyFunx ***
|
|
5
|
+
*****************
|
|
6
|
+
|
|
7
|
+
mek it funx up
|
|
8
|
+
|
|
9
|
+
This library contains functions used to in personal financial analysis
|
|
10
|
+
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export function calculateMinPayment (principal, periodicRate, periods) {
|
|
14
|
+
return periodicRate > 0 ?
|
|
15
|
+
principal * (
|
|
16
|
+
(
|
|
17
|
+
periodicRate * (1 + periodicRate) ** periods
|
|
18
|
+
) / (
|
|
19
|
+
(1 + periodicRate) ** periods - 1
|
|
20
|
+
)
|
|
21
|
+
) :
|
|
22
|
+
principal / periods;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function principalRemaining (principal, payment, periodicRate, periods) {
|
|
26
|
+
return Math.max(
|
|
27
|
+
(principal * (1 + periodicRate) ** periods) - (
|
|
28
|
+
payment * (
|
|
29
|
+
((1 + periodicRate) ** periods - 1) / (periodicRate)
|
|
30
|
+
)
|
|
31
|
+
),
|
|
32
|
+
0
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function numPaymentsToZero (principal, payment, periodicRate) {
|
|
37
|
+
return Math.ceil(
|
|
38
|
+
Math.log(
|
|
39
|
+
(payment / (payment - principal * periodicRate))
|
|
40
|
+
) / Math.log(periodicRate + 1)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//
|
|
45
|
+
export class Loan {
|
|
46
|
+
constructor (principal, annualRate, periodsPerYear, termInYears) {
|
|
47
|
+
this.id = String(Math.floor(Math.random() * Date.now()));
|
|
48
|
+
this.principal = principal;
|
|
49
|
+
this.annualRate = annualRate;
|
|
50
|
+
this.periodsPerYear = periodsPerYear;
|
|
51
|
+
this.termInYears = termInYears;
|
|
52
|
+
this.periodicRate = this.annualRate / this.periodsPerYear;
|
|
53
|
+
this.periods = this.periodsPerYear * this.termInYears;
|
|
54
|
+
this.minPayment = this.calculateMinPayment();
|
|
55
|
+
this.totalInterest = (this.minPayment * (this.periods)) - this.principal;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
validatePayment(payment=this.minPayment) {
|
|
59
|
+
if (payment < this.minPayment) {
|
|
60
|
+
throw `payment of ${payment} cannot be less than ${this.minPayment}`;
|
|
61
|
+
} else {
|
|
62
|
+
return payment;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
calculateMinPayment() {
|
|
67
|
+
return calculateMinPayment(this.principal, this.periodicRate, this.periods);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
accrueInterest(balance=this.principal) {
|
|
71
|
+
// TODO: figure out if this is valid
|
|
72
|
+
// UPDATE: 24-3-2022 this is valid AF
|
|
73
|
+
return balance * this.periodicRate;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
numPaymentsToZero(payment=this.minPayment, balance=this.principal) {
|
|
77
|
+
payment = this.validatePayment(payment);
|
|
78
|
+
return numPaymentsToZero(
|
|
79
|
+
balance,
|
|
80
|
+
payment,
|
|
81
|
+
this.periodicRate
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
principalRemaining(periods, payment=this.minPayment, balance=this.principal) {
|
|
86
|
+
payment = this.validatePayment(payment);
|
|
87
|
+
return periods < this.numPaymentsToZero(payment, balance) ?
|
|
88
|
+
principalRemaining(
|
|
89
|
+
balance,
|
|
90
|
+
payment,
|
|
91
|
+
this.periodicRate,
|
|
92
|
+
periods
|
|
93
|
+
) :
|
|
94
|
+
0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interestPaid(periods, payment=this.minPayment, balance=this.principal) {
|
|
98
|
+
// TODO: Fix this
|
|
99
|
+
// 18-3-2022: Need to compute interest for the final payment and add it to the ternary
|
|
100
|
+
payment = this.validatePayment(payment);
|
|
101
|
+
return periods < this.numPaymentsToZero(payment, balance) ?
|
|
102
|
+
(payment * periods) - (balance - this.principalRemaining(periods, payment, balance)) :
|
|
103
|
+
Math.max(
|
|
104
|
+
payment * (this.numPaymentsToZero(payment, balance) - 1) - (
|
|
105
|
+
balance - this.principalRemaining(
|
|
106
|
+
this.numPaymentsToZero(payment) - 1,
|
|
107
|
+
payment,
|
|
108
|
+
balance
|
|
109
|
+
)
|
|
110
|
+
) + (
|
|
111
|
+
this.accrueInterest(
|
|
112
|
+
this.principalRemaining(
|
|
113
|
+
this.numPaymentsToZero(payment) - 1,
|
|
114
|
+
payment,
|
|
115
|
+
balance
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
),
|
|
119
|
+
0
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
*/
|
|
4
|
+
import * as loanLib from "./loan.js";
|
|
5
|
+
|
|
6
|
+
//
|
|
7
|
+
export function determineExtraPayment (loans, payment) {
|
|
8
|
+
const totalMinPayment = loans.reduce(
|
|
9
|
+
(previousValue, currentValue) => previousValue + currentValue.minPayment,
|
|
10
|
+
0
|
|
11
|
+
);
|
|
12
|
+
if (totalMinPayment > payment) {
|
|
13
|
+
throw `Payment amount of ${payment} must be greater than ${totalMinPayment}`;
|
|
14
|
+
}
|
|
15
|
+
return payment - totalMinPayment;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
//
|
|
19
|
+
export function amortizePayments (loan, payment, numPayments, startPeriod) {
|
|
20
|
+
payment = loan.validatePayment(payment);
|
|
21
|
+
let amortizationSchedule = [];
|
|
22
|
+
for (
|
|
23
|
+
let period=0;
|
|
24
|
+
period<numPayments;
|
|
25
|
+
period++
|
|
26
|
+
) {
|
|
27
|
+
let interestThisPeriod = loan.accrueInterest(
|
|
28
|
+
loan.principalRemaining(
|
|
29
|
+
period,
|
|
30
|
+
payment,
|
|
31
|
+
loan.principalRemaining(startPeriod)
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
let principalThisPeriod = Math.min(
|
|
35
|
+
payment - interestThisPeriod,
|
|
36
|
+
loan.principalRemaining(
|
|
37
|
+
period,
|
|
38
|
+
payment,
|
|
39
|
+
loan.principalRemaining(startPeriod)
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
amortizationSchedule.push(
|
|
43
|
+
{
|
|
44
|
+
period: startPeriod + period + 1,
|
|
45
|
+
principal: principalThisPeriod,
|
|
46
|
+
interest: interestThisPeriod,
|
|
47
|
+
principalRemaining: loan.principalRemaining(
|
|
48
|
+
period + 1,
|
|
49
|
+
payment,
|
|
50
|
+
loan.principalRemaining(startPeriod)
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return amortizationSchedule;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//
|
|
59
|
+
export function payLoans (loans, payment) {
|
|
60
|
+
let loanInterestTotals = {};
|
|
61
|
+
loans.map(
|
|
62
|
+
(loan) => {
|
|
63
|
+
loanInterestTotals[loan.id] = {lifetimeInterest: 0, amortizationSchedule: []};
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
let periodsElapsed = 0;
|
|
68
|
+
let paidLoans = 0;
|
|
69
|
+
|
|
70
|
+
while (paidLoans < loans.length) {
|
|
71
|
+
let extraPaymentAmount = determineExtraPayment(loans.slice(paidLoans), payment);
|
|
72
|
+
let firstLoan = loans.slice(paidLoans)[0];
|
|
73
|
+
let firstLoanPayment = firstLoan.minPayment + extraPaymentAmount;
|
|
74
|
+
let periodsToPay = loanLib.numPaymentsToZero(
|
|
75
|
+
firstLoan.principalRemaining(
|
|
76
|
+
periodsElapsed,
|
|
77
|
+
firstLoan.minPayment
|
|
78
|
+
),
|
|
79
|
+
firstLoanPayment,
|
|
80
|
+
firstLoan.periodicRate
|
|
81
|
+
);
|
|
82
|
+
let firstLoanInterestPaid = firstLoan.interestPaid(
|
|
83
|
+
periodsToPay,
|
|
84
|
+
firstLoanPayment,
|
|
85
|
+
firstLoan.principalRemaining(periodsElapsed)
|
|
86
|
+
);
|
|
87
|
+
loanInterestTotals[firstLoan.id].lifetimeInterest += firstLoanInterestPaid;
|
|
88
|
+
loanInterestTotals[firstLoan.id].amortizationSchedule = [
|
|
89
|
+
...loanInterestTotals[firstLoan.id].amortizationSchedule,
|
|
90
|
+
...amortizePayments(firstLoan, firstLoanPayment, periodsToPay, periodsElapsed)
|
|
91
|
+
];
|
|
92
|
+
paidLoans += 1;
|
|
93
|
+
loans.slice(paidLoans).map((loan) => {
|
|
94
|
+
loanInterestTotals[loan.id].lifetimeInterest += loan.interestPaid(
|
|
95
|
+
periodsToPay,
|
|
96
|
+
loan.minPayment,
|
|
97
|
+
loan.principalRemaining(periodsElapsed)
|
|
98
|
+
);
|
|
99
|
+
loanInterestTotals[loan.id].amortizationSchedule = [
|
|
100
|
+
...loanInterestTotals[loan.id].amortizationSchedule,
|
|
101
|
+
...amortizePayments(loan, loan.minPayment, periodsToPay, periodsElapsed)
|
|
102
|
+
];
|
|
103
|
+
});
|
|
104
|
+
periodsElapsed += periodsToPay;
|
|
105
|
+
}
|
|
106
|
+
return loanInterestTotals;
|
|
107
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
NOTE: When sorting by avalanche, sort by snowball first for optimal interest minimization
|
|
4
|
+
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
Sorts loans descending by interest rate
|
|
9
|
+
*/
|
|
10
|
+
export function snowball(loan1, loan2) {
|
|
11
|
+
return loan2.annualRate - loan1.annualRate;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
Sorts loans ascending by principal
|
|
16
|
+
*/
|
|
17
|
+
export function avalanche(loan1, loan2) {
|
|
18
|
+
return loan1.principal - loan2.principal;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
Sorts an array of loans using the provided sortFunc
|
|
23
|
+
*/
|
|
24
|
+
export function sortLoans(loans, sortFunc) {
|
|
25
|
+
return loans.sort(sortFunc);
|
|
26
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { test, expect } from "@jest/globals";
|
|
2
|
+
import * as loan from "../src/lib/loan.js";
|
|
3
|
+
|
|
4
|
+
test(
|
|
5
|
+
"Loan with principal=7500, interest=0.068, periods/year=12, years=10 has proper attributes",
|
|
6
|
+
() => {
|
|
7
|
+
const loan1 = new loan.Loan(7500, 0.068, 12, 10);
|
|
8
|
+
|
|
9
|
+
expect(loan1.periodicRate).toBe(0.005666666666666667);
|
|
10
|
+
expect(loan1.periods).toBe(120);
|
|
11
|
+
expect(loan1.minPayment).toBe(86.31024763658397);
|
|
12
|
+
expect(loan1.totalInterest).toBe(2857.2297163900766);
|
|
13
|
+
|
|
14
|
+
expect(loan1.numPaymentsToZero()).toBe(120);
|
|
15
|
+
expect(loan1.numPaymentsToZero(300)).toBe(28);
|
|
16
|
+
expect(() => {loan1.numPaymentsToZero(30);}).toThrow("payment of 30 cannot be less than 86.31024763658397");
|
|
17
|
+
|
|
18
|
+
expect(loan1.validatePayment(100)).toBe(100);
|
|
19
|
+
expect(loan1.validatePayment()).toBe(86.31024763658397);
|
|
20
|
+
expect(() => {loan1.validatePayment(20);}).toThrow("payment of 20 cannot be less than 86.31024763658397");
|
|
21
|
+
expect(() => {loan1.validatePayment(-160);}).toThrow("payment of -160 cannot be less than 86.31024763658397");
|
|
22
|
+
|
|
23
|
+
expect(loan1.principalRemaining(33)).toBe(5915.168870573016);
|
|
24
|
+
expect(loan1.principalRemaining(0)).toBe(7500);
|
|
25
|
+
expect(loan1.principalRemaining(40, 500)).toBe(0);
|
|
26
|
+
expect(loan1.principalRemaining(3, 200, 3000)).toBe(2447.8831236666597);
|
|
27
|
+
expect(() => {loan1.principalRemaining(21, 40, 9000);}).toThrow("payment of 40 cannot be less than 86.31024763658397");
|
|
28
|
+
|
|
29
|
+
expect(loan1.interestPaid(0)).toBe(0);
|
|
30
|
+
expect(loan1.interestPaid(22)).toBe(875.4263974363766);
|
|
31
|
+
expect(loan1.interestPaid(50, 300)).toBe(610.3609925683494);
|
|
32
|
+
expect(loan1.interestPaid(120)).toBe(loan1.totalInterest);
|
|
33
|
+
expect(() => {loan1.interestPaid(30, 10, 20000);}).toThrow("payment of 10 cannot be less than 86.31024763658397");
|
|
34
|
+
}
|
|
35
|
+
);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { expect, test } from "@jest/globals";
|
|
2
|
+
import * as loan from "../src/lib/loan.js";
|
|
3
|
+
import * as sorting from "../src/lib/sorting.js";
|
|
4
|
+
import * as payments from "../src/lib/payments.js";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
test(
|
|
8
|
+
"Payments are good",
|
|
9
|
+
() => {
|
|
10
|
+
const loan1 = new loan.Loan(7500, .068, 12, 10);
|
|
11
|
+
const loan2 = new loan.Loan(7500, .0368, 12, 10);
|
|
12
|
+
const loan3 = new loan.Loan(4500, .0429, 12, 10);
|
|
13
|
+
|
|
14
|
+
const loans = sorting.sortLoans(
|
|
15
|
+
[loan2, loan3, loan1],
|
|
16
|
+
sorting.snowball
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const loan2AmortizationSchedule = payments.amortizePayments(loan2, loan2.minPayment, 120, 0);
|
|
20
|
+
|
|
21
|
+
expect(loan2AmortizationSchedule.length).toBe(120);
|
|
22
|
+
expect(loan2AmortizationSchedule[3].period).toBe(4);
|
|
23
|
+
expect(loan2AmortizationSchedule[3].principal).toBe(52.27646756701894);
|
|
24
|
+
expect(loan2AmortizationSchedule[3].interest).toBe(22.5219912775614);
|
|
25
|
+
expect(loan2AmortizationSchedule[3].principalRemaining).toBe(7291.851122942134);
|
|
26
|
+
|
|
27
|
+
expect(payments.determineExtraPayment(loans, 400)).toBe(192.70819668183697);
|
|
28
|
+
expect(() => {payments.determineExtraPayment(loans, 0);}).toThrow("Payment amount of 0 must be greater than 207.29180331816303");
|
|
29
|
+
|
|
30
|
+
const loanPaymentTotals1 = payments.payLoans(loans, 400);
|
|
31
|
+
|
|
32
|
+
expect(Object.keys(loanPaymentTotals1).length).toBe(3);
|
|
33
|
+
expect(loanPaymentTotals1[loan1.id].lifetimeInterest).toBe(659.9318259100721);
|
|
34
|
+
expect(loanPaymentTotals1[loan2.id].lifetimeInterest).toBe(841.5352714723776);
|
|
35
|
+
expect(loanPaymentTotals1[loan3.id].lifetimeInterest).toBe(462.70985781957734);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { expect, test } from "@jest/globals";
|
|
2
|
+
import * as loan from "../src/lib/loan.js";
|
|
3
|
+
import * as sorting from "../src/lib/sorting.js";
|
|
4
|
+
|
|
5
|
+
test(
|
|
6
|
+
"Loans sort well",
|
|
7
|
+
() => {
|
|
8
|
+
const loan1 = new loan.Loan(7500, .068, 12, 10);
|
|
9
|
+
const loan2 = new loan.Loan(7500, .0368, 12, 10);
|
|
10
|
+
const loan3 = new loan.Loan(4500, .0429, 12, 10);
|
|
11
|
+
|
|
12
|
+
const loans = [loan2, loan3, loan1];
|
|
13
|
+
|
|
14
|
+
expect(sorting.snowball(loan1, loan2)).toBe(-0.031200000000000006);
|
|
15
|
+
expect(sorting.snowball(loan3, loan2)).toBe(-0.006100000000000001);
|
|
16
|
+
|
|
17
|
+
expect(sorting.avalanche(loan1, loan3)).toBe(3000);
|
|
18
|
+
expect(sorting.avalanche(loan1, loan2)).toBe(0);
|
|
19
|
+
|
|
20
|
+
expect(
|
|
21
|
+
sorting.sortLoans(
|
|
22
|
+
loans,
|
|
23
|
+
sorting.snowball
|
|
24
|
+
)
|
|
25
|
+
).toStrictEqual([loan1, loan3, loan2]);
|
|
26
|
+
expect(
|
|
27
|
+
sorting.sortLoans(
|
|
28
|
+
loans,
|
|
29
|
+
sorting.avalanche
|
|
30
|
+
)
|
|
31
|
+
).toStrictEqual([loan3, loan1, loan2]);
|
|
32
|
+
expect(
|
|
33
|
+
sorting.sortLoans(
|
|
34
|
+
sorting.sortLoans(
|
|
35
|
+
loans,
|
|
36
|
+
sorting.snowball
|
|
37
|
+
),
|
|
38
|
+
sorting.avalanche
|
|
39
|
+
)
|
|
40
|
+
).toStrictEqual([loan3, loan1, loan2]);
|
|
41
|
+
}
|
|
42
|
+
);
|