@pyverret/ratejs 1.1.0 → 1.1.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/README.md +51 -15
- package/dist/index.cjs +149 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +149 -46
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -8,6 +8,24 @@ Lightweight, dependency-free TypeScript financial math library providing pure ca
|
|
|
8
8
|
npm i @pyverret/ratejs
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
## Validate Before Push
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm run validate
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Automated Releases
|
|
18
|
+
|
|
19
|
+
Releases are managed by `semantic-release` on pushes to `main`.
|
|
20
|
+
|
|
21
|
+
Required GitHub secrets:
|
|
22
|
+
- `NPM_TOKEN`: npm automation token with publish access.
|
|
23
|
+
|
|
24
|
+
Behavior:
|
|
25
|
+
- Conventional Commits drive version bumps (`fix` = patch, `feat` = minor, `feat!` or `BREAKING CHANGE` = major).
|
|
26
|
+
- Release workflow runs `npm run validate` before publishing.
|
|
27
|
+
- Git tags use format `vX.Y.Z`.
|
|
28
|
+
|
|
11
29
|
## Live Demo
|
|
12
30
|
|
|
13
31
|
- GitHub Pages URL: `https://pyverret.github.io/ratejs/`
|
|
@@ -44,16 +62,19 @@ const payment = pmt({
|
|
|
44
62
|
ratePerPeriod: 0.06 / 12,
|
|
45
63
|
periods: 360,
|
|
46
64
|
presentValue: 250000,
|
|
47
|
-
futureValue: 0,
|
|
48
|
-
timing: "end",
|
|
65
|
+
futureValue: 0, // optional
|
|
66
|
+
timing: "end", // optional
|
|
49
67
|
});
|
|
50
68
|
|
|
51
69
|
const impliedRate = rate({
|
|
52
70
|
periods: 360,
|
|
53
71
|
payment,
|
|
54
72
|
presentValue: 250000,
|
|
55
|
-
futureValue: 0,
|
|
56
|
-
timing: "end",
|
|
73
|
+
futureValue: 0, // optional
|
|
74
|
+
timing: "end", // optional
|
|
75
|
+
guess: 0.1, // optional
|
|
76
|
+
maxIterations: 100, // optional
|
|
77
|
+
tolerance: 1e-10, // optional
|
|
57
78
|
lowerBound: -0.99, // optional
|
|
58
79
|
upperBound: 10, // optional
|
|
59
80
|
});
|
|
@@ -62,6 +83,7 @@ const impliedRate = rate({
|
|
|
62
83
|
Edge cases:
|
|
63
84
|
- `nper` throws `RangeError` when `ratePerPeriod <= -1`.
|
|
64
85
|
- `rate` throws `RangeError` when no root is found within search bounds.
|
|
86
|
+
- `pmt`, `pv`, and `fv` throw `RangeError` when `ratePerPeriod <= -1`.
|
|
65
87
|
|
|
66
88
|
### Interest & growth
|
|
67
89
|
|
|
@@ -88,11 +110,11 @@ presentValue({ futureValue: 5000, rate: 0.06, timesPerYear: 12, years: 5 });
|
|
|
88
110
|
```ts
|
|
89
111
|
investmentGrowth({
|
|
90
112
|
initial: 1000,
|
|
91
|
-
contributionPerPeriod: 100,
|
|
113
|
+
contributionPerPeriod: 100, // optional
|
|
92
114
|
rate: 0.06,
|
|
93
115
|
timesPerYear: 12,
|
|
94
116
|
years: 2,
|
|
95
|
-
contributionTiming: "end", //
|
|
117
|
+
contributionTiming: "end", // optional ("end" | "begin")
|
|
96
118
|
});
|
|
97
119
|
```
|
|
98
120
|
|
|
@@ -111,13 +133,16 @@ periodsToReachGoal({
|
|
|
111
133
|
rate: 0.06,
|
|
112
134
|
timesPerYear: 12,
|
|
113
135
|
contributionPerPeriod: 0, // optional
|
|
114
|
-
contributionTiming: "end",
|
|
136
|
+
contributionTiming: "end", // optional
|
|
137
|
+
maxPeriods: 100000, // optional
|
|
115
138
|
});
|
|
116
139
|
```
|
|
117
140
|
|
|
118
141
|
Edge cases:
|
|
119
142
|
- Returns `Infinity` when the goal is unreachable.
|
|
120
143
|
- Throws `RangeError` when `rate / timesPerYear <= -1`.
|
|
144
|
+
- Throws `RangeError` when `maxPeriods` is exceeded for contribution-based iteration.
|
|
145
|
+
- Throws `RangeError` when `contributionTiming` is not `"end"` or `"begin"`.
|
|
121
146
|
|
|
122
147
|
- **`rateToReachGoal`** - Rate per period required to reach a target future value in a given number of periods.
|
|
123
148
|
|
|
@@ -126,11 +151,19 @@ rateToReachGoal({
|
|
|
126
151
|
principal: 1000,
|
|
127
152
|
targetFutureValue: 1500,
|
|
128
153
|
periods: 24,
|
|
129
|
-
contributionPerPeriod: 0,
|
|
130
|
-
contributionTiming: "end",
|
|
154
|
+
contributionPerPeriod: 0, // optional
|
|
155
|
+
contributionTiming: "end", // optional
|
|
156
|
+
maxIterations: 100, // optional
|
|
157
|
+
tolerance: 1e-10, // optional
|
|
158
|
+
lowerBound: -0.99, // optional
|
|
159
|
+
upperBound: 10, // optional
|
|
131
160
|
});
|
|
132
161
|
```
|
|
133
162
|
|
|
163
|
+
Edge cases:
|
|
164
|
+
- Throws `RangeError` when no root is found within search bounds.
|
|
165
|
+
- Throws `RangeError` when `contributionTiming` is not `"end"` or `"begin"`.
|
|
166
|
+
|
|
134
167
|
- **`ruleOf72`** - Approximate years to double a lump sum at a given annual rate. Optional `constant: 69` for rule of 69.
|
|
135
168
|
|
|
136
169
|
```ts
|
|
@@ -151,8 +184,8 @@ cagr({ startValue: 1000, endValue: 2000, years: 10 });
|
|
|
151
184
|
```ts
|
|
152
185
|
irr({
|
|
153
186
|
cashFlows: [-1000, 300, 400, 500],
|
|
154
|
-
guess: 0.1,
|
|
155
|
-
maxIterations: 100,
|
|
187
|
+
guess: 0.1, // optional
|
|
188
|
+
maxIterations: 100, // optional
|
|
156
189
|
lowerBound: -0.99, // optional
|
|
157
190
|
upperBound: 10, // optional
|
|
158
191
|
});
|
|
@@ -197,7 +230,7 @@ presentValueOfAnnuity({
|
|
|
197
230
|
paymentPerPeriod: 100,
|
|
198
231
|
ratePerPeriod: 0.01,
|
|
199
232
|
periods: 36,
|
|
200
|
-
timing: "end",
|
|
233
|
+
timing: "end", // optional
|
|
201
234
|
});
|
|
202
235
|
```
|
|
203
236
|
|
|
@@ -208,7 +241,7 @@ paymentFromPresentValue({
|
|
|
208
241
|
presentValue: 100000,
|
|
209
242
|
ratePerPeriod: 0.005,
|
|
210
243
|
periods: 360,
|
|
211
|
-
timing: "end",
|
|
244
|
+
timing: "end", // optional
|
|
212
245
|
});
|
|
213
246
|
```
|
|
214
247
|
|
|
@@ -225,6 +258,9 @@ loanPayment({
|
|
|
225
258
|
});
|
|
226
259
|
```
|
|
227
260
|
|
|
261
|
+
Edge cases:
|
|
262
|
+
- Throws `RangeError` when `annualRate / paymentsPerYear <= -1`.
|
|
263
|
+
|
|
228
264
|
- **`amortizationSchedule`** - Full schedule: `{ paymentPerPeriod, schedule, totalPaid, totalInterest }`. Optional `extraPaymentPerPeriod`.
|
|
229
265
|
|
|
230
266
|
```ts
|
|
@@ -233,7 +269,7 @@ amortizationSchedule({
|
|
|
233
269
|
annualRate: 0.06,
|
|
234
270
|
paymentsPerYear: 12,
|
|
235
271
|
years: 30,
|
|
236
|
-
extraPaymentPerPeriod: 50,
|
|
272
|
+
extraPaymentPerPeriod: 50, // optional
|
|
237
273
|
});
|
|
238
274
|
```
|
|
239
275
|
|
|
@@ -270,7 +306,7 @@ Edge cases:
|
|
|
270
306
|
|
|
271
307
|
```ts
|
|
272
308
|
roundToCurrency({ value: 2.125 }); // 2.13
|
|
273
|
-
roundToCurrency({ value: 2.125, decimals: 2, mode: "half-even" });
|
|
309
|
+
roundToCurrency({ value: 2.125, decimals: 2, mode: "half-even" }); // decimals/mode optional
|
|
274
310
|
```
|
|
275
311
|
|
|
276
312
|
## Design
|
package/dist/index.cjs
CHANGED
|
@@ -187,32 +187,31 @@ function hasPositiveAndNegative(cashFlows) {
|
|
|
187
187
|
return false;
|
|
188
188
|
}
|
|
189
189
|
function findBracket(fn, lowerBound, upperBound) {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
190
|
+
const scan = (start, end, segments = 200) => {
|
|
191
|
+
let prevX;
|
|
192
|
+
let prevValue;
|
|
193
|
+
for (let i = 0; i <= segments; i++) {
|
|
194
|
+
const x = start + (end - start) * i / segments;
|
|
195
|
+
const value = fn(x);
|
|
196
|
+
if (!Number.isFinite(value)) continue;
|
|
197
|
+
if (value === 0) return { lower: x, upper: x };
|
|
198
|
+
if (prevX !== void 0 && prevValue !== void 0 && prevValue * value < 0) {
|
|
199
|
+
return { lower: prevX, upper: x };
|
|
200
|
+
}
|
|
201
|
+
prevX = x;
|
|
202
|
+
prevValue = value;
|
|
203
|
+
}
|
|
204
|
+
return void 0;
|
|
205
|
+
};
|
|
196
206
|
let lower = lowerBound;
|
|
197
207
|
let upper = upperBound;
|
|
198
|
-
let fLo = fLower;
|
|
199
|
-
let fHi = fUpper;
|
|
200
208
|
for (let i = 0; i < 20; i++) {
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return { lower: nextLower, upper };
|
|
205
|
-
}
|
|
206
|
-
if (Number.isFinite(fNextLower)) {
|
|
207
|
-
lower = nextLower;
|
|
208
|
-
fLo = fNextLower;
|
|
209
|
-
}
|
|
209
|
+
const bracket = scan(lower, upper);
|
|
210
|
+
if (bracket !== void 0) return bracket;
|
|
211
|
+
lower = Math.max(-0.999999999, (lower - 1) / 2);
|
|
210
212
|
upper = upper * 2 + 1;
|
|
211
|
-
fHi = fn(upper);
|
|
212
|
-
if (!Number.isFinite(fHi)) continue;
|
|
213
|
-
if (fLo * fHi < 0) return { lower, upper };
|
|
214
213
|
}
|
|
215
|
-
return
|
|
214
|
+
return scan(lower, upper, 400);
|
|
216
215
|
}
|
|
217
216
|
function irr(params) {
|
|
218
217
|
const {
|
|
@@ -302,6 +301,11 @@ function paymentFromPresentValue(params) {
|
|
|
302
301
|
}
|
|
303
302
|
|
|
304
303
|
// src/interest/periodsToReachGoal.ts
|
|
304
|
+
function assertContributionTiming(value, name) {
|
|
305
|
+
if (value !== "end" && value !== "begin") {
|
|
306
|
+
throw new RangeError(`${name} must be "end" or "begin"`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
305
309
|
function periodsToReachGoal(params) {
|
|
306
310
|
const {
|
|
307
311
|
principal,
|
|
@@ -309,13 +313,16 @@ function periodsToReachGoal(params) {
|
|
|
309
313
|
rate: rate2,
|
|
310
314
|
timesPerYear,
|
|
311
315
|
contributionPerPeriod = 0,
|
|
312
|
-
contributionTiming = "end"
|
|
316
|
+
contributionTiming = "end",
|
|
317
|
+
maxPeriods = 1e5
|
|
313
318
|
} = params;
|
|
314
319
|
assertNonNegative(principal, "principal");
|
|
315
320
|
assertNonNegative(targetFutureValue, "targetFutureValue");
|
|
316
321
|
assertFiniteNumber(rate2, "rate");
|
|
317
322
|
assertPositive(timesPerYear, "timesPerYear");
|
|
318
323
|
assertNonNegative(contributionPerPeriod, "contributionPerPeriod");
|
|
324
|
+
assertPositive(maxPeriods, "maxPeriods");
|
|
325
|
+
assertContributionTiming(contributionTiming, "contributionTiming");
|
|
319
326
|
if (targetFutureValue <= principal) return 0;
|
|
320
327
|
const r = rate2 / timesPerYear;
|
|
321
328
|
if (r <= -1) {
|
|
@@ -335,13 +342,13 @@ function periodsToReachGoal(params) {
|
|
|
335
342
|
}
|
|
336
343
|
let fv2 = principal;
|
|
337
344
|
let periods = 0;
|
|
338
|
-
const maxPeriods = 1e4;
|
|
339
345
|
while (fv2 < targetFutureValue && periods < maxPeriods) {
|
|
340
346
|
if (contributionTiming === "begin") fv2 += contributionPerPeriod;
|
|
341
347
|
fv2 = fv2 * (1 + r) + (contributionTiming === "end" ? contributionPerPeriod : 0);
|
|
342
348
|
periods++;
|
|
343
349
|
}
|
|
344
|
-
|
|
350
|
+
if (fv2 >= targetFutureValue) return periods;
|
|
351
|
+
throw new RangeError("maxPeriods exceeded before reaching target");
|
|
345
352
|
}
|
|
346
353
|
|
|
347
354
|
// src/interest/presentValue.ts
|
|
@@ -371,22 +378,46 @@ function presentValueOfAnnuity(params) {
|
|
|
371
378
|
}
|
|
372
379
|
|
|
373
380
|
// src/interest/rateToReachGoal.ts
|
|
381
|
+
function assertContributionTiming2(value, name) {
|
|
382
|
+
if (value !== "end" && value !== "begin") {
|
|
383
|
+
throw new RangeError(`${name} must be "end" or "begin"`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
374
386
|
function rateToReachGoal(params) {
|
|
375
387
|
const {
|
|
376
388
|
principal,
|
|
377
389
|
targetFutureValue,
|
|
378
390
|
periods,
|
|
379
391
|
contributionPerPeriod = 0,
|
|
380
|
-
contributionTiming = "end"
|
|
392
|
+
contributionTiming = "end",
|
|
393
|
+
lowerBound = -0.99,
|
|
394
|
+
upperBound = 10,
|
|
395
|
+
maxIterations = 100,
|
|
396
|
+
tolerance = 1e-10
|
|
381
397
|
} = params;
|
|
382
398
|
assertNonNegative(principal, "principal");
|
|
383
399
|
assertNonNegative(targetFutureValue, "targetFutureValue");
|
|
384
400
|
assertPositive(periods, "periods");
|
|
385
401
|
assertNonNegative(contributionPerPeriod, "contributionPerPeriod");
|
|
386
|
-
|
|
402
|
+
assertFiniteNumber(lowerBound, "lowerBound");
|
|
403
|
+
assertFiniteNumber(upperBound, "upperBound");
|
|
404
|
+
assertFiniteNumber(maxIterations, "maxIterations");
|
|
405
|
+
assertFiniteNumber(tolerance, "tolerance");
|
|
406
|
+
assertContributionTiming2(contributionTiming, "contributionTiming");
|
|
407
|
+
if (lowerBound <= -1) throw new RangeError("lowerBound must be > -1");
|
|
408
|
+
if (upperBound <= lowerBound) throw new RangeError("upperBound must be greater than lowerBound");
|
|
409
|
+
if (!Number.isInteger(maxIterations) || maxIterations <= 0) {
|
|
410
|
+
throw new RangeError("maxIterations must be a positive integer");
|
|
411
|
+
}
|
|
412
|
+
if (tolerance <= 0) throw new RangeError("tolerance must be > 0");
|
|
387
413
|
if (targetFutureValue <= principal && contributionPerPeriod === 0) return 0;
|
|
388
414
|
if (contributionPerPeriod === 0) {
|
|
389
415
|
if (targetFutureValue <= principal) return 0;
|
|
416
|
+
if (principal === 0) {
|
|
417
|
+
throw new RangeError(
|
|
418
|
+
"rateToReachGoal is undefined when principal is 0 and contributions are 0"
|
|
419
|
+
);
|
|
420
|
+
}
|
|
390
421
|
return (targetFutureValue / principal) ** (1 / periods) - 1;
|
|
391
422
|
}
|
|
392
423
|
const dueFactor = contributionTiming === "begin" ? 1 : 0;
|
|
@@ -404,23 +435,56 @@ function rateToReachGoal(params) {
|
|
|
404
435
|
};
|
|
405
436
|
const initialGuess = (targetFutureValue / (principal + contributionPerPeriod * periods)) ** (1 / periods) - 1;
|
|
406
437
|
const newton = newtonRaphson({
|
|
407
|
-
initialGuess: Number.isFinite(initialGuess) ? Math.max(initialGuess,
|
|
438
|
+
initialGuess: Number.isFinite(initialGuess) ? Math.max(initialGuess, lowerBound) : 0.01,
|
|
408
439
|
fn,
|
|
409
440
|
derivative,
|
|
410
|
-
tolerance
|
|
411
|
-
maxIterations
|
|
412
|
-
min:
|
|
413
|
-
max:
|
|
441
|
+
tolerance,
|
|
442
|
+
maxIterations,
|
|
443
|
+
min: lowerBound,
|
|
444
|
+
max: upperBound
|
|
414
445
|
});
|
|
415
446
|
if (newton !== void 0) return newton;
|
|
447
|
+
const bracket = findBracket2(fn, lowerBound, upperBound);
|
|
448
|
+
if (bracket === void 0) {
|
|
449
|
+
throw new RangeError("rateToReachGoal did not converge within search bounds");
|
|
450
|
+
}
|
|
451
|
+
if (bracket.lower === bracket.upper) return bracket.lower;
|
|
416
452
|
const bisected = bisection({
|
|
417
453
|
fn,
|
|
418
|
-
lower:
|
|
419
|
-
upper:
|
|
420
|
-
tolerance
|
|
421
|
-
maxIterations:
|
|
454
|
+
lower: bracket.lower,
|
|
455
|
+
upper: bracket.upper,
|
|
456
|
+
tolerance,
|
|
457
|
+
maxIterations: maxIterations * 2
|
|
422
458
|
});
|
|
423
|
-
|
|
459
|
+
if (bisected !== void 0) return bisected;
|
|
460
|
+
throw new RangeError("rateToReachGoal did not converge");
|
|
461
|
+
}
|
|
462
|
+
function findBracket2(fn, lowerBound, upperBound) {
|
|
463
|
+
const scan = (start, end, segments = 200) => {
|
|
464
|
+
let prevX;
|
|
465
|
+
let prevValue;
|
|
466
|
+
for (let i = 0; i <= segments; i++) {
|
|
467
|
+
const x = start + (end - start) * i / segments;
|
|
468
|
+
const value = fn(x);
|
|
469
|
+
if (!Number.isFinite(value)) continue;
|
|
470
|
+
if (value === 0) return { lower: x, upper: x };
|
|
471
|
+
if (prevX !== void 0 && prevValue !== void 0 && prevValue * value < 0) {
|
|
472
|
+
return { lower: prevX, upper: x };
|
|
473
|
+
}
|
|
474
|
+
prevX = x;
|
|
475
|
+
prevValue = value;
|
|
476
|
+
}
|
|
477
|
+
return void 0;
|
|
478
|
+
};
|
|
479
|
+
let lower = lowerBound;
|
|
480
|
+
let upper = upperBound;
|
|
481
|
+
for (let i = 0; i < 20; i++) {
|
|
482
|
+
const bracket = scan(lower, upper);
|
|
483
|
+
if (bracket !== void 0) return bracket;
|
|
484
|
+
lower = Math.max(-0.999999999, (lower - 1) / 2);
|
|
485
|
+
upper = upper * 2 + 1;
|
|
486
|
+
}
|
|
487
|
+
return scan(lower, upper, 400);
|
|
424
488
|
}
|
|
425
489
|
|
|
426
490
|
// src/interest/realReturn.ts
|
|
@@ -450,8 +514,12 @@ function annuityDueFactor(ratePerPeriod, timing) {
|
|
|
450
514
|
return timing === "begin" ? 1 + ratePerPeriod : 1;
|
|
451
515
|
}
|
|
452
516
|
function futureValueFromCashFlows(ratePerPeriod, periods, payment, presentValue2, timing) {
|
|
517
|
+
if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
|
|
518
|
+
throw new RangeError("ratePerPeriod must be > -1");
|
|
519
|
+
}
|
|
453
520
|
if (ratePerPeriod === 0) return -(presentValue2 + payment * periods);
|
|
454
521
|
const growth = (1 + ratePerPeriod) ** periods;
|
|
522
|
+
if (!Number.isFinite(growth) || growth <= 0) return Number.NaN;
|
|
455
523
|
const paymentFv = payment * ((growth - 1) / ratePerPeriod) * annuityDueFactor(ratePerPeriod, timing);
|
|
456
524
|
return -(presentValue2 * growth + paymentFv);
|
|
457
525
|
}
|
|
@@ -462,7 +530,11 @@ function fv(params) {
|
|
|
462
530
|
assertFiniteNumber(payment, "payment");
|
|
463
531
|
assertFiniteNumber(presentValue2, "presentValue");
|
|
464
532
|
assertTiming(timing, "timing");
|
|
465
|
-
|
|
533
|
+
const value = futureValueFromCashFlows(ratePerPeriod, periods, payment, presentValue2, timing);
|
|
534
|
+
if (!Number.isFinite(value)) {
|
|
535
|
+
throw new RangeError("Numerical instability for given inputs");
|
|
536
|
+
}
|
|
537
|
+
return value;
|
|
466
538
|
}
|
|
467
539
|
function pv(params) {
|
|
468
540
|
const { ratePerPeriod, periods, payment = 0, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
@@ -471,10 +543,20 @@ function pv(params) {
|
|
|
471
543
|
assertFiniteNumber(payment, "payment");
|
|
472
544
|
assertFiniteNumber(futureValue2, "futureValue");
|
|
473
545
|
assertTiming(timing, "timing");
|
|
546
|
+
if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
|
|
547
|
+
throw new RangeError("ratePerPeriod must be > -1");
|
|
548
|
+
}
|
|
474
549
|
if (ratePerPeriod === 0) return -(futureValue2 + payment * periods);
|
|
475
550
|
const growth = (1 + ratePerPeriod) ** periods;
|
|
551
|
+
if (!Number.isFinite(growth) || growth <= 0) {
|
|
552
|
+
throw new RangeError("Numerical instability for given ratePerPeriod/periods");
|
|
553
|
+
}
|
|
476
554
|
const paymentPv = payment * (1 - 1 / growth) / ratePerPeriod * annuityDueFactor(ratePerPeriod, timing);
|
|
477
|
-
|
|
555
|
+
const value = -futureValue2 / growth - paymentPv;
|
|
556
|
+
if (!Number.isFinite(value)) {
|
|
557
|
+
throw new RangeError("Numerical instability for given inputs");
|
|
558
|
+
}
|
|
559
|
+
return value;
|
|
478
560
|
}
|
|
479
561
|
function pmt(params) {
|
|
480
562
|
const { ratePerPeriod, periods, presentValue: presentValue2, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
@@ -483,11 +565,21 @@ function pmt(params) {
|
|
|
483
565
|
assertFiniteNumber(presentValue2, "presentValue");
|
|
484
566
|
assertFiniteNumber(futureValue2, "futureValue");
|
|
485
567
|
assertTiming(timing, "timing");
|
|
568
|
+
if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
|
|
569
|
+
throw new RangeError("ratePerPeriod must be > -1");
|
|
570
|
+
}
|
|
486
571
|
if (ratePerPeriod === 0) return -(presentValue2 + futureValue2) / periods;
|
|
487
572
|
const growth = (1 + ratePerPeriod) ** periods;
|
|
573
|
+
if (!Number.isFinite(growth) || growth <= 0) {
|
|
574
|
+
throw new RangeError("Numerical instability for given ratePerPeriod/periods");
|
|
575
|
+
}
|
|
488
576
|
const numerator = -(futureValue2 + presentValue2 * growth) * ratePerPeriod;
|
|
489
577
|
const denominator = (growth - 1) * annuityDueFactor(ratePerPeriod, timing);
|
|
490
|
-
|
|
578
|
+
const value = numerator / denominator;
|
|
579
|
+
if (!Number.isFinite(value)) {
|
|
580
|
+
throw new RangeError("Numerical instability for given inputs");
|
|
581
|
+
}
|
|
582
|
+
return value;
|
|
491
583
|
}
|
|
492
584
|
function nper(params) {
|
|
493
585
|
const { ratePerPeriod, payment, presentValue: presentValue2, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
@@ -608,13 +700,21 @@ function findRateBracket(fn, lowerBound, upperBound) {
|
|
|
608
700
|
function loanPayment(params) {
|
|
609
701
|
const { principal, annualRate, paymentsPerYear, years } = params;
|
|
610
702
|
assertNonNegative(principal, "principal");
|
|
703
|
+
assertFiniteNumber(annualRate, "annualRate");
|
|
611
704
|
assertPositive(paymentsPerYear, "paymentsPerYear");
|
|
612
705
|
assertNonNegative(years, "years");
|
|
613
706
|
const n = Math.round(paymentsPerYear * years);
|
|
614
707
|
if (n === 0) return 0;
|
|
615
708
|
const r = annualRate / paymentsPerYear;
|
|
709
|
+
if (r <= -1) {
|
|
710
|
+
throw new RangeError("annualRate / paymentsPerYear must be > -1");
|
|
711
|
+
}
|
|
616
712
|
if (r === 0) return principal / n;
|
|
617
|
-
|
|
713
|
+
const value = r * principal / (1 - (1 + r) ** -n);
|
|
714
|
+
if (!Number.isFinite(value)) {
|
|
715
|
+
throw new RangeError("Numerical instability for given inputs");
|
|
716
|
+
}
|
|
717
|
+
return value;
|
|
618
718
|
}
|
|
619
719
|
|
|
620
720
|
// src/loans/amortizationSchedule.ts
|
|
@@ -712,13 +812,16 @@ function roundToCurrency(params) {
|
|
|
712
812
|
const factor = 10 ** d;
|
|
713
813
|
if (mode === "half-even") {
|
|
714
814
|
const scaled = value * factor;
|
|
715
|
-
const
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
815
|
+
const sign = scaled < 0 ? -1 : 1;
|
|
816
|
+
const absScaled = Math.abs(scaled);
|
|
817
|
+
const floorAbs = Math.floor(absScaled);
|
|
818
|
+
const fraction = absScaled - floorAbs;
|
|
819
|
+
const epsilon = 1e-12;
|
|
820
|
+
if (Math.abs(fraction - 0.5) <= epsilon) {
|
|
821
|
+
const nearestEven = floorAbs % 2 === 0 ? floorAbs : floorAbs + 1;
|
|
822
|
+
return sign * nearestEven / factor;
|
|
720
823
|
}
|
|
721
|
-
return
|
|
824
|
+
return Math.round(scaled) / factor;
|
|
722
825
|
}
|
|
723
826
|
return Math.round(value * factor) / factor;
|
|
724
827
|
}
|