@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/dist/index.js
CHANGED
|
@@ -185,32 +185,31 @@ function hasPositiveAndNegative(cashFlows) {
|
|
|
185
185
|
return false;
|
|
186
186
|
}
|
|
187
187
|
function findBracket(fn, lowerBound, upperBound) {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
188
|
+
const scan = (start, end, segments = 200) => {
|
|
189
|
+
let prevX;
|
|
190
|
+
let prevValue;
|
|
191
|
+
for (let i = 0; i <= segments; i++) {
|
|
192
|
+
const x = start + (end - start) * i / segments;
|
|
193
|
+
const value = fn(x);
|
|
194
|
+
if (!Number.isFinite(value)) continue;
|
|
195
|
+
if (value === 0) return { lower: x, upper: x };
|
|
196
|
+
if (prevX !== void 0 && prevValue !== void 0 && prevValue * value < 0) {
|
|
197
|
+
return { lower: prevX, upper: x };
|
|
198
|
+
}
|
|
199
|
+
prevX = x;
|
|
200
|
+
prevValue = value;
|
|
201
|
+
}
|
|
202
|
+
return void 0;
|
|
203
|
+
};
|
|
194
204
|
let lower = lowerBound;
|
|
195
205
|
let upper = upperBound;
|
|
196
|
-
let fLo = fLower;
|
|
197
|
-
let fHi = fUpper;
|
|
198
206
|
for (let i = 0; i < 20; i++) {
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return { lower: nextLower, upper };
|
|
203
|
-
}
|
|
204
|
-
if (Number.isFinite(fNextLower)) {
|
|
205
|
-
lower = nextLower;
|
|
206
|
-
fLo = fNextLower;
|
|
207
|
-
}
|
|
207
|
+
const bracket = scan(lower, upper);
|
|
208
|
+
if (bracket !== void 0) return bracket;
|
|
209
|
+
lower = Math.max(-0.999999999, (lower - 1) / 2);
|
|
208
210
|
upper = upper * 2 + 1;
|
|
209
|
-
fHi = fn(upper);
|
|
210
|
-
if (!Number.isFinite(fHi)) continue;
|
|
211
|
-
if (fLo * fHi < 0) return { lower, upper };
|
|
212
211
|
}
|
|
213
|
-
return
|
|
212
|
+
return scan(lower, upper, 400);
|
|
214
213
|
}
|
|
215
214
|
function irr(params) {
|
|
216
215
|
const {
|
|
@@ -300,6 +299,11 @@ function paymentFromPresentValue(params) {
|
|
|
300
299
|
}
|
|
301
300
|
|
|
302
301
|
// src/interest/periodsToReachGoal.ts
|
|
302
|
+
function assertContributionTiming(value, name) {
|
|
303
|
+
if (value !== "end" && value !== "begin") {
|
|
304
|
+
throw new RangeError(`${name} must be "end" or "begin"`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
303
307
|
function periodsToReachGoal(params) {
|
|
304
308
|
const {
|
|
305
309
|
principal,
|
|
@@ -307,13 +311,16 @@ function periodsToReachGoal(params) {
|
|
|
307
311
|
rate: rate2,
|
|
308
312
|
timesPerYear,
|
|
309
313
|
contributionPerPeriod = 0,
|
|
310
|
-
contributionTiming = "end"
|
|
314
|
+
contributionTiming = "end",
|
|
315
|
+
maxPeriods = 1e5
|
|
311
316
|
} = params;
|
|
312
317
|
assertNonNegative(principal, "principal");
|
|
313
318
|
assertNonNegative(targetFutureValue, "targetFutureValue");
|
|
314
319
|
assertFiniteNumber(rate2, "rate");
|
|
315
320
|
assertPositive(timesPerYear, "timesPerYear");
|
|
316
321
|
assertNonNegative(contributionPerPeriod, "contributionPerPeriod");
|
|
322
|
+
assertPositive(maxPeriods, "maxPeriods");
|
|
323
|
+
assertContributionTiming(contributionTiming, "contributionTiming");
|
|
317
324
|
if (targetFutureValue <= principal) return 0;
|
|
318
325
|
const r = rate2 / timesPerYear;
|
|
319
326
|
if (r <= -1) {
|
|
@@ -333,13 +340,13 @@ function periodsToReachGoal(params) {
|
|
|
333
340
|
}
|
|
334
341
|
let fv2 = principal;
|
|
335
342
|
let periods = 0;
|
|
336
|
-
const maxPeriods = 1e4;
|
|
337
343
|
while (fv2 < targetFutureValue && periods < maxPeriods) {
|
|
338
344
|
if (contributionTiming === "begin") fv2 += contributionPerPeriod;
|
|
339
345
|
fv2 = fv2 * (1 + r) + (contributionTiming === "end" ? contributionPerPeriod : 0);
|
|
340
346
|
periods++;
|
|
341
347
|
}
|
|
342
|
-
|
|
348
|
+
if (fv2 >= targetFutureValue) return periods;
|
|
349
|
+
throw new RangeError("maxPeriods exceeded before reaching target");
|
|
343
350
|
}
|
|
344
351
|
|
|
345
352
|
// src/interest/presentValue.ts
|
|
@@ -369,22 +376,46 @@ function presentValueOfAnnuity(params) {
|
|
|
369
376
|
}
|
|
370
377
|
|
|
371
378
|
// src/interest/rateToReachGoal.ts
|
|
379
|
+
function assertContributionTiming2(value, name) {
|
|
380
|
+
if (value !== "end" && value !== "begin") {
|
|
381
|
+
throw new RangeError(`${name} must be "end" or "begin"`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
372
384
|
function rateToReachGoal(params) {
|
|
373
385
|
const {
|
|
374
386
|
principal,
|
|
375
387
|
targetFutureValue,
|
|
376
388
|
periods,
|
|
377
389
|
contributionPerPeriod = 0,
|
|
378
|
-
contributionTiming = "end"
|
|
390
|
+
contributionTiming = "end",
|
|
391
|
+
lowerBound = -0.99,
|
|
392
|
+
upperBound = 10,
|
|
393
|
+
maxIterations = 100,
|
|
394
|
+
tolerance = 1e-10
|
|
379
395
|
} = params;
|
|
380
396
|
assertNonNegative(principal, "principal");
|
|
381
397
|
assertNonNegative(targetFutureValue, "targetFutureValue");
|
|
382
398
|
assertPositive(periods, "periods");
|
|
383
399
|
assertNonNegative(contributionPerPeriod, "contributionPerPeriod");
|
|
384
|
-
|
|
400
|
+
assertFiniteNumber(lowerBound, "lowerBound");
|
|
401
|
+
assertFiniteNumber(upperBound, "upperBound");
|
|
402
|
+
assertFiniteNumber(maxIterations, "maxIterations");
|
|
403
|
+
assertFiniteNumber(tolerance, "tolerance");
|
|
404
|
+
assertContributionTiming2(contributionTiming, "contributionTiming");
|
|
405
|
+
if (lowerBound <= -1) throw new RangeError("lowerBound must be > -1");
|
|
406
|
+
if (upperBound <= lowerBound) throw new RangeError("upperBound must be greater than lowerBound");
|
|
407
|
+
if (!Number.isInteger(maxIterations) || maxIterations <= 0) {
|
|
408
|
+
throw new RangeError("maxIterations must be a positive integer");
|
|
409
|
+
}
|
|
410
|
+
if (tolerance <= 0) throw new RangeError("tolerance must be > 0");
|
|
385
411
|
if (targetFutureValue <= principal && contributionPerPeriod === 0) return 0;
|
|
386
412
|
if (contributionPerPeriod === 0) {
|
|
387
413
|
if (targetFutureValue <= principal) return 0;
|
|
414
|
+
if (principal === 0) {
|
|
415
|
+
throw new RangeError(
|
|
416
|
+
"rateToReachGoal is undefined when principal is 0 and contributions are 0"
|
|
417
|
+
);
|
|
418
|
+
}
|
|
388
419
|
return (targetFutureValue / principal) ** (1 / periods) - 1;
|
|
389
420
|
}
|
|
390
421
|
const dueFactor = contributionTiming === "begin" ? 1 : 0;
|
|
@@ -402,23 +433,56 @@ function rateToReachGoal(params) {
|
|
|
402
433
|
};
|
|
403
434
|
const initialGuess = (targetFutureValue / (principal + contributionPerPeriod * periods)) ** (1 / periods) - 1;
|
|
404
435
|
const newton = newtonRaphson({
|
|
405
|
-
initialGuess: Number.isFinite(initialGuess) ? Math.max(initialGuess,
|
|
436
|
+
initialGuess: Number.isFinite(initialGuess) ? Math.max(initialGuess, lowerBound) : 0.01,
|
|
406
437
|
fn,
|
|
407
438
|
derivative,
|
|
408
|
-
tolerance
|
|
409
|
-
maxIterations
|
|
410
|
-
min:
|
|
411
|
-
max:
|
|
439
|
+
tolerance,
|
|
440
|
+
maxIterations,
|
|
441
|
+
min: lowerBound,
|
|
442
|
+
max: upperBound
|
|
412
443
|
});
|
|
413
444
|
if (newton !== void 0) return newton;
|
|
445
|
+
const bracket = findBracket2(fn, lowerBound, upperBound);
|
|
446
|
+
if (bracket === void 0) {
|
|
447
|
+
throw new RangeError("rateToReachGoal did not converge within search bounds");
|
|
448
|
+
}
|
|
449
|
+
if (bracket.lower === bracket.upper) return bracket.lower;
|
|
414
450
|
const bisected = bisection({
|
|
415
451
|
fn,
|
|
416
|
-
lower:
|
|
417
|
-
upper:
|
|
418
|
-
tolerance
|
|
419
|
-
maxIterations:
|
|
452
|
+
lower: bracket.lower,
|
|
453
|
+
upper: bracket.upper,
|
|
454
|
+
tolerance,
|
|
455
|
+
maxIterations: maxIterations * 2
|
|
420
456
|
});
|
|
421
|
-
|
|
457
|
+
if (bisected !== void 0) return bisected;
|
|
458
|
+
throw new RangeError("rateToReachGoal did not converge");
|
|
459
|
+
}
|
|
460
|
+
function findBracket2(fn, lowerBound, upperBound) {
|
|
461
|
+
const scan = (start, end, segments = 200) => {
|
|
462
|
+
let prevX;
|
|
463
|
+
let prevValue;
|
|
464
|
+
for (let i = 0; i <= segments; i++) {
|
|
465
|
+
const x = start + (end - start) * i / segments;
|
|
466
|
+
const value = fn(x);
|
|
467
|
+
if (!Number.isFinite(value)) continue;
|
|
468
|
+
if (value === 0) return { lower: x, upper: x };
|
|
469
|
+
if (prevX !== void 0 && prevValue !== void 0 && prevValue * value < 0) {
|
|
470
|
+
return { lower: prevX, upper: x };
|
|
471
|
+
}
|
|
472
|
+
prevX = x;
|
|
473
|
+
prevValue = value;
|
|
474
|
+
}
|
|
475
|
+
return void 0;
|
|
476
|
+
};
|
|
477
|
+
let lower = lowerBound;
|
|
478
|
+
let upper = upperBound;
|
|
479
|
+
for (let i = 0; i < 20; i++) {
|
|
480
|
+
const bracket = scan(lower, upper);
|
|
481
|
+
if (bracket !== void 0) return bracket;
|
|
482
|
+
lower = Math.max(-0.999999999, (lower - 1) / 2);
|
|
483
|
+
upper = upper * 2 + 1;
|
|
484
|
+
}
|
|
485
|
+
return scan(lower, upper, 400);
|
|
422
486
|
}
|
|
423
487
|
|
|
424
488
|
// src/interest/realReturn.ts
|
|
@@ -448,8 +512,12 @@ function annuityDueFactor(ratePerPeriod, timing) {
|
|
|
448
512
|
return timing === "begin" ? 1 + ratePerPeriod : 1;
|
|
449
513
|
}
|
|
450
514
|
function futureValueFromCashFlows(ratePerPeriod, periods, payment, presentValue2, timing) {
|
|
515
|
+
if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
|
|
516
|
+
throw new RangeError("ratePerPeriod must be > -1");
|
|
517
|
+
}
|
|
451
518
|
if (ratePerPeriod === 0) return -(presentValue2 + payment * periods);
|
|
452
519
|
const growth = (1 + ratePerPeriod) ** periods;
|
|
520
|
+
if (!Number.isFinite(growth) || growth <= 0) return Number.NaN;
|
|
453
521
|
const paymentFv = payment * ((growth - 1) / ratePerPeriod) * annuityDueFactor(ratePerPeriod, timing);
|
|
454
522
|
return -(presentValue2 * growth + paymentFv);
|
|
455
523
|
}
|
|
@@ -460,7 +528,11 @@ function fv(params) {
|
|
|
460
528
|
assertFiniteNumber(payment, "payment");
|
|
461
529
|
assertFiniteNumber(presentValue2, "presentValue");
|
|
462
530
|
assertTiming(timing, "timing");
|
|
463
|
-
|
|
531
|
+
const value = futureValueFromCashFlows(ratePerPeriod, periods, payment, presentValue2, timing);
|
|
532
|
+
if (!Number.isFinite(value)) {
|
|
533
|
+
throw new RangeError("Numerical instability for given inputs");
|
|
534
|
+
}
|
|
535
|
+
return value;
|
|
464
536
|
}
|
|
465
537
|
function pv(params) {
|
|
466
538
|
const { ratePerPeriod, periods, payment = 0, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
@@ -469,10 +541,20 @@ function pv(params) {
|
|
|
469
541
|
assertFiniteNumber(payment, "payment");
|
|
470
542
|
assertFiniteNumber(futureValue2, "futureValue");
|
|
471
543
|
assertTiming(timing, "timing");
|
|
544
|
+
if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
|
|
545
|
+
throw new RangeError("ratePerPeriod must be > -1");
|
|
546
|
+
}
|
|
472
547
|
if (ratePerPeriod === 0) return -(futureValue2 + payment * periods);
|
|
473
548
|
const growth = (1 + ratePerPeriod) ** periods;
|
|
549
|
+
if (!Number.isFinite(growth) || growth <= 0) {
|
|
550
|
+
throw new RangeError("Numerical instability for given ratePerPeriod/periods");
|
|
551
|
+
}
|
|
474
552
|
const paymentPv = payment * (1 - 1 / growth) / ratePerPeriod * annuityDueFactor(ratePerPeriod, timing);
|
|
475
|
-
|
|
553
|
+
const value = -futureValue2 / growth - paymentPv;
|
|
554
|
+
if (!Number.isFinite(value)) {
|
|
555
|
+
throw new RangeError("Numerical instability for given inputs");
|
|
556
|
+
}
|
|
557
|
+
return value;
|
|
476
558
|
}
|
|
477
559
|
function pmt(params) {
|
|
478
560
|
const { ratePerPeriod, periods, presentValue: presentValue2, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
@@ -481,11 +563,21 @@ function pmt(params) {
|
|
|
481
563
|
assertFiniteNumber(presentValue2, "presentValue");
|
|
482
564
|
assertFiniteNumber(futureValue2, "futureValue");
|
|
483
565
|
assertTiming(timing, "timing");
|
|
566
|
+
if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
|
|
567
|
+
throw new RangeError("ratePerPeriod must be > -1");
|
|
568
|
+
}
|
|
484
569
|
if (ratePerPeriod === 0) return -(presentValue2 + futureValue2) / periods;
|
|
485
570
|
const growth = (1 + ratePerPeriod) ** periods;
|
|
571
|
+
if (!Number.isFinite(growth) || growth <= 0) {
|
|
572
|
+
throw new RangeError("Numerical instability for given ratePerPeriod/periods");
|
|
573
|
+
}
|
|
486
574
|
const numerator = -(futureValue2 + presentValue2 * growth) * ratePerPeriod;
|
|
487
575
|
const denominator = (growth - 1) * annuityDueFactor(ratePerPeriod, timing);
|
|
488
|
-
|
|
576
|
+
const value = numerator / denominator;
|
|
577
|
+
if (!Number.isFinite(value)) {
|
|
578
|
+
throw new RangeError("Numerical instability for given inputs");
|
|
579
|
+
}
|
|
580
|
+
return value;
|
|
489
581
|
}
|
|
490
582
|
function nper(params) {
|
|
491
583
|
const { ratePerPeriod, payment, presentValue: presentValue2, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
@@ -606,13 +698,21 @@ function findRateBracket(fn, lowerBound, upperBound) {
|
|
|
606
698
|
function loanPayment(params) {
|
|
607
699
|
const { principal, annualRate, paymentsPerYear, years } = params;
|
|
608
700
|
assertNonNegative(principal, "principal");
|
|
701
|
+
assertFiniteNumber(annualRate, "annualRate");
|
|
609
702
|
assertPositive(paymentsPerYear, "paymentsPerYear");
|
|
610
703
|
assertNonNegative(years, "years");
|
|
611
704
|
const n = Math.round(paymentsPerYear * years);
|
|
612
705
|
if (n === 0) return 0;
|
|
613
706
|
const r = annualRate / paymentsPerYear;
|
|
707
|
+
if (r <= -1) {
|
|
708
|
+
throw new RangeError("annualRate / paymentsPerYear must be > -1");
|
|
709
|
+
}
|
|
614
710
|
if (r === 0) return principal / n;
|
|
615
|
-
|
|
711
|
+
const value = r * principal / (1 - (1 + r) ** -n);
|
|
712
|
+
if (!Number.isFinite(value)) {
|
|
713
|
+
throw new RangeError("Numerical instability for given inputs");
|
|
714
|
+
}
|
|
715
|
+
return value;
|
|
616
716
|
}
|
|
617
717
|
|
|
618
718
|
// src/loans/amortizationSchedule.ts
|
|
@@ -710,13 +810,16 @@ function roundToCurrency(params) {
|
|
|
710
810
|
const factor = 10 ** d;
|
|
711
811
|
if (mode === "half-even") {
|
|
712
812
|
const scaled = value * factor;
|
|
713
|
-
const
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
813
|
+
const sign = scaled < 0 ? -1 : 1;
|
|
814
|
+
const absScaled = Math.abs(scaled);
|
|
815
|
+
const floorAbs = Math.floor(absScaled);
|
|
816
|
+
const fraction = absScaled - floorAbs;
|
|
817
|
+
const epsilon = 1e-12;
|
|
818
|
+
if (Math.abs(fraction - 0.5) <= epsilon) {
|
|
819
|
+
const nearestEven = floorAbs % 2 === 0 ? floorAbs : floorAbs + 1;
|
|
820
|
+
return sign * nearestEven / factor;
|
|
718
821
|
}
|
|
719
|
-
return
|
|
822
|
+
return Math.round(scaled) / factor;
|
|
720
823
|
}
|
|
721
824
|
return Math.round(value * factor) / factor;
|
|
722
825
|
}
|