@pyverret/ratejs 1.0.0 → 1.1.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/dist/index.cjs CHANGED
@@ -30,14 +30,14 @@ function cagr(params) {
30
30
 
31
31
  // src/interest/compound.ts
32
32
  function compound(params) {
33
- const { principal, rate, timesPerYear, years } = params;
33
+ const { principal, rate: rate2, timesPerYear, years } = params;
34
34
  assertNonNegative(principal, "principal");
35
35
  assertPositive(timesPerYear, "timesPerYear");
36
36
  assertNonNegative(years, "years");
37
- if (rate === 0 || years === 0) return principal;
37
+ if (rate2 === 0 || years === 0) return principal;
38
38
  const n = timesPerYear;
39
39
  const t = years;
40
- return principal * (1 + rate / n) ** (n * t);
40
+ return principal * (1 + rate2 / n) ** (n * t);
41
41
  }
42
42
 
43
43
  // src/interest/effectiveAnnualRate.ts
@@ -51,14 +51,14 @@ function effectiveAnnualRate(params) {
51
51
 
52
52
  // src/interest/futureValue.ts
53
53
  function futureValue(params) {
54
- const { presentValue: presentValue2, rate, timesPerYear, years } = params;
54
+ const { presentValue: presentValue2, rate: rate2, timesPerYear, years } = params;
55
55
  assertNonNegative(presentValue2, "presentValue");
56
56
  assertPositive(timesPerYear, "timesPerYear");
57
57
  assertNonNegative(years, "years");
58
- if (rate === 0 || years === 0) return presentValue2;
58
+ if (rate2 === 0 || years === 0) return presentValue2;
59
59
  const n = timesPerYear;
60
60
  const t = years;
61
- return presentValue2 * (1 + rate / n) ** (n * t);
61
+ return presentValue2 * (1 + rate2 / n) ** (n * t);
62
62
  }
63
63
 
64
64
  // src/interest/inflationAdjustedAmount.ts
@@ -76,7 +76,7 @@ function investmentGrowth(params) {
76
76
  const {
77
77
  initial,
78
78
  contributionPerPeriod = 0,
79
- rate,
79
+ rate: rate2,
80
80
  timesPerYear,
81
81
  years,
82
82
  contributionTiming = "end"
@@ -93,11 +93,11 @@ function investmentGrowth(params) {
93
93
  totalInterest: 0
94
94
  };
95
95
  }
96
- const r = rate / timesPerYear;
97
- const fvInitial = rate === 0 ? initial : initial * (1 + r) ** periods;
96
+ const r = rate2 / timesPerYear;
97
+ const fvInitial = rate2 === 0 ? initial : initial * (1 + r) ** periods;
98
98
  let fvContrib = 0;
99
99
  if (contributionPerPeriod !== 0) {
100
- if (rate === 0) {
100
+ if (rate2 === 0) {
101
101
  fvContrib = contributionPerPeriod * periods;
102
102
  } else {
103
103
  const ordinary = contributionPerPeriod * (((1 + r) ** periods - 1) / r);
@@ -110,38 +110,180 @@ function investmentGrowth(params) {
110
110
  return { futureValue: futureValue2, totalContributions, totalInterest };
111
111
  }
112
112
 
113
+ // src/utils/solvers.ts
114
+ function newtonRaphson(params) {
115
+ const {
116
+ initialGuess,
117
+ fn,
118
+ derivative,
119
+ tolerance = 1e-10,
120
+ maxIterations = 100,
121
+ min = Number.NEGATIVE_INFINITY,
122
+ max = Number.POSITIVE_INFINITY
123
+ } = params;
124
+ let x = initialGuess;
125
+ for (let i = 0; i < maxIterations; i++) {
126
+ const value = fn(x);
127
+ if (!Number.isFinite(value)) return void 0;
128
+ if (Math.abs(value) <= tolerance) return x;
129
+ const slope = derivative(x);
130
+ if (!Number.isFinite(slope) || Math.abs(slope) < 1e-14) return void 0;
131
+ x -= value / slope;
132
+ if (x < min) x = min;
133
+ if (x > max) x = max;
134
+ }
135
+ return void 0;
136
+ }
137
+ function bisection(params) {
138
+ const { fn, lower, upper, tolerance = 1e-10, maxIterations = 200 } = params;
139
+ let a = lower;
140
+ let b = upper;
141
+ let fa = fn(a);
142
+ let fb = fn(b);
143
+ if (!Number.isFinite(fa) || !Number.isFinite(fb) || fa * fb > 0) return void 0;
144
+ if (Math.abs(fa) <= tolerance) return a;
145
+ if (Math.abs(fb) <= tolerance) return b;
146
+ for (let i = 0; i < maxIterations; i++) {
147
+ const c = (a + b) / 2;
148
+ const fc = fn(c);
149
+ if (!Number.isFinite(fc)) return void 0;
150
+ if (Math.abs(fc) <= tolerance || Math.abs(b - a) <= tolerance) return c;
151
+ if (fa * fc < 0) {
152
+ b = c;
153
+ fb = fc;
154
+ } else {
155
+ a = c;
156
+ fa = fc;
157
+ }
158
+ }
159
+ return (a + b) / 2;
160
+ }
161
+
113
162
  // src/interest/irr.ts
114
- function npv(rate, cashFlows) {
163
+ function npvAt(rate2, cashFlows) {
115
164
  let sum = 0;
116
165
  for (let t = 0; t < cashFlows.length; t++) {
117
166
  const cf = cashFlows[t];
118
- if (cf !== void 0) sum += cf / (1 + rate) ** t;
167
+ if (cf !== void 0) sum += cf / (1 + rate2) ** t;
119
168
  }
120
169
  return sum;
121
170
  }
122
- function npvDerivative(rate, cashFlows) {
171
+ function npvDerivativeAt(rate2, cashFlows) {
123
172
  let sum = 0;
124
173
  for (let t = 1; t < cashFlows.length; t++) {
125
174
  const cf = cashFlows[t];
126
- if (cf !== void 0) sum -= t * cf / (1 + rate) ** (t + 1);
175
+ if (cf !== void 0) sum -= t * cf / (1 + rate2) ** (t + 1);
127
176
  }
128
177
  return sum;
129
178
  }
179
+ function hasPositiveAndNegative(cashFlows) {
180
+ let hasPositive = false;
181
+ let hasNegative = false;
182
+ for (const cf of cashFlows) {
183
+ if (cf > 0) hasPositive = true;
184
+ if (cf < 0) hasNegative = true;
185
+ if (hasPositive && hasNegative) return true;
186
+ }
187
+ return false;
188
+ }
189
+ function findBracket(fn, lowerBound, upperBound) {
190
+ const fLower = fn(lowerBound);
191
+ const fUpper = fn(upperBound);
192
+ if (!Number.isFinite(fLower) || !Number.isFinite(fUpper)) return void 0;
193
+ if (fLower === 0) return { lower: lowerBound, upper: lowerBound };
194
+ if (fUpper === 0) return { lower: upperBound, upper: upperBound };
195
+ if (fLower * fUpper < 0) return { lower: lowerBound, upper: upperBound };
196
+ let lower = lowerBound;
197
+ let upper = upperBound;
198
+ let fLo = fLower;
199
+ let fHi = fUpper;
200
+ for (let i = 0; i < 20; i++) {
201
+ const nextLower = Math.max(-0.999999999, (lower - 1) / 2);
202
+ const fNextLower = fn(nextLower);
203
+ if (Number.isFinite(fNextLower) && fNextLower * fHi < 0) {
204
+ return { lower: nextLower, upper };
205
+ }
206
+ if (Number.isFinite(fNextLower)) {
207
+ lower = nextLower;
208
+ fLo = fNextLower;
209
+ }
210
+ upper = upper * 2 + 1;
211
+ fHi = fn(upper);
212
+ if (!Number.isFinite(fHi)) continue;
213
+ if (fLo * fHi < 0) return { lower, upper };
214
+ }
215
+ return void 0;
216
+ }
130
217
  function irr(params) {
131
- const { cashFlows, guess = 0.1, maxIterations = 100 } = params;
132
- if (cashFlows.length === 0) return NaN;
218
+ const {
219
+ cashFlows,
220
+ guess = 0.1,
221
+ maxIterations = 100,
222
+ lowerBound = -0.99,
223
+ upperBound = 10
224
+ } = params;
225
+ if (cashFlows.length === 0) {
226
+ throw new RangeError("cashFlows must contain at least one value");
227
+ }
228
+ assertFiniteNumber(guess, "guess");
229
+ assertFiniteNumber(maxIterations, "maxIterations");
230
+ assertFiniteNumber(lowerBound, "lowerBound");
231
+ assertFiniteNumber(upperBound, "upperBound");
232
+ if (maxIterations <= 0 || !Number.isInteger(maxIterations)) {
233
+ throw new RangeError("maxIterations must be a positive integer");
234
+ }
235
+ if (lowerBound <= -1) {
236
+ throw new RangeError("lowerBound must be > -1");
237
+ }
238
+ if (upperBound <= lowerBound) {
239
+ throw new RangeError("upperBound must be greater than lowerBound");
240
+ }
133
241
  for (const cf of cashFlows) assertFiniteNumber(cf, "cashFlows[]");
134
- let r = guess;
135
- for (let i = 0; i < maxIterations; i++) {
136
- const val = npv(r, cashFlows);
137
- if (Math.abs(val) < 1e-10) return r;
138
- const der = npvDerivative(r, cashFlows);
139
- if (!Number.isFinite(der) || Math.abs(der) < 1e-15) break;
140
- r = r - val / der;
141
- if (r < -0.99) r = -0.99;
142
- if (r > 10) r = 10;
242
+ if (!hasPositiveAndNegative(cashFlows)) {
243
+ throw new RangeError("cashFlows must include at least one positive and one negative value");
143
244
  }
144
- return r;
245
+ const fn = (rate2) => npvAt(rate2, cashFlows);
246
+ const derivative = (rate2) => npvDerivativeAt(rate2, cashFlows);
247
+ const newton = newtonRaphson({
248
+ initialGuess: guess,
249
+ fn,
250
+ derivative,
251
+ tolerance: 1e-10,
252
+ maxIterations,
253
+ min: lowerBound,
254
+ max: upperBound
255
+ });
256
+ if (newton !== void 0) return newton;
257
+ const bracket = findBracket(fn, lowerBound, upperBound);
258
+ if (bracket === void 0) {
259
+ throw new RangeError("IRR did not converge within search bounds");
260
+ }
261
+ if (bracket.lower === bracket.upper) return bracket.lower;
262
+ const bisected = bisection({
263
+ fn,
264
+ lower: bracket.lower,
265
+ upper: bracket.upper,
266
+ tolerance: 1e-10,
267
+ maxIterations: maxIterations * 2
268
+ });
269
+ if (bisected !== void 0) return bisected;
270
+ throw new RangeError("IRR did not converge");
271
+ }
272
+
273
+ // src/interest/npv.ts
274
+ function npv(params) {
275
+ const { rate: rate2, cashFlows } = params;
276
+ assertFiniteNumber(rate2, "rate");
277
+ if (rate2 <= -1) throw new RangeError("rate must be > -1");
278
+ if (cashFlows.length === 0) return 0;
279
+ let sum = 0;
280
+ for (let t = 0; t < cashFlows.length; t++) {
281
+ const cf = cashFlows[t];
282
+ if (cf === void 0) continue;
283
+ assertFiniteNumber(cf, `cashFlows[${t}]`);
284
+ sum += cf / (1 + rate2) ** t;
285
+ }
286
+ return sum;
145
287
  }
146
288
 
147
289
  // src/interest/paymentFromPresentValue.ts
@@ -151,12 +293,12 @@ function paymentFromPresentValue(params) {
151
293
  assertNonNegative(periods, "periods");
152
294
  if (presentValue2 === 0 || periods === 0) return 0;
153
295
  if (ratePerPeriod === 0) {
154
- const pmt2 = presentValue2 / periods;
155
- return timing === "begin" ? pmt2 / (1 + ratePerPeriod) : pmt2;
296
+ const pmt3 = presentValue2 / periods;
297
+ return timing === "begin" ? pmt3 / (1 + ratePerPeriod) : pmt3;
156
298
  }
157
- let pmt = ratePerPeriod * presentValue2 / (1 - (1 + ratePerPeriod) ** -periods);
158
- if (timing === "begin") pmt /= 1 + ratePerPeriod;
159
- return pmt;
299
+ let pmt2 = ratePerPeriod * presentValue2 / (1 - (1 + ratePerPeriod) ** -periods);
300
+ if (timing === "begin") pmt2 /= 1 + ratePerPeriod;
301
+ return pmt2;
160
302
  }
161
303
 
162
304
  // src/interest/periodsToReachGoal.ts
@@ -164,54 +306,54 @@ function periodsToReachGoal(params) {
164
306
  const {
165
307
  principal,
166
308
  targetFutureValue,
167
- rate,
309
+ rate: rate2,
168
310
  timesPerYear,
169
311
  contributionPerPeriod = 0,
170
312
  contributionTiming = "end"
171
313
  } = params;
172
314
  assertNonNegative(principal, "principal");
173
315
  assertNonNegative(targetFutureValue, "targetFutureValue");
174
- assertFiniteNumber(rate, "rate");
316
+ assertFiniteNumber(rate2, "rate");
175
317
  assertPositive(timesPerYear, "timesPerYear");
176
318
  assertNonNegative(contributionPerPeriod, "contributionPerPeriod");
177
319
  if (targetFutureValue <= principal) return 0;
178
- const r = rate / timesPerYear;
320
+ const r = rate2 / timesPerYear;
179
321
  if (r <= -1) {
180
322
  throw new RangeError("rate / timesPerYear must be > -1");
181
323
  }
182
324
  if (contributionPerPeriod === 0) {
183
325
  if (principal === 0) return Number.POSITIVE_INFINITY;
184
- if (rate === 0) return targetFutureValue <= principal ? 0 : Number.POSITIVE_INFINITY;
326
+ if (rate2 === 0) return targetFutureValue <= principal ? 0 : Number.POSITIVE_INFINITY;
185
327
  if (r < 0) return Number.POSITIVE_INFINITY;
186
328
  const n = Math.log(targetFutureValue / principal) / Math.log(1 + r);
187
329
  return Number.isFinite(n) && n >= 0 ? Math.ceil(n) : Number.POSITIVE_INFINITY;
188
330
  }
189
- if (rate === 0) {
331
+ if (rate2 === 0) {
190
332
  const needed = targetFutureValue - principal;
191
333
  if (needed <= 0) return 0;
192
334
  return Math.ceil(needed / contributionPerPeriod);
193
335
  }
194
- let fv = principal;
336
+ let fv2 = principal;
195
337
  let periods = 0;
196
338
  const maxPeriods = 1e4;
197
- while (fv < targetFutureValue && periods < maxPeriods) {
198
- if (contributionTiming === "begin") fv += contributionPerPeriod;
199
- fv = fv * (1 + r) + (contributionTiming === "end" ? contributionPerPeriod : 0);
339
+ while (fv2 < targetFutureValue && periods < maxPeriods) {
340
+ if (contributionTiming === "begin") fv2 += contributionPerPeriod;
341
+ fv2 = fv2 * (1 + r) + (contributionTiming === "end" ? contributionPerPeriod : 0);
200
342
  periods++;
201
343
  }
202
- return fv >= targetFutureValue ? periods : Number.POSITIVE_INFINITY;
344
+ return fv2 >= targetFutureValue ? periods : Number.POSITIVE_INFINITY;
203
345
  }
204
346
 
205
347
  // src/interest/presentValue.ts
206
348
  function presentValue(params) {
207
- const { futureValue: fv, rate, timesPerYear, years } = params;
208
- assertNonNegative(fv, "futureValue");
349
+ const { futureValue: fv2, rate: rate2, timesPerYear, years } = params;
350
+ assertNonNegative(fv2, "futureValue");
209
351
  assertPositive(timesPerYear, "timesPerYear");
210
352
  assertNonNegative(years, "years");
211
- if (rate === 0 || years === 0) return fv;
353
+ if (rate2 === 0 || years === 0) return fv2;
212
354
  const n = timesPerYear;
213
355
  const t = years;
214
- return fv / (1 + rate / n) ** (n * t);
356
+ return fv2 / (1 + rate2 / n) ** (n * t);
215
357
  }
216
358
 
217
359
  // src/interest/presentValueOfAnnuity.ts
@@ -221,11 +363,11 @@ function presentValueOfAnnuity(params) {
221
363
  assertNonNegative(periods, "periods");
222
364
  if (periods === 0) return 0;
223
365
  if (ratePerPeriod === 0) {
224
- const pv2 = paymentPerPeriod * periods;
225
- return timing === "begin" ? pv2 * (1 + ratePerPeriod) : pv2;
366
+ const pv3 = paymentPerPeriod * periods;
367
+ return timing === "begin" ? pv3 * (1 + ratePerPeriod) : pv3;
226
368
  }
227
- const pv = paymentPerPeriod * (1 - (1 + ratePerPeriod) ** -periods) / ratePerPeriod;
228
- return timing === "begin" ? pv * (1 + ratePerPeriod) : pv;
369
+ const pv2 = paymentPerPeriod * (1 - (1 + ratePerPeriod) ** -periods) / ratePerPeriod;
370
+ return timing === "begin" ? pv2 * (1 + ratePerPeriod) : pv2;
229
371
  }
230
372
 
231
373
  // src/interest/rateToReachGoal.ts
@@ -248,27 +390,37 @@ function rateToReachGoal(params) {
248
390
  return (targetFutureValue / principal) ** (1 / periods) - 1;
249
391
  }
250
392
  const dueFactor = contributionTiming === "begin" ? 1 : 0;
251
- let r = (targetFutureValue / (principal + contributionPerPeriod * periods)) ** (1 / periods) - 1;
252
- if (r <= -1) r = 0.01;
253
- for (let i = 0; i < 100; i++) {
393
+ const fn = (r) => {
254
394
  const onePlusR = 1 + r;
255
395
  const onePlusRN = onePlusR ** periods;
256
396
  const fvLump = principal * onePlusRN;
257
- const annuity = contributionPerPeriod * ((onePlusRN - 1) / (r || 1e-14)) * (1 + dueFactor * r);
258
- const fv = fvLump + annuity;
259
- const err = fv - targetFutureValue;
260
- if (Math.abs(err) < 1e-10) return r;
261
- const dr = r * 1e-6 || 1e-10;
262
- const r2 = r + dr;
263
- const onePlusR2 = 1 + r2;
264
- const onePlusR2N = onePlusR2 ** periods;
265
- const fv2 = principal * onePlusR2N + contributionPerPeriod * ((onePlusR2N - 1) / r2) * (1 + dueFactor * r2);
266
- const dFvDr = (fv2 - fv) / dr;
267
- r = r - err / dFvDr;
268
- if (r <= -1) r = 0.01;
269
- if (r > 10) r = 10;
270
- }
271
- return r;
397
+ const annuityBase = r === 0 ? periods : (onePlusRN - 1) / r;
398
+ const fvAnnuity = contributionPerPeriod * annuityBase * (1 + dueFactor * r);
399
+ return fvLump + fvAnnuity - targetFutureValue;
400
+ };
401
+ const derivative = (r) => {
402
+ const delta = Math.abs(r) > 1e-6 ? Math.abs(r) * 1e-6 : 1e-6;
403
+ return (fn(r + delta) - fn(r - delta)) / (2 * delta);
404
+ };
405
+ const initialGuess = (targetFutureValue / (principal + contributionPerPeriod * periods)) ** (1 / periods) - 1;
406
+ const newton = newtonRaphson({
407
+ initialGuess: Number.isFinite(initialGuess) ? Math.max(initialGuess, -0.99) : 0.01,
408
+ fn,
409
+ derivative,
410
+ tolerance: 1e-10,
411
+ maxIterations: 100,
412
+ min: -0.99,
413
+ max: 10
414
+ });
415
+ if (newton !== void 0) return newton;
416
+ const bisected = bisection({
417
+ fn,
418
+ lower: -0.99,
419
+ upper: 10,
420
+ tolerance: 1e-10,
421
+ maxIterations: 200
422
+ });
423
+ return bisected ?? Number.NaN;
272
424
  }
273
425
 
274
426
  // src/interest/realReturn.ts
@@ -282,10 +434,174 @@ function realReturn(params) {
282
434
 
283
435
  // src/interest/ruleOf72.ts
284
436
  function ruleOf72(params) {
285
- const { rate, constant = 72 } = params;
286
- assertFiniteNumber(rate, "rate");
287
- assertPositive(rate, "rate");
288
- return constant / 100 / rate;
437
+ const { rate: rate2, constant = 72 } = params;
438
+ assertFiniteNumber(rate2, "rate");
439
+ assertPositive(rate2, "rate");
440
+ return constant / 100 / rate2;
441
+ }
442
+
443
+ // src/interest/tvom.ts
444
+ function assertTiming(value, name) {
445
+ if (value !== "end" && value !== "begin") {
446
+ throw new RangeError(`${name} must be "end" or "begin"`);
447
+ }
448
+ }
449
+ function annuityDueFactor(ratePerPeriod, timing) {
450
+ return timing === "begin" ? 1 + ratePerPeriod : 1;
451
+ }
452
+ function futureValueFromCashFlows(ratePerPeriod, periods, payment, presentValue2, timing) {
453
+ if (ratePerPeriod === 0) return -(presentValue2 + payment * periods);
454
+ const growth = (1 + ratePerPeriod) ** periods;
455
+ const paymentFv = payment * ((growth - 1) / ratePerPeriod) * annuityDueFactor(ratePerPeriod, timing);
456
+ return -(presentValue2 * growth + paymentFv);
457
+ }
458
+ function fv(params) {
459
+ const { ratePerPeriod, periods, payment = 0, presentValue: presentValue2, timing = "end" } = params;
460
+ assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
461
+ assertPositive(periods, "periods");
462
+ assertFiniteNumber(payment, "payment");
463
+ assertFiniteNumber(presentValue2, "presentValue");
464
+ assertTiming(timing, "timing");
465
+ return futureValueFromCashFlows(ratePerPeriod, periods, payment, presentValue2, timing);
466
+ }
467
+ function pv(params) {
468
+ const { ratePerPeriod, periods, payment = 0, futureValue: futureValue2 = 0, timing = "end" } = params;
469
+ assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
470
+ assertPositive(periods, "periods");
471
+ assertFiniteNumber(payment, "payment");
472
+ assertFiniteNumber(futureValue2, "futureValue");
473
+ assertTiming(timing, "timing");
474
+ if (ratePerPeriod === 0) return -(futureValue2 + payment * periods);
475
+ const growth = (1 + ratePerPeriod) ** periods;
476
+ const paymentPv = payment * (1 - 1 / growth) / ratePerPeriod * annuityDueFactor(ratePerPeriod, timing);
477
+ return -futureValue2 / growth - paymentPv;
478
+ }
479
+ function pmt(params) {
480
+ const { ratePerPeriod, periods, presentValue: presentValue2, futureValue: futureValue2 = 0, timing = "end" } = params;
481
+ assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
482
+ assertPositive(periods, "periods");
483
+ assertFiniteNumber(presentValue2, "presentValue");
484
+ assertFiniteNumber(futureValue2, "futureValue");
485
+ assertTiming(timing, "timing");
486
+ if (ratePerPeriod === 0) return -(presentValue2 + futureValue2) / periods;
487
+ const growth = (1 + ratePerPeriod) ** periods;
488
+ const numerator = -(futureValue2 + presentValue2 * growth) * ratePerPeriod;
489
+ const denominator = (growth - 1) * annuityDueFactor(ratePerPeriod, timing);
490
+ return numerator / denominator;
491
+ }
492
+ function nper(params) {
493
+ const { ratePerPeriod, payment, presentValue: presentValue2, futureValue: futureValue2 = 0, timing = "end" } = params;
494
+ assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
495
+ assertFiniteNumber(payment, "payment");
496
+ assertFiniteNumber(presentValue2, "presentValue");
497
+ assertFiniteNumber(futureValue2, "futureValue");
498
+ assertTiming(timing, "timing");
499
+ if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
500
+ throw new RangeError("ratePerPeriod must be > -1");
501
+ }
502
+ if (ratePerPeriod === 0) {
503
+ const linear = -(presentValue2 + futureValue2) / payment;
504
+ return Number.isFinite(linear) && linear >= 0 ? linear : Number.NaN;
505
+ }
506
+ const adjustedPayment = payment * annuityDueFactor(ratePerPeriod, timing);
507
+ const numerator = adjustedPayment - futureValue2 * ratePerPeriod;
508
+ const denominator = adjustedPayment + presentValue2 * ratePerPeriod;
509
+ if (numerator === 0 || denominator === 0) return Number.NaN;
510
+ const ratio = numerator / denominator;
511
+ if (ratio <= 0) return Number.NaN;
512
+ const periods = Math.log(ratio) / Math.log(1 + ratePerPeriod);
513
+ return Number.isFinite(periods) ? periods : Number.NaN;
514
+ }
515
+ function rate(params) {
516
+ const {
517
+ periods,
518
+ payment,
519
+ presentValue: presentValue2,
520
+ futureValue: futureValue2 = 0,
521
+ timing = "end",
522
+ guess = 0.1,
523
+ tolerance = 1e-10,
524
+ maxIterations = 100,
525
+ lowerBound = -0.999999999,
526
+ upperBound = 10
527
+ } = params;
528
+ assertPositive(periods, "periods");
529
+ assertFiniteNumber(payment, "payment");
530
+ assertFiniteNumber(presentValue2, "presentValue");
531
+ assertFiniteNumber(futureValue2, "futureValue");
532
+ assertFiniteNumber(guess, "guess");
533
+ assertFiniteNumber(maxIterations, "maxIterations");
534
+ assertFiniteNumber(lowerBound, "lowerBound");
535
+ assertFiniteNumber(upperBound, "upperBound");
536
+ assertTiming(timing, "timing");
537
+ if (ratePerPeriodIsInvalidForDomain(lowerBound)) {
538
+ throw new RangeError("lowerBound must be > -1");
539
+ }
540
+ if (upperBound <= lowerBound) {
541
+ throw new RangeError("upperBound must be greater than lowerBound");
542
+ }
543
+ if (!Number.isInteger(maxIterations) || maxIterations <= 0) {
544
+ throw new RangeError("maxIterations must be a positive integer");
545
+ }
546
+ const fn = (r) => futureValueFromCashFlows(r, periods, payment, presentValue2, timing) - futureValue2;
547
+ const derivative = (r) => {
548
+ const delta = Math.abs(r) > 1e-5 ? Math.abs(r) * 1e-6 : 1e-6;
549
+ return (fn(r + delta) - fn(r - delta)) / (2 * delta);
550
+ };
551
+ const newton = newtonRaphson({
552
+ initialGuess: guess,
553
+ fn,
554
+ derivative,
555
+ tolerance,
556
+ maxIterations,
557
+ min: lowerBound,
558
+ max: upperBound
559
+ });
560
+ if (newton !== void 0) return newton;
561
+ const bracket = findRateBracket(fn, lowerBound, upperBound);
562
+ if (bracket === void 0) {
563
+ throw new RangeError("RATE did not converge within search bounds");
564
+ }
565
+ if (bracket.lower === bracket.upper) return bracket.lower;
566
+ const bisected = bisection({
567
+ fn,
568
+ lower: bracket.lower,
569
+ upper: bracket.upper,
570
+ tolerance,
571
+ maxIterations: maxIterations * 2
572
+ });
573
+ if (bisected !== void 0) return bisected;
574
+ throw new RangeError("RATE did not converge");
575
+ }
576
+ function ratePerPeriodIsInvalidForDomain(ratePerPeriod) {
577
+ return ratePerPeriod <= -1;
578
+ }
579
+ function findRateBracket(fn, lowerBound, upperBound) {
580
+ const scan = (start, end, segments = 200) => {
581
+ let prevX;
582
+ let prevValue;
583
+ for (let i = 0; i <= segments; i++) {
584
+ const x = start + (end - start) * i / segments;
585
+ const value = fn(x);
586
+ if (!Number.isFinite(value)) continue;
587
+ if (value === 0) return { lower: x, upper: x };
588
+ if (prevValue !== void 0 && prevX !== void 0 && prevValue * value < 0) {
589
+ return { lower: prevX, upper: x };
590
+ }
591
+ prevX = x;
592
+ prevValue = value;
593
+ }
594
+ return void 0;
595
+ };
596
+ let lower = lowerBound;
597
+ let upper = upperBound;
598
+ for (let i = 0; i < 20; i++) {
599
+ const bracket = scan(lower, upper);
600
+ if (bracket !== void 0) return bracket;
601
+ lower = Math.max(-0.999999999, (lower - 1) / 2);
602
+ upper = upper * 2 + 1;
603
+ }
604
+ return scan(lower, upper, 400);
289
605
  }
290
606
 
291
607
  // src/loans/loanPayment.ts
@@ -408,19 +724,27 @@ function roundToCurrency(params) {
408
724
  }
409
725
 
410
726
  exports.amortizationSchedule = amortizationSchedule;
727
+ exports.bisection = bisection;
411
728
  exports.cagr = cagr;
412
729
  exports.compound = compound;
413
730
  exports.effectiveAnnualRate = effectiveAnnualRate;
414
731
  exports.futureValue = futureValue;
732
+ exports.fv = fv;
415
733
  exports.inflationAdjustedAmount = inflationAdjustedAmount;
416
734
  exports.investmentGrowth = investmentGrowth;
417
735
  exports.irr = irr;
418
736
  exports.loanPayment = loanPayment;
737
+ exports.newtonRaphson = newtonRaphson;
738
+ exports.nper = nper;
739
+ exports.npv = npv;
419
740
  exports.paymentFromPresentValue = paymentFromPresentValue;
420
741
  exports.payoffPeriodWithExtra = payoffPeriodWithExtra;
421
742
  exports.periodsToReachGoal = periodsToReachGoal;
743
+ exports.pmt = pmt;
422
744
  exports.presentValue = presentValue;
423
745
  exports.presentValueOfAnnuity = presentValueOfAnnuity;
746
+ exports.pv = pv;
747
+ exports.rate = rate;
424
748
  exports.rateToReachGoal = rateToReachGoal;
425
749
  exports.realReturn = realReturn;
426
750
  exports.remainingBalance = remainingBalance;