@pyverret/ratejs 1.1.0 → 1.1.1
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 +10 -0
- package/dist/index.cjs +137 -45
- 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 +137 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ const impliedRate = rate({
|
|
|
62
62
|
Edge cases:
|
|
63
63
|
- `nper` throws `RangeError` when `ratePerPeriod <= -1`.
|
|
64
64
|
- `rate` throws `RangeError` when no root is found within search bounds.
|
|
65
|
+
- `pmt`, `pv`, and `fv` throw `RangeError` when `ratePerPeriod <= -1`.
|
|
65
66
|
|
|
66
67
|
### Interest & growth
|
|
67
68
|
|
|
@@ -118,6 +119,7 @@ periodsToReachGoal({
|
|
|
118
119
|
Edge cases:
|
|
119
120
|
- Returns `Infinity` when the goal is unreachable.
|
|
120
121
|
- Throws `RangeError` when `rate / timesPerYear <= -1`.
|
|
122
|
+
- Throws `RangeError` when `maxPeriods` is exceeded for contribution-based iteration.
|
|
121
123
|
|
|
122
124
|
- **`rateToReachGoal`** - Rate per period required to reach a target future value in a given number of periods.
|
|
123
125
|
|
|
@@ -128,9 +130,14 @@ rateToReachGoal({
|
|
|
128
130
|
periods: 24,
|
|
129
131
|
contributionPerPeriod: 0,
|
|
130
132
|
contributionTiming: "end",
|
|
133
|
+
lowerBound: -0.99, // optional
|
|
134
|
+
upperBound: 10, // optional
|
|
131
135
|
});
|
|
132
136
|
```
|
|
133
137
|
|
|
138
|
+
Edge cases:
|
|
139
|
+
- Throws `RangeError` when no root is found within search bounds.
|
|
140
|
+
|
|
134
141
|
- **`ruleOf72`** - Approximate years to double a lump sum at a given annual rate. Optional `constant: 69` for rule of 69.
|
|
135
142
|
|
|
136
143
|
```ts
|
|
@@ -225,6 +232,9 @@ loanPayment({
|
|
|
225
232
|
});
|
|
226
233
|
```
|
|
227
234
|
|
|
235
|
+
Edge cases:
|
|
236
|
+
- Throws `RangeError` when `annualRate / paymentsPerYear <= -1`.
|
|
237
|
+
|
|
228
238
|
- **`amortizationSchedule`** - Full schedule: `{ paymentPerPeriod, schedule, totalPaid, totalInterest }`. Optional `extraPaymentPerPeriod`.
|
|
229
239
|
|
|
230
240
|
```ts
|
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 {
|
|
@@ -309,13 +308,15 @@ function periodsToReachGoal(params) {
|
|
|
309
308
|
rate: rate2,
|
|
310
309
|
timesPerYear,
|
|
311
310
|
contributionPerPeriod = 0,
|
|
312
|
-
contributionTiming = "end"
|
|
311
|
+
contributionTiming = "end",
|
|
312
|
+
maxPeriods = 1e5
|
|
313
313
|
} = params;
|
|
314
314
|
assertNonNegative(principal, "principal");
|
|
315
315
|
assertNonNegative(targetFutureValue, "targetFutureValue");
|
|
316
316
|
assertFiniteNumber(rate2, "rate");
|
|
317
317
|
assertPositive(timesPerYear, "timesPerYear");
|
|
318
318
|
assertNonNegative(contributionPerPeriod, "contributionPerPeriod");
|
|
319
|
+
assertPositive(maxPeriods, "maxPeriods");
|
|
319
320
|
if (targetFutureValue <= principal) return 0;
|
|
320
321
|
const r = rate2 / timesPerYear;
|
|
321
322
|
if (r <= -1) {
|
|
@@ -335,13 +336,13 @@ function periodsToReachGoal(params) {
|
|
|
335
336
|
}
|
|
336
337
|
let fv2 = principal;
|
|
337
338
|
let periods = 0;
|
|
338
|
-
const maxPeriods = 1e4;
|
|
339
339
|
while (fv2 < targetFutureValue && periods < maxPeriods) {
|
|
340
340
|
if (contributionTiming === "begin") fv2 += contributionPerPeriod;
|
|
341
341
|
fv2 = fv2 * (1 + r) + (contributionTiming === "end" ? contributionPerPeriod : 0);
|
|
342
342
|
periods++;
|
|
343
343
|
}
|
|
344
|
-
|
|
344
|
+
if (fv2 >= targetFutureValue) return periods;
|
|
345
|
+
throw new RangeError("maxPeriods exceeded before reaching target");
|
|
345
346
|
}
|
|
346
347
|
|
|
347
348
|
// src/interest/presentValue.ts
|
|
@@ -377,16 +378,35 @@ function rateToReachGoal(params) {
|
|
|
377
378
|
targetFutureValue,
|
|
378
379
|
periods,
|
|
379
380
|
contributionPerPeriod = 0,
|
|
380
|
-
contributionTiming = "end"
|
|
381
|
+
contributionTiming = "end",
|
|
382
|
+
lowerBound = -0.99,
|
|
383
|
+
upperBound = 10,
|
|
384
|
+
maxIterations = 100,
|
|
385
|
+
tolerance = 1e-10
|
|
381
386
|
} = params;
|
|
382
387
|
assertNonNegative(principal, "principal");
|
|
383
388
|
assertNonNegative(targetFutureValue, "targetFutureValue");
|
|
384
389
|
assertPositive(periods, "periods");
|
|
385
390
|
assertNonNegative(contributionPerPeriod, "contributionPerPeriod");
|
|
391
|
+
assertFiniteNumber(lowerBound, "lowerBound");
|
|
392
|
+
assertFiniteNumber(upperBound, "upperBound");
|
|
393
|
+
assertFiniteNumber(maxIterations, "maxIterations");
|
|
394
|
+
assertFiniteNumber(tolerance, "tolerance");
|
|
395
|
+
if (lowerBound <= -1) throw new RangeError("lowerBound must be > -1");
|
|
396
|
+
if (upperBound <= lowerBound) throw new RangeError("upperBound must be greater than lowerBound");
|
|
397
|
+
if (!Number.isInteger(maxIterations) || maxIterations <= 0) {
|
|
398
|
+
throw new RangeError("maxIterations must be a positive integer");
|
|
399
|
+
}
|
|
400
|
+
if (tolerance <= 0) throw new RangeError("tolerance must be > 0");
|
|
386
401
|
if (periods === 0) return 0;
|
|
387
402
|
if (targetFutureValue <= principal && contributionPerPeriod === 0) return 0;
|
|
388
403
|
if (contributionPerPeriod === 0) {
|
|
389
404
|
if (targetFutureValue <= principal) return 0;
|
|
405
|
+
if (principal === 0) {
|
|
406
|
+
throw new RangeError(
|
|
407
|
+
"rateToReachGoal is undefined when principal is 0 and contributions are 0"
|
|
408
|
+
);
|
|
409
|
+
}
|
|
390
410
|
return (targetFutureValue / principal) ** (1 / periods) - 1;
|
|
391
411
|
}
|
|
392
412
|
const dueFactor = contributionTiming === "begin" ? 1 : 0;
|
|
@@ -404,23 +424,56 @@ function rateToReachGoal(params) {
|
|
|
404
424
|
};
|
|
405
425
|
const initialGuess = (targetFutureValue / (principal + contributionPerPeriod * periods)) ** (1 / periods) - 1;
|
|
406
426
|
const newton = newtonRaphson({
|
|
407
|
-
initialGuess: Number.isFinite(initialGuess) ? Math.max(initialGuess,
|
|
427
|
+
initialGuess: Number.isFinite(initialGuess) ? Math.max(initialGuess, lowerBound) : 0.01,
|
|
408
428
|
fn,
|
|
409
429
|
derivative,
|
|
410
|
-
tolerance
|
|
411
|
-
maxIterations
|
|
412
|
-
min:
|
|
413
|
-
max:
|
|
430
|
+
tolerance,
|
|
431
|
+
maxIterations,
|
|
432
|
+
min: lowerBound,
|
|
433
|
+
max: upperBound
|
|
414
434
|
});
|
|
415
435
|
if (newton !== void 0) return newton;
|
|
436
|
+
const bracket = findBracket2(fn, lowerBound, upperBound);
|
|
437
|
+
if (bracket === void 0) {
|
|
438
|
+
throw new RangeError("rateToReachGoal did not converge within search bounds");
|
|
439
|
+
}
|
|
440
|
+
if (bracket.lower === bracket.upper) return bracket.lower;
|
|
416
441
|
const bisected = bisection({
|
|
417
442
|
fn,
|
|
418
|
-
lower:
|
|
419
|
-
upper:
|
|
420
|
-
tolerance
|
|
421
|
-
maxIterations:
|
|
443
|
+
lower: bracket.lower,
|
|
444
|
+
upper: bracket.upper,
|
|
445
|
+
tolerance,
|
|
446
|
+
maxIterations: maxIterations * 2
|
|
422
447
|
});
|
|
423
|
-
|
|
448
|
+
if (bisected !== void 0) return bisected;
|
|
449
|
+
throw new RangeError("rateToReachGoal did not converge");
|
|
450
|
+
}
|
|
451
|
+
function findBracket2(fn, lowerBound, upperBound) {
|
|
452
|
+
const scan = (start, end, segments = 200) => {
|
|
453
|
+
let prevX;
|
|
454
|
+
let prevValue;
|
|
455
|
+
for (let i = 0; i <= segments; i++) {
|
|
456
|
+
const x = start + (end - start) * i / segments;
|
|
457
|
+
const value = fn(x);
|
|
458
|
+
if (!Number.isFinite(value)) continue;
|
|
459
|
+
if (value === 0) return { lower: x, upper: x };
|
|
460
|
+
if (prevX !== void 0 && prevValue !== void 0 && prevValue * value < 0) {
|
|
461
|
+
return { lower: prevX, upper: x };
|
|
462
|
+
}
|
|
463
|
+
prevX = x;
|
|
464
|
+
prevValue = value;
|
|
465
|
+
}
|
|
466
|
+
return void 0;
|
|
467
|
+
};
|
|
468
|
+
let lower = lowerBound;
|
|
469
|
+
let upper = upperBound;
|
|
470
|
+
for (let i = 0; i < 20; i++) {
|
|
471
|
+
const bracket = scan(lower, upper);
|
|
472
|
+
if (bracket !== void 0) return bracket;
|
|
473
|
+
lower = Math.max(-0.999999999, (lower - 1) / 2);
|
|
474
|
+
upper = upper * 2 + 1;
|
|
475
|
+
}
|
|
476
|
+
return scan(lower, upper, 400);
|
|
424
477
|
}
|
|
425
478
|
|
|
426
479
|
// src/interest/realReturn.ts
|
|
@@ -450,8 +503,12 @@ function annuityDueFactor(ratePerPeriod, timing) {
|
|
|
450
503
|
return timing === "begin" ? 1 + ratePerPeriod : 1;
|
|
451
504
|
}
|
|
452
505
|
function futureValueFromCashFlows(ratePerPeriod, periods, payment, presentValue2, timing) {
|
|
506
|
+
if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
|
|
507
|
+
throw new RangeError("ratePerPeriod must be > -1");
|
|
508
|
+
}
|
|
453
509
|
if (ratePerPeriod === 0) return -(presentValue2 + payment * periods);
|
|
454
510
|
const growth = (1 + ratePerPeriod) ** periods;
|
|
511
|
+
if (!Number.isFinite(growth) || growth <= 0) return Number.NaN;
|
|
455
512
|
const paymentFv = payment * ((growth - 1) / ratePerPeriod) * annuityDueFactor(ratePerPeriod, timing);
|
|
456
513
|
return -(presentValue2 * growth + paymentFv);
|
|
457
514
|
}
|
|
@@ -462,7 +519,11 @@ function fv(params) {
|
|
|
462
519
|
assertFiniteNumber(payment, "payment");
|
|
463
520
|
assertFiniteNumber(presentValue2, "presentValue");
|
|
464
521
|
assertTiming(timing, "timing");
|
|
465
|
-
|
|
522
|
+
const value = futureValueFromCashFlows(ratePerPeriod, periods, payment, presentValue2, timing);
|
|
523
|
+
if (!Number.isFinite(value)) {
|
|
524
|
+
throw new RangeError("Numerical instability for given inputs");
|
|
525
|
+
}
|
|
526
|
+
return value;
|
|
466
527
|
}
|
|
467
528
|
function pv(params) {
|
|
468
529
|
const { ratePerPeriod, periods, payment = 0, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
@@ -471,10 +532,20 @@ function pv(params) {
|
|
|
471
532
|
assertFiniteNumber(payment, "payment");
|
|
472
533
|
assertFiniteNumber(futureValue2, "futureValue");
|
|
473
534
|
assertTiming(timing, "timing");
|
|
535
|
+
if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
|
|
536
|
+
throw new RangeError("ratePerPeriod must be > -1");
|
|
537
|
+
}
|
|
474
538
|
if (ratePerPeriod === 0) return -(futureValue2 + payment * periods);
|
|
475
539
|
const growth = (1 + ratePerPeriod) ** periods;
|
|
540
|
+
if (!Number.isFinite(growth) || growth <= 0) {
|
|
541
|
+
throw new RangeError("Numerical instability for given ratePerPeriod/periods");
|
|
542
|
+
}
|
|
476
543
|
const paymentPv = payment * (1 - 1 / growth) / ratePerPeriod * annuityDueFactor(ratePerPeriod, timing);
|
|
477
|
-
|
|
544
|
+
const value = -futureValue2 / growth - paymentPv;
|
|
545
|
+
if (!Number.isFinite(value)) {
|
|
546
|
+
throw new RangeError("Numerical instability for given inputs");
|
|
547
|
+
}
|
|
548
|
+
return value;
|
|
478
549
|
}
|
|
479
550
|
function pmt(params) {
|
|
480
551
|
const { ratePerPeriod, periods, presentValue: presentValue2, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
@@ -483,11 +554,21 @@ function pmt(params) {
|
|
|
483
554
|
assertFiniteNumber(presentValue2, "presentValue");
|
|
484
555
|
assertFiniteNumber(futureValue2, "futureValue");
|
|
485
556
|
assertTiming(timing, "timing");
|
|
557
|
+
if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
|
|
558
|
+
throw new RangeError("ratePerPeriod must be > -1");
|
|
559
|
+
}
|
|
486
560
|
if (ratePerPeriod === 0) return -(presentValue2 + futureValue2) / periods;
|
|
487
561
|
const growth = (1 + ratePerPeriod) ** periods;
|
|
562
|
+
if (!Number.isFinite(growth) || growth <= 0) {
|
|
563
|
+
throw new RangeError("Numerical instability for given ratePerPeriod/periods");
|
|
564
|
+
}
|
|
488
565
|
const numerator = -(futureValue2 + presentValue2 * growth) * ratePerPeriod;
|
|
489
566
|
const denominator = (growth - 1) * annuityDueFactor(ratePerPeriod, timing);
|
|
490
|
-
|
|
567
|
+
const value = numerator / denominator;
|
|
568
|
+
if (!Number.isFinite(value)) {
|
|
569
|
+
throw new RangeError("Numerical instability for given inputs");
|
|
570
|
+
}
|
|
571
|
+
return value;
|
|
491
572
|
}
|
|
492
573
|
function nper(params) {
|
|
493
574
|
const { ratePerPeriod, payment, presentValue: presentValue2, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
@@ -608,13 +689,21 @@ function findRateBracket(fn, lowerBound, upperBound) {
|
|
|
608
689
|
function loanPayment(params) {
|
|
609
690
|
const { principal, annualRate, paymentsPerYear, years } = params;
|
|
610
691
|
assertNonNegative(principal, "principal");
|
|
692
|
+
assertFiniteNumber(annualRate, "annualRate");
|
|
611
693
|
assertPositive(paymentsPerYear, "paymentsPerYear");
|
|
612
694
|
assertNonNegative(years, "years");
|
|
613
695
|
const n = Math.round(paymentsPerYear * years);
|
|
614
696
|
if (n === 0) return 0;
|
|
615
697
|
const r = annualRate / paymentsPerYear;
|
|
698
|
+
if (r <= -1) {
|
|
699
|
+
throw new RangeError("annualRate / paymentsPerYear must be > -1");
|
|
700
|
+
}
|
|
616
701
|
if (r === 0) return principal / n;
|
|
617
|
-
|
|
702
|
+
const value = r * principal / (1 - (1 + r) ** -n);
|
|
703
|
+
if (!Number.isFinite(value)) {
|
|
704
|
+
throw new RangeError("Numerical instability for given inputs");
|
|
705
|
+
}
|
|
706
|
+
return value;
|
|
618
707
|
}
|
|
619
708
|
|
|
620
709
|
// src/loans/amortizationSchedule.ts
|
|
@@ -712,13 +801,16 @@ function roundToCurrency(params) {
|
|
|
712
801
|
const factor = 10 ** d;
|
|
713
802
|
if (mode === "half-even") {
|
|
714
803
|
const scaled = value * factor;
|
|
715
|
-
const
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
804
|
+
const sign = scaled < 0 ? -1 : 1;
|
|
805
|
+
const absScaled = Math.abs(scaled);
|
|
806
|
+
const floorAbs = Math.floor(absScaled);
|
|
807
|
+
const fraction = absScaled - floorAbs;
|
|
808
|
+
const epsilon = 1e-12;
|
|
809
|
+
if (Math.abs(fraction - 0.5) <= epsilon) {
|
|
810
|
+
const nearestEven = floorAbs % 2 === 0 ? floorAbs : floorAbs + 1;
|
|
811
|
+
return sign * nearestEven / factor;
|
|
720
812
|
}
|
|
721
|
-
return
|
|
813
|
+
return Math.round(scaled) / factor;
|
|
722
814
|
}
|
|
723
815
|
return Math.round(value * factor) / factor;
|
|
724
816
|
}
|