@travishorn/financejs 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright 2020 kgkars
4
+ Copyright 2026 Travis Horn
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
7
+ this software and associated documentation files (the “Software”), to deal in
8
+ the Software without restriction, including without limitation the rights to
9
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
10
+ the Software, and to permit persons to whom the Software is furnished to do so,
11
+ subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # financejs
2
+
3
+ Excel-style time value of money and cash-flow formulas. This is a modern rewrite
4
+ of [tvm-financejs](https://github.com/kgkars/tvm-financejs/) by
5
+ [kgkars](https://github.com/kgkars).
6
+
7
+ ## Why this rewrite
8
+
9
+ This project keeps the formula behavior and conventions from the original
10
+ library, but modernizes the implementation and tooling:
11
+
12
+ - Native ESM
13
+ - Named function exports (no class wrapper)
14
+ - Native error handling
15
+ - Strict type-checking
16
+ - JSDoc documentation
17
+ - Vitest-based test suite
18
+ - 100% test coverage
19
+ - ESLint + Prettier setup
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install @travishorn/financejs
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```js
30
+ import { pmt, rate, irr } from "@travishorn/financejs";
31
+
32
+ const payment = pmt(0.0525, 5, -10000);
33
+ // 2325.7331680465
34
+
35
+ const periodicRate = rate(60, 500, -25000);
36
+ // 0.00618341316125388
37
+
38
+ const internalRate = irr([-1500, 500, 500, 500, 500]);
39
+ // 0.125898324962364
40
+ ```
41
+
42
+ ## Excel-style conventions
43
+
44
+ - Outputs are not rounded automatically.
45
+ - `pv` is typically negative for loans/investments (same convention as Excel).
46
+ - `type` means payment timing:
47
+ - `0` = end of period (arrears)
48
+ - `1` = beginning of period (advance)
49
+ - `rate` must match period frequency (e.g., annual rate divided by 12 for
50
+ monthly periods).
51
+
52
+ ## API
53
+
54
+ ### Input Variables
55
+
56
+ | Variable | Description |
57
+ | -------- | ------------------------------------------------------------------------------------- |
58
+ | `pv` | Present value |
59
+ | `fv` | Future value |
60
+ | `pmt` | Payment |
61
+ | `nper` | Total number of periods |
62
+ | `per` | A specific period |
63
+ | `rate` | Rate for the period(s) |
64
+ | `type` | When payments are due. `0` = end of period/arrears. `1` = beginning of period/advance |
65
+ | `guess` | A guess at the rate |
66
+ | `values` | A set of periodic cash flows |
67
+
68
+ ### `pv(rate, nper, pmt, fv = 0, type = 0)`
69
+
70
+ Returns the present value of an investment, or the total amount that a series of
71
+ future payments is worth now.
72
+
73
+ ### `fv(rate, nper, pmt, pv, type = 0)`
74
+
75
+ Returns the future value of an investment based on periodic, equal, payments and
76
+ a constant interest rate.
77
+
78
+ ### `pmt(rate, nper, pv, fv = 0, type = 0)`
79
+
80
+ Calculates the payment for a loan based on a constant stream of equal payments
81
+ and a constant interest rate.
82
+
83
+ ### `nper(rate, pmt, pv, fv = 0, type = 0)`
84
+
85
+ Number of periods.
86
+
87
+ ### `ipmt(rate, per, nper, pv, fv = 0, type = 0)`
88
+
89
+ Returns the calculated interest portion of a payment for a specific period based
90
+ on a constant stream of equal payments and a constant interest rate.
91
+
92
+ ### `ppmt(rate, per, nper, pv, fv = 0, type = 0)`
93
+
94
+ Returns the calculated principal portion of a payment for a specific period
95
+ based on a constant stream of equal payments and a constant interest rate.
96
+
97
+ ### `rate(nper, pmt, pv, fv = 0, type = 0, guess = 0.1)`
98
+
99
+ Returns the interest rate per period for a loan or investment (iterative solve).
100
+
101
+ ### `npv(rate, ...values)`
102
+
103
+ Returns the net present value of an investment based on a constant rate of
104
+ return and a series of future payments/investments (as negative values) and
105
+ income/return (as positive values).
106
+
107
+ ### `irr(values, guess = 0.1)`
108
+
109
+ Returns the internal rate of return for a series of cash flows.
110
+
111
+ A couple of items to note about this formula:
112
+
113
+ - The variable values must be input as an array.
114
+ - There must be at least one negative and one positive value as part of the cash flow.
115
+ - Cash flows are assumed to be due in the same order they are arranged in the Array.
116
+
117
+ Example usage:
118
+
119
+ ```javascript
120
+ returnIRR() {
121
+ const values = [-1500, 500, 500, 500, 500];
122
+ return Math.round(irr(values) * 100 ) / 100 * 100;
123
+ }
124
+ // returns 12.59
125
+ ```
126
+
127
+ ## Error behavior
128
+
129
+ All functions will either return a number, or throw `RangeError` for invalid
130
+ inputs or non-convergent iterative solves.
131
+
132
+ ## Development
133
+
134
+ Clone the repository:
135
+
136
+ ```bash
137
+ git clone https://github.com/travishorn/financejs
138
+ ```
139
+
140
+ Change into the directory:
141
+
142
+ ```bash
143
+ cd financejs
144
+ ```
145
+
146
+ Run tests:
147
+
148
+ ```bash
149
+ npm test
150
+ ```
151
+
152
+ Lint with ESLint:
153
+
154
+ ```bash
155
+ npm run lint
156
+ ```
157
+
158
+ Check types:
159
+
160
+ ```bash
161
+ npm run lint:types
162
+ ```
163
+
164
+ Format with Prettier:
165
+
166
+ ```bash
167
+ npm run format
168
+ ```
169
+
170
+ ## Notes on compatibility
171
+
172
+ This rewrite is intended to match Excel-style formulas closely (tests validate
173
+ to 8 decimal places), while using a modern JavaScript module API.
174
+
175
+ ## License
176
+
177
+ The MIT License
178
+
179
+ Copyright 2020 kgkars
180
+ Copyright 2026 Travis Horn
181
+
182
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
183
+ this software and associated documentation files (the “Software”), to deal in
184
+ the Software without restriction, including without limitation the rights to
185
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
186
+ the Software, and to permit persons to whom the Software is furnished to do so,
187
+ subject to the following conditions:
188
+
189
+ The above copyright notice and this permission notice shall be included in all
190
+ copies or substantial portions of the Software.
191
+
192
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
193
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
194
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
195
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
196
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
197
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@travishorn/financejs",
3
+ "version": "1.0.0",
4
+ "description": "Modern JavaScript time value of money and cash-flow financial formulas with Excel-style behavior.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/travishorn/financejs.git"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/travishorn/financejs/issues"
11
+ },
12
+ "homepage": "https://github.com/travishorn/financejs#readme",
13
+ "exports": "./src/index.js",
14
+ "files": [
15
+ "src",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "test": "vitest --run",
24
+ "lint": "eslint .",
25
+ "lint:types": "tsc --noEmit",
26
+ "format": "prettier --write ."
27
+ },
28
+ "keywords": [
29
+ "finance",
30
+ "financial",
31
+ "tvm",
32
+ "time-value-of-money",
33
+ "excel",
34
+ "npv",
35
+ "irr",
36
+ "rate",
37
+ "loan",
38
+ "amortization"
39
+ ],
40
+ "author": "Travis Horn <travis@travishorn.com> (https://travishorn.com/)",
41
+ "license": "MIT",
42
+ "type": "module",
43
+ "devDependencies": {
44
+ "@eslint/js": "^10.0.1",
45
+ "@vitest/coverage-v8": "^4.0.18",
46
+ "eslint": "^10.0.2",
47
+ "eslint-config-prettier": "^10.1.8",
48
+ "globals": "^17.4.0",
49
+ "prettier": "^3.8.1",
50
+ "typescript": "^5.9.3",
51
+ "vitest": "^4.0.18"
52
+ }
53
+ }
package/src/fv.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Calculates the future value of an investment or loan.
3
+ *
4
+ * @param {number} rate - The interest rate per period.
5
+ * @param {number} nper - The total number of payment periods.
6
+ * @param {number} pmt - The payment made each period.
7
+ * @param {number} pv - The present value.
8
+ * @param {0|1} [type=0] - Payment timing: 0 = end of period, 1 = beginning of period.
9
+ * @returns {number} The future value.
10
+ */
11
+ export function fv(rate, nper, pmt, pv, type = 0) {
12
+ if (rate === 0) {
13
+ return -pv - pmt * nper;
14
+ } else {
15
+ const paymentTimingFactor = type !== 0 ? 1 + rate : 1;
16
+ const interestFactor = 1 + rate;
17
+ const compoundFactor = Math.pow(interestFactor, nper);
18
+
19
+ return (
20
+ -pv * compoundFactor -
21
+ (pmt / rate) * paymentTimingFactor * (compoundFactor - 1)
22
+ );
23
+ }
24
+ }
package/src/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { pv } from "./pv.js";
2
+ export { nper } from "./nper.js";
3
+ export { pmt } from "./pmt.js";
4
+ export { fv } from "./fv.js";
5
+ export { ipmt } from "./ipmt.js";
6
+ export { ppmt } from "./ppmt.js";
7
+ export { npv } from "./npv.js";
8
+ export { irr } from "./irr.js";
9
+ export { rate } from "./rate.js";
package/src/ipmt.js ADDED
@@ -0,0 +1,36 @@
1
+ import { fv } from "./fv.js";
2
+ import { pmt } from "./pmt.js";
3
+
4
+ /**
5
+ * Calculates the interest portion of a payment for a specific period.
6
+ *
7
+ * @param {number} rate - The interest rate per period.
8
+ * @param {number} per - The target period (1-based).
9
+ * @param {number} nper - The total number of payment periods.
10
+ * @param {number} pv - The present value.
11
+ * @param {number} [futureValue=0] - The future value.
12
+ * @param {0|1} [type=0] - Payment timing: 0 = end of period, 1 = beginning of period.
13
+ * @returns {number} The interest payment for the specified period.
14
+ * @throws {RangeError} When `per` is outside the valid range.
15
+ */
16
+ export function ipmt(rate, per, nper, pv, futureValue = 0, type = 0) {
17
+ if (per <= 0 || per >= nper + 1) {
18
+ throw new RangeError("Invalid period.");
19
+ }
20
+
21
+ if (type !== 0 && per === 1) {
22
+ return 0;
23
+ }
24
+
25
+ const periodOffset = type !== 0 ? 2 : 1;
26
+ const periodicPayment = pmt(rate, nper, pv, futureValue, type);
27
+ const adjustedPresentValue = type !== 0 ? pv + periodicPayment : pv;
28
+ const periodFutureValue = fv(
29
+ rate,
30
+ per - periodOffset,
31
+ periodicPayment,
32
+ adjustedPresentValue,
33
+ );
34
+
35
+ return periodFutureValue * rate;
36
+ }
package/src/irr.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Evaluates present value for an IRR iteration guess.
3
+ *
4
+ * @param {number[]} values - Cash flow values.
5
+ * @param {number} [guess=0.1] - Rate guess.
6
+ * @returns {number} Present value at the supplied guess.
7
+ */
8
+ function internalPv(values, guess = 0.1) {
9
+ let lowerBound = 0;
10
+ const upperBound = values.length - 1;
11
+ let total = 0;
12
+ const discountRate = 1 + guess;
13
+
14
+ while (lowerBound <= upperBound && values[lowerBound] === 0) {
15
+ lowerBound += 1;
16
+ }
17
+
18
+ for (let index = upperBound; index >= lowerBound; index -= 1) {
19
+ total /= discountRate;
20
+ total += values[index];
21
+ }
22
+
23
+ return total;
24
+ }
25
+
26
+ /**
27
+ * Calculates the internal rate of return for a series of cash flows.
28
+ *
29
+ * @param {number[]} values - Cash flow values where negatives are investments and positives are returns.
30
+ * @param {number} [guess=0.1] - Initial guess for the IRR iteration.
31
+ * @returns {number} The internal rate of return.
32
+ * @throws {RangeError} When inputs are invalid or the algorithm cannot converge.
33
+ */
34
+ export function irr(values, guess = 0.1) {
35
+ if (guess <= -1) {
36
+ throw new RangeError("Invalid guess.");
37
+ }
38
+
39
+ if (values.length < 1) {
40
+ throw new RangeError("Invalid values.");
41
+ }
42
+
43
+ const epsilonMax = 0.0000001;
44
+ const step = 0.00001;
45
+ const iterationMax = 39;
46
+
47
+ let maxAbsoluteValue = Math.abs(values[0]);
48
+
49
+ for (let index = 0; index < values.length; index += 1) {
50
+ const absoluteValue = Math.abs(values[index]);
51
+
52
+ if (absoluteValue > maxAbsoluteValue) {
53
+ maxAbsoluteValue = absoluteValue;
54
+ }
55
+ }
56
+
57
+ const npvEpsilon = maxAbsoluteValue * epsilonMax * 0.01;
58
+
59
+ let rate0 = guess;
60
+ let npv0 = internalPv(values, rate0);
61
+ let rate1 = npv0 > 0 ? rate0 + step : rate0 - step;
62
+
63
+ if (rate1 <= -1) {
64
+ throw new RangeError("Invalid values.");
65
+ }
66
+
67
+ let npv1 = internalPv(values, rate1);
68
+
69
+ for (let iteration = 0; iteration <= iterationMax; iteration += 1) {
70
+ if (npv1 === npv0) {
71
+ rate0 = rate1 > rate0 ? rate0 - step : rate0 + step;
72
+ npv0 = internalPv(values, rate0);
73
+
74
+ if (npv1 === npv0) {
75
+ throw new RangeError("Invalid values.");
76
+ }
77
+ }
78
+
79
+ rate0 = rate1 - ((rate1 - rate0) * npv1) / (npv1 - npv0);
80
+
81
+ if (rate0 <= -1) {
82
+ rate0 = (rate1 - 1) * 0.5;
83
+ }
84
+
85
+ npv0 = internalPv(values, rate0);
86
+
87
+ const rateDelta = Math.abs(rate0 - rate1);
88
+ const absoluteNpv = Math.abs(npv0);
89
+
90
+ if (absoluteNpv < npvEpsilon && rateDelta < epsilonMax) {
91
+ return rate0;
92
+ }
93
+
94
+ const nextNpv = npv0;
95
+ npv0 = npv1;
96
+ npv1 = nextNpv;
97
+
98
+ const nextRate = rate0;
99
+ rate0 = rate1;
100
+ rate1 = nextRate;
101
+ }
102
+
103
+ throw new RangeError("Maximum iterations exceeded while calculating IRR.");
104
+ }
package/src/nper.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Calculates the number of periods for an investment/loan.
3
+ *
4
+ * @param {number} rate - Interest rate per period.
5
+ * @param {number} pmt - Payment made each period.
6
+ * @param {number} pv - Present value.
7
+ * @param {number} [fv=0] - Future value.
8
+ * @param {0|1} [type=0] - Payment timing: 0 = end of period, 1 = beginning.
9
+ * @returns {number} Number of periods.
10
+ * @throws {RangeError} When calculation is impossible with the provided inputs.
11
+ */
12
+ export function nper(rate, pmt, pv, fv = 0, type = 0) {
13
+ if (rate === 0) {
14
+ if (pmt === 0) {
15
+ throw new RangeError("Payment cannot be 0 when rate is 0.");
16
+ }
17
+
18
+ return -(pv + fv) / pmt;
19
+ }
20
+
21
+ const paymentAdjustment = type !== 0 ? pmt * (1 + rate) : pmt;
22
+ const paymentOverRate = paymentAdjustment / rate;
23
+
24
+ let futureValueTerm = -fv + paymentOverRate;
25
+ let presentValueTerm = pv + paymentOverRate;
26
+
27
+ // Ensure values are valid for logarithms.
28
+ if (futureValueTerm < 0 && presentValueTerm < 0) {
29
+ futureValueTerm *= -1;
30
+ presentValueTerm *= -1;
31
+ } else if (futureValueTerm <= 0 || presentValueTerm <= 0) {
32
+ throw new RangeError("Cannot calculate NPER with the provided values.");
33
+ }
34
+
35
+ const growthFactor = 1 + rate;
36
+
37
+ return (
38
+ (Math.log(futureValueTerm) - Math.log(presentValueTerm)) /
39
+ Math.log(growthFactor)
40
+ );
41
+ }
package/src/npv.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Evaluates net present value across a bounded portion of a values array.
3
+ *
4
+ * @param {number} rate - Discount rate per period.
5
+ * @param {number[]} values - Cash flow values.
6
+ * @param {number} [lowerBound=0] - Start index (inclusive).
7
+ * @param {number} [upperBound=values.length - 1] - End index (inclusive).
8
+ * @returns {number} The evaluated NPV for the specified range.
9
+ */
10
+ function evalNpv(rate, values, lowerBound = 0, upperBound = values.length - 1) {
11
+ let discountFactor = 1;
12
+ let total = 0;
13
+
14
+ for (let index = lowerBound; index <= upperBound; index += 1) {
15
+ const value = values[index];
16
+ discountFactor += discountFactor * rate;
17
+ total += value / discountFactor;
18
+ }
19
+
20
+ return total;
21
+ }
22
+
23
+ /**
24
+ * Calculates the net present value of a series of cash flows.
25
+ *
26
+ * @param {number} rate - Discount rate per period.
27
+ * @param {...number} values - Cash flow values.
28
+ * @returns {number} The net present value.
29
+ * @throws {RangeError} When there are no cash flow values or rate is invalid.
30
+ */
31
+ export function npv(rate, ...values) {
32
+ if (values.length < 1) {
33
+ throw new RangeError("Invalid values.");
34
+ }
35
+
36
+ if (rate === -1) {
37
+ throw new RangeError("Invalid rate.");
38
+ }
39
+
40
+ return evalNpv(rate, values, 0, values.length - 1);
41
+ }
package/src/pmt.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Calculates the periodic payment for a loan or investment.
3
+ *
4
+ * @param {number} rate - The interest rate per period.
5
+ * @param {number} nper - The total number of payment periods.
6
+ * @param {number} pv - The present value.
7
+ * @param {number} [fv=0] - The future value, or remaining balance after the last payment. Defaults to 0.
8
+ * @param {0|1} [type=0] - Payment timing: 0 = end of period, 1 = beginning of period. Defaults to 0.
9
+ * @returns {number} The periodic payment amount.
10
+ */
11
+ export function pmt(rate, nper, pv, fv = 0, type = 0) {
12
+ if (rate === 0) {
13
+ return (-fv - pv) / nper;
14
+ } else {
15
+ const paymentTimingFactor = type !== 0 ? 1 + rate : 1;
16
+ const interestFactor = 1 + rate;
17
+ const compoundFactor = Math.pow(interestFactor, nper);
18
+
19
+ return (
20
+ ((-fv - pv * compoundFactor) /
21
+ (paymentTimingFactor * (compoundFactor - 1))) *
22
+ rate
23
+ );
24
+ }
25
+ }
package/src/ppmt.js ADDED
@@ -0,0 +1,25 @@
1
+ import { ipmt } from "./ipmt.js";
2
+ import { pmt } from "./pmt.js";
3
+
4
+ /**
5
+ * Calculates the principal portion of a payment for a specific period.
6
+ *
7
+ * @param {number} rate - The interest rate per period.
8
+ * @param {number} per - The target period (1-based).
9
+ * @param {number} nper - The total number of payment periods.
10
+ * @param {number} pv - The present value.
11
+ * @param {number} [futureValue=0] - The future value.
12
+ * @param {0|1} [type=0] - Payment timing: 0 = end of period, 1 = beginning of period.
13
+ * @returns {number} The principal payment for the specified period.
14
+ * @throws {RangeError} When `per` is outside the valid range.
15
+ */
16
+ export function ppmt(rate, per, nper, pv, futureValue = 0, type = 0) {
17
+ if (per <= 0 || per >= nper + 1) {
18
+ throw new RangeError("Invalid period.");
19
+ }
20
+
21
+ const periodicPayment = pmt(rate, nper, pv, futureValue, type);
22
+ const interestPayment = ipmt(rate, per, nper, pv, futureValue, type);
23
+
24
+ return periodicPayment - interestPayment;
25
+ }
package/src/pv.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Calculates the present value of an investment based on a series of regular payments and a constant interest rate.
3
+ *
4
+ * @param {number} rate - The interest rate per period.
5
+ * @param {number} nper - The total number of payment periods.
6
+ * @param {number} pmt - The payment made each period; cannot change over the life of the annuity.
7
+ * @param {number} fv - The future value, or a cash balance you want to attain after the last payment is made. Defaults to 0.
8
+ * @param {0|1} [type=0] - The number 0 or 1 and indicates when payments are due. 0 = end of period, 1 = beginning of period. Defaults to 0.
9
+ * @returns {number} The present value of the investment.
10
+ */
11
+ export function pv(rate, nper, pmt, fv = 0, type = 0) {
12
+ if (rate === 0) {
13
+ return -pmt * nper - fv;
14
+ } else {
15
+ const paymentTimingFactor = type !== 0 ? 1 + rate : 1;
16
+ const interestFactor = 1 + rate;
17
+ const compoundFactor = Math.pow(interestFactor, nper);
18
+
19
+ return (
20
+ -(fv + pmt * paymentTimingFactor * ((compoundFactor - 1) / rate)) /
21
+ compoundFactor
22
+ );
23
+ }
24
+ }
package/src/rate.js ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Evaluates the RATE equation for a candidate rate.
3
+ *
4
+ * @param {number} rate - Candidate rate.
5
+ * @param {number} nper - Total number of periods.
6
+ * @param {number} pmt - Periodic payment.
7
+ * @param {number} pv - Present value.
8
+ * @param {number} [fv=0] - Future value.
9
+ * @param {0|1} [type=0] - Payment timing: 0 = end of period, 1 = beginning.
10
+ * @returns {number} Equation result for the supplied rate.
11
+ */
12
+ function evalRate(rate, nper, pmt, pv, fv = 0, type = 0) {
13
+ if (rate === 0) {
14
+ return pv + pmt * nper + fv;
15
+ } else {
16
+ const interestFactor = 1 + rate;
17
+ const compoundFactor = Math.pow(interestFactor, nper);
18
+ const paymentTimingFactor = type !== 0 ? 1 + rate : 1;
19
+
20
+ return (
21
+ pv * compoundFactor +
22
+ (pmt * paymentTimingFactor * (compoundFactor - 1)) / rate +
23
+ fv
24
+ );
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Calculates the interest rate per period using iterative approximation.
30
+ *
31
+ * @param {number} nper - Total number of periods.
32
+ * @param {number} pmt - Periodic payment.
33
+ * @param {number} pv - Present value.
34
+ * @param {number} [fv=0] - Future value.
35
+ * @param {0|1} [type=0] - Payment timing: 0 = end of period, 1 = beginning.
36
+ * @param {number} [guess=0.1] - Initial guess for rate.
37
+ * @returns {number} The calculated rate per period.
38
+ * @throws {RangeError} When inputs are invalid or the algorithm cannot converge.
39
+ */
40
+ export function rate(nper, pmt, pv, fv = 0, type = 0, guess = 0.1) {
41
+ if (nper <= 0) {
42
+ throw new RangeError("Invalid period.");
43
+ }
44
+
45
+ const epsilonMax = 0.0000001;
46
+ const step = 0.00001;
47
+ const iterationMax = 128;
48
+
49
+ let rate0 = guess;
50
+ let y0 = evalRate(rate0, nper, pmt, pv, fv, type);
51
+
52
+ let rate1 = y0 > 0 ? rate0 / 2 : rate0 * 2;
53
+ let y1 = evalRate(rate1, nper, pmt, pv, fv, type);
54
+
55
+ for (let iteration = 0; iteration < iterationMax; iteration += 1) {
56
+ if (y1 === y0) {
57
+ rate0 = rate0 < rate1 ? rate0 - step : rate0 + step;
58
+ y0 = evalRate(rate0, nper, pmt, pv, fv, type);
59
+ }
60
+
61
+ if (y1 === y0) {
62
+ throw new RangeError("Cannot calculate RATE with the provided values.");
63
+ }
64
+
65
+ rate0 = rate1 - ((rate1 - rate0) * y1) / (y1 - y0);
66
+ y0 = evalRate(rate0, nper, pmt, pv, fv, type);
67
+
68
+ if (Math.abs(y0) < epsilonMax) {
69
+ return rate0;
70
+ }
71
+
72
+ const nextY = y0;
73
+ y0 = y1;
74
+ y1 = nextY;
75
+
76
+ const nextRate = rate0;
77
+ rate0 = rate1;
78
+ rate1 = nextRate;
79
+ }
80
+
81
+ throw new RangeError("Maximum iterations exceeded while calculating RATE.");
82
+ }