@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/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 fLower = fn(lowerBound);
189
- const fUpper = fn(upperBound);
190
- if (!Number.isFinite(fLower) || !Number.isFinite(fUpper)) return void 0;
191
- if (fLower === 0) return { lower: lowerBound, upper: lowerBound };
192
- if (fUpper === 0) return { lower: upperBound, upper: upperBound };
193
- if (fLower * fUpper < 0) return { lower: lowerBound, upper: upperBound };
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 nextLower = Math.max(-0.999999999, (lower - 1) / 2);
200
- const fNextLower = fn(nextLower);
201
- if (Number.isFinite(fNextLower) && fNextLower * fHi < 0) {
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 void 0;
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
- return fv2 >= targetFutureValue ? periods : Number.POSITIVE_INFINITY;
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
- if (periods === 0) return 0;
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, -0.99) : 0.01,
436
+ initialGuess: Number.isFinite(initialGuess) ? Math.max(initialGuess, lowerBound) : 0.01,
406
437
  fn,
407
438
  derivative,
408
- tolerance: 1e-10,
409
- maxIterations: 100,
410
- min: -0.99,
411
- max: 10
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: -0.99,
417
- upper: 10,
418
- tolerance: 1e-10,
419
- maxIterations: 200
452
+ lower: bracket.lower,
453
+ upper: bracket.upper,
454
+ tolerance,
455
+ maxIterations: maxIterations * 2
420
456
  });
421
- return bisected ?? Number.NaN;
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
- return futureValueFromCashFlows(ratePerPeriod, periods, payment, presentValue2, timing);
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
- return -futureValue2 / growth - paymentPv;
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
- return numerator / denominator;
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
- return r * principal / (1 - (1 + r) ** -n);
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 rounded = Math.round(scaled);
714
- const remainder = Math.abs(scaled - rounded);
715
- if (remainder === 0.5) {
716
- const down = Math.floor(scaled);
717
- return (down % 2 === 0 ? down : down + 1) / factor;
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 rounded / factor;
822
+ return Math.round(scaled) / factor;
720
823
  }
721
824
  return Math.round(value * factor) / factor;
722
825
  }