@pyverret/ratejs 1.0.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/dist/index.js CHANGED
@@ -28,14 +28,14 @@ function cagr(params) {
28
28
 
29
29
  // src/interest/compound.ts
30
30
  function compound(params) {
31
- const { principal, rate, timesPerYear, years } = params;
31
+ const { principal, rate: rate2, timesPerYear, years } = params;
32
32
  assertNonNegative(principal, "principal");
33
33
  assertPositive(timesPerYear, "timesPerYear");
34
34
  assertNonNegative(years, "years");
35
- if (rate === 0 || years === 0) return principal;
35
+ if (rate2 === 0 || years === 0) return principal;
36
36
  const n = timesPerYear;
37
37
  const t = years;
38
- return principal * (1 + rate / n) ** (n * t);
38
+ return principal * (1 + rate2 / n) ** (n * t);
39
39
  }
40
40
 
41
41
  // src/interest/effectiveAnnualRate.ts
@@ -49,14 +49,14 @@ function effectiveAnnualRate(params) {
49
49
 
50
50
  // src/interest/futureValue.ts
51
51
  function futureValue(params) {
52
- const { presentValue: presentValue2, rate, timesPerYear, years } = params;
52
+ const { presentValue: presentValue2, rate: rate2, timesPerYear, years } = params;
53
53
  assertNonNegative(presentValue2, "presentValue");
54
54
  assertPositive(timesPerYear, "timesPerYear");
55
55
  assertNonNegative(years, "years");
56
- if (rate === 0 || years === 0) return presentValue2;
56
+ if (rate2 === 0 || years === 0) return presentValue2;
57
57
  const n = timesPerYear;
58
58
  const t = years;
59
- return presentValue2 * (1 + rate / n) ** (n * t);
59
+ return presentValue2 * (1 + rate2 / n) ** (n * t);
60
60
  }
61
61
 
62
62
  // src/interest/inflationAdjustedAmount.ts
@@ -74,7 +74,7 @@ function investmentGrowth(params) {
74
74
  const {
75
75
  initial,
76
76
  contributionPerPeriod = 0,
77
- rate,
77
+ rate: rate2,
78
78
  timesPerYear,
79
79
  years,
80
80
  contributionTiming = "end"
@@ -91,11 +91,11 @@ function investmentGrowth(params) {
91
91
  totalInterest: 0
92
92
  };
93
93
  }
94
- const r = rate / timesPerYear;
95
- const fvInitial = rate === 0 ? initial : initial * (1 + r) ** periods;
94
+ const r = rate2 / timesPerYear;
95
+ const fvInitial = rate2 === 0 ? initial : initial * (1 + r) ** periods;
96
96
  let fvContrib = 0;
97
97
  if (contributionPerPeriod !== 0) {
98
- if (rate === 0) {
98
+ if (rate2 === 0) {
99
99
  fvContrib = contributionPerPeriod * periods;
100
100
  } else {
101
101
  const ordinary = contributionPerPeriod * (((1 + r) ** periods - 1) / r);
@@ -108,38 +108,179 @@ function investmentGrowth(params) {
108
108
  return { futureValue: futureValue2, totalContributions, totalInterest };
109
109
  }
110
110
 
111
+ // src/utils/solvers.ts
112
+ function newtonRaphson(params) {
113
+ const {
114
+ initialGuess,
115
+ fn,
116
+ derivative,
117
+ tolerance = 1e-10,
118
+ maxIterations = 100,
119
+ min = Number.NEGATIVE_INFINITY,
120
+ max = Number.POSITIVE_INFINITY
121
+ } = params;
122
+ let x = initialGuess;
123
+ for (let i = 0; i < maxIterations; i++) {
124
+ const value = fn(x);
125
+ if (!Number.isFinite(value)) return void 0;
126
+ if (Math.abs(value) <= tolerance) return x;
127
+ const slope = derivative(x);
128
+ if (!Number.isFinite(slope) || Math.abs(slope) < 1e-14) return void 0;
129
+ x -= value / slope;
130
+ if (x < min) x = min;
131
+ if (x > max) x = max;
132
+ }
133
+ return void 0;
134
+ }
135
+ function bisection(params) {
136
+ const { fn, lower, upper, tolerance = 1e-10, maxIterations = 200 } = params;
137
+ let a = lower;
138
+ let b = upper;
139
+ let fa = fn(a);
140
+ let fb = fn(b);
141
+ if (!Number.isFinite(fa) || !Number.isFinite(fb) || fa * fb > 0) return void 0;
142
+ if (Math.abs(fa) <= tolerance) return a;
143
+ if (Math.abs(fb) <= tolerance) return b;
144
+ for (let i = 0; i < maxIterations; i++) {
145
+ const c = (a + b) / 2;
146
+ const fc = fn(c);
147
+ if (!Number.isFinite(fc)) return void 0;
148
+ if (Math.abs(fc) <= tolerance || Math.abs(b - a) <= tolerance) return c;
149
+ if (fa * fc < 0) {
150
+ b = c;
151
+ fb = fc;
152
+ } else {
153
+ a = c;
154
+ fa = fc;
155
+ }
156
+ }
157
+ return (a + b) / 2;
158
+ }
159
+
111
160
  // src/interest/irr.ts
112
- function npv(rate, cashFlows) {
161
+ function npvAt(rate2, cashFlows) {
113
162
  let sum = 0;
114
163
  for (let t = 0; t < cashFlows.length; t++) {
115
164
  const cf = cashFlows[t];
116
- if (cf !== void 0) sum += cf / (1 + rate) ** t;
165
+ if (cf !== void 0) sum += cf / (1 + rate2) ** t;
117
166
  }
118
167
  return sum;
119
168
  }
120
- function npvDerivative(rate, cashFlows) {
169
+ function npvDerivativeAt(rate2, cashFlows) {
121
170
  let sum = 0;
122
171
  for (let t = 1; t < cashFlows.length; t++) {
123
172
  const cf = cashFlows[t];
124
- if (cf !== void 0) sum -= t * cf / (1 + rate) ** (t + 1);
173
+ if (cf !== void 0) sum -= t * cf / (1 + rate2) ** (t + 1);
125
174
  }
126
175
  return sum;
127
176
  }
177
+ function hasPositiveAndNegative(cashFlows) {
178
+ let hasPositive = false;
179
+ let hasNegative = false;
180
+ for (const cf of cashFlows) {
181
+ if (cf > 0) hasPositive = true;
182
+ if (cf < 0) hasNegative = true;
183
+ if (hasPositive && hasNegative) return true;
184
+ }
185
+ return false;
186
+ }
187
+ function findBracket(fn, lowerBound, 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
+ };
204
+ let lower = lowerBound;
205
+ let upper = upperBound;
206
+ for (let i = 0; i < 20; i++) {
207
+ const bracket = scan(lower, upper);
208
+ if (bracket !== void 0) return bracket;
209
+ lower = Math.max(-0.999999999, (lower - 1) / 2);
210
+ upper = upper * 2 + 1;
211
+ }
212
+ return scan(lower, upper, 400);
213
+ }
128
214
  function irr(params) {
129
- const { cashFlows, guess = 0.1, maxIterations = 100 } = params;
130
- if (cashFlows.length === 0) return NaN;
215
+ const {
216
+ cashFlows,
217
+ guess = 0.1,
218
+ maxIterations = 100,
219
+ lowerBound = -0.99,
220
+ upperBound = 10
221
+ } = params;
222
+ if (cashFlows.length === 0) {
223
+ throw new RangeError("cashFlows must contain at least one value");
224
+ }
225
+ assertFiniteNumber(guess, "guess");
226
+ assertFiniteNumber(maxIterations, "maxIterations");
227
+ assertFiniteNumber(lowerBound, "lowerBound");
228
+ assertFiniteNumber(upperBound, "upperBound");
229
+ if (maxIterations <= 0 || !Number.isInteger(maxIterations)) {
230
+ throw new RangeError("maxIterations must be a positive integer");
231
+ }
232
+ if (lowerBound <= -1) {
233
+ throw new RangeError("lowerBound must be > -1");
234
+ }
235
+ if (upperBound <= lowerBound) {
236
+ throw new RangeError("upperBound must be greater than lowerBound");
237
+ }
131
238
  for (const cf of cashFlows) assertFiniteNumber(cf, "cashFlows[]");
132
- let r = guess;
133
- for (let i = 0; i < maxIterations; i++) {
134
- const val = npv(r, cashFlows);
135
- if (Math.abs(val) < 1e-10) return r;
136
- const der = npvDerivative(r, cashFlows);
137
- if (!Number.isFinite(der) || Math.abs(der) < 1e-15) break;
138
- r = r - val / der;
139
- if (r < -0.99) r = -0.99;
140
- if (r > 10) r = 10;
239
+ if (!hasPositiveAndNegative(cashFlows)) {
240
+ throw new RangeError("cashFlows must include at least one positive and one negative value");
241
+ }
242
+ const fn = (rate2) => npvAt(rate2, cashFlows);
243
+ const derivative = (rate2) => npvDerivativeAt(rate2, cashFlows);
244
+ const newton = newtonRaphson({
245
+ initialGuess: guess,
246
+ fn,
247
+ derivative,
248
+ tolerance: 1e-10,
249
+ maxIterations,
250
+ min: lowerBound,
251
+ max: upperBound
252
+ });
253
+ if (newton !== void 0) return newton;
254
+ const bracket = findBracket(fn, lowerBound, upperBound);
255
+ if (bracket === void 0) {
256
+ throw new RangeError("IRR did not converge within search bounds");
141
257
  }
142
- return r;
258
+ if (bracket.lower === bracket.upper) return bracket.lower;
259
+ const bisected = bisection({
260
+ fn,
261
+ lower: bracket.lower,
262
+ upper: bracket.upper,
263
+ tolerance: 1e-10,
264
+ maxIterations: maxIterations * 2
265
+ });
266
+ if (bisected !== void 0) return bisected;
267
+ throw new RangeError("IRR did not converge");
268
+ }
269
+
270
+ // src/interest/npv.ts
271
+ function npv(params) {
272
+ const { rate: rate2, cashFlows } = params;
273
+ assertFiniteNumber(rate2, "rate");
274
+ if (rate2 <= -1) throw new RangeError("rate must be > -1");
275
+ if (cashFlows.length === 0) return 0;
276
+ let sum = 0;
277
+ for (let t = 0; t < cashFlows.length; t++) {
278
+ const cf = cashFlows[t];
279
+ if (cf === void 0) continue;
280
+ assertFiniteNumber(cf, `cashFlows[${t}]`);
281
+ sum += cf / (1 + rate2) ** t;
282
+ }
283
+ return sum;
143
284
  }
144
285
 
145
286
  // src/interest/paymentFromPresentValue.ts
@@ -149,12 +290,12 @@ function paymentFromPresentValue(params) {
149
290
  assertNonNegative(periods, "periods");
150
291
  if (presentValue2 === 0 || periods === 0) return 0;
151
292
  if (ratePerPeriod === 0) {
152
- const pmt2 = presentValue2 / periods;
153
- return timing === "begin" ? pmt2 / (1 + ratePerPeriod) : pmt2;
293
+ const pmt3 = presentValue2 / periods;
294
+ return timing === "begin" ? pmt3 / (1 + ratePerPeriod) : pmt3;
154
295
  }
155
- let pmt = ratePerPeriod * presentValue2 / (1 - (1 + ratePerPeriod) ** -periods);
156
- if (timing === "begin") pmt /= 1 + ratePerPeriod;
157
- return pmt;
296
+ let pmt2 = ratePerPeriod * presentValue2 / (1 - (1 + ratePerPeriod) ** -periods);
297
+ if (timing === "begin") pmt2 /= 1 + ratePerPeriod;
298
+ return pmt2;
158
299
  }
159
300
 
160
301
  // src/interest/periodsToReachGoal.ts
@@ -162,54 +303,56 @@ function periodsToReachGoal(params) {
162
303
  const {
163
304
  principal,
164
305
  targetFutureValue,
165
- rate,
306
+ rate: rate2,
166
307
  timesPerYear,
167
308
  contributionPerPeriod = 0,
168
- contributionTiming = "end"
309
+ contributionTiming = "end",
310
+ maxPeriods = 1e5
169
311
  } = params;
170
312
  assertNonNegative(principal, "principal");
171
313
  assertNonNegative(targetFutureValue, "targetFutureValue");
172
- assertFiniteNumber(rate, "rate");
314
+ assertFiniteNumber(rate2, "rate");
173
315
  assertPositive(timesPerYear, "timesPerYear");
174
316
  assertNonNegative(contributionPerPeriod, "contributionPerPeriod");
317
+ assertPositive(maxPeriods, "maxPeriods");
175
318
  if (targetFutureValue <= principal) return 0;
176
- const r = rate / timesPerYear;
319
+ const r = rate2 / timesPerYear;
177
320
  if (r <= -1) {
178
321
  throw new RangeError("rate / timesPerYear must be > -1");
179
322
  }
180
323
  if (contributionPerPeriod === 0) {
181
324
  if (principal === 0) return Number.POSITIVE_INFINITY;
182
- if (rate === 0) return targetFutureValue <= principal ? 0 : Number.POSITIVE_INFINITY;
325
+ if (rate2 === 0) return targetFutureValue <= principal ? 0 : Number.POSITIVE_INFINITY;
183
326
  if (r < 0) return Number.POSITIVE_INFINITY;
184
327
  const n = Math.log(targetFutureValue / principal) / Math.log(1 + r);
185
328
  return Number.isFinite(n) && n >= 0 ? Math.ceil(n) : Number.POSITIVE_INFINITY;
186
329
  }
187
- if (rate === 0) {
330
+ if (rate2 === 0) {
188
331
  const needed = targetFutureValue - principal;
189
332
  if (needed <= 0) return 0;
190
333
  return Math.ceil(needed / contributionPerPeriod);
191
334
  }
192
- let fv = principal;
335
+ let fv2 = principal;
193
336
  let periods = 0;
194
- const maxPeriods = 1e4;
195
- while (fv < targetFutureValue && periods < maxPeriods) {
196
- if (contributionTiming === "begin") fv += contributionPerPeriod;
197
- fv = fv * (1 + r) + (contributionTiming === "end" ? contributionPerPeriod : 0);
337
+ while (fv2 < targetFutureValue && periods < maxPeriods) {
338
+ if (contributionTiming === "begin") fv2 += contributionPerPeriod;
339
+ fv2 = fv2 * (1 + r) + (contributionTiming === "end" ? contributionPerPeriod : 0);
198
340
  periods++;
199
341
  }
200
- return fv >= targetFutureValue ? periods : Number.POSITIVE_INFINITY;
342
+ if (fv2 >= targetFutureValue) return periods;
343
+ throw new RangeError("maxPeriods exceeded before reaching target");
201
344
  }
202
345
 
203
346
  // src/interest/presentValue.ts
204
347
  function presentValue(params) {
205
- const { futureValue: fv, rate, timesPerYear, years } = params;
206
- assertNonNegative(fv, "futureValue");
348
+ const { futureValue: fv2, rate: rate2, timesPerYear, years } = params;
349
+ assertNonNegative(fv2, "futureValue");
207
350
  assertPositive(timesPerYear, "timesPerYear");
208
351
  assertNonNegative(years, "years");
209
- if (rate === 0 || years === 0) return fv;
352
+ if (rate2 === 0 || years === 0) return fv2;
210
353
  const n = timesPerYear;
211
354
  const t = years;
212
- return fv / (1 + rate / n) ** (n * t);
355
+ return fv2 / (1 + rate2 / n) ** (n * t);
213
356
  }
214
357
 
215
358
  // src/interest/presentValueOfAnnuity.ts
@@ -219,11 +362,11 @@ function presentValueOfAnnuity(params) {
219
362
  assertNonNegative(periods, "periods");
220
363
  if (periods === 0) return 0;
221
364
  if (ratePerPeriod === 0) {
222
- const pv2 = paymentPerPeriod * periods;
223
- return timing === "begin" ? pv2 * (1 + ratePerPeriod) : pv2;
365
+ const pv3 = paymentPerPeriod * periods;
366
+ return timing === "begin" ? pv3 * (1 + ratePerPeriod) : pv3;
224
367
  }
225
- const pv = paymentPerPeriod * (1 - (1 + ratePerPeriod) ** -periods) / ratePerPeriod;
226
- return timing === "begin" ? pv * (1 + ratePerPeriod) : pv;
368
+ const pv2 = paymentPerPeriod * (1 - (1 + ratePerPeriod) ** -periods) / ratePerPeriod;
369
+ return timing === "begin" ? pv2 * (1 + ratePerPeriod) : pv2;
227
370
  }
228
371
 
229
372
  // src/interest/rateToReachGoal.ts
@@ -233,40 +376,102 @@ function rateToReachGoal(params) {
233
376
  targetFutureValue,
234
377
  periods,
235
378
  contributionPerPeriod = 0,
236
- contributionTiming = "end"
379
+ contributionTiming = "end",
380
+ lowerBound = -0.99,
381
+ upperBound = 10,
382
+ maxIterations = 100,
383
+ tolerance = 1e-10
237
384
  } = params;
238
385
  assertNonNegative(principal, "principal");
239
386
  assertNonNegative(targetFutureValue, "targetFutureValue");
240
387
  assertPositive(periods, "periods");
241
388
  assertNonNegative(contributionPerPeriod, "contributionPerPeriod");
389
+ assertFiniteNumber(lowerBound, "lowerBound");
390
+ assertFiniteNumber(upperBound, "upperBound");
391
+ assertFiniteNumber(maxIterations, "maxIterations");
392
+ assertFiniteNumber(tolerance, "tolerance");
393
+ if (lowerBound <= -1) throw new RangeError("lowerBound must be > -1");
394
+ if (upperBound <= lowerBound) throw new RangeError("upperBound must be greater than lowerBound");
395
+ if (!Number.isInteger(maxIterations) || maxIterations <= 0) {
396
+ throw new RangeError("maxIterations must be a positive integer");
397
+ }
398
+ if (tolerance <= 0) throw new RangeError("tolerance must be > 0");
242
399
  if (periods === 0) return 0;
243
400
  if (targetFutureValue <= principal && contributionPerPeriod === 0) return 0;
244
401
  if (contributionPerPeriod === 0) {
245
402
  if (targetFutureValue <= principal) return 0;
403
+ if (principal === 0) {
404
+ throw new RangeError(
405
+ "rateToReachGoal is undefined when principal is 0 and contributions are 0"
406
+ );
407
+ }
246
408
  return (targetFutureValue / principal) ** (1 / periods) - 1;
247
409
  }
248
410
  const dueFactor = contributionTiming === "begin" ? 1 : 0;
249
- let r = (targetFutureValue / (principal + contributionPerPeriod * periods)) ** (1 / periods) - 1;
250
- if (r <= -1) r = 0.01;
251
- for (let i = 0; i < 100; i++) {
411
+ const fn = (r) => {
252
412
  const onePlusR = 1 + r;
253
413
  const onePlusRN = onePlusR ** periods;
254
414
  const fvLump = principal * onePlusRN;
255
- const annuity = contributionPerPeriod * ((onePlusRN - 1) / (r || 1e-14)) * (1 + dueFactor * r);
256
- const fv = fvLump + annuity;
257
- const err = fv - targetFutureValue;
258
- if (Math.abs(err) < 1e-10) return r;
259
- const dr = r * 1e-6 || 1e-10;
260
- const r2 = r + dr;
261
- const onePlusR2 = 1 + r2;
262
- const onePlusR2N = onePlusR2 ** periods;
263
- const fv2 = principal * onePlusR2N + contributionPerPeriod * ((onePlusR2N - 1) / r2) * (1 + dueFactor * r2);
264
- const dFvDr = (fv2 - fv) / dr;
265
- r = r - err / dFvDr;
266
- if (r <= -1) r = 0.01;
267
- if (r > 10) r = 10;
268
- }
269
- return r;
415
+ const annuityBase = r === 0 ? periods : (onePlusRN - 1) / r;
416
+ const fvAnnuity = contributionPerPeriod * annuityBase * (1 + dueFactor * r);
417
+ return fvLump + fvAnnuity - targetFutureValue;
418
+ };
419
+ const derivative = (r) => {
420
+ const delta = Math.abs(r) > 1e-6 ? Math.abs(r) * 1e-6 : 1e-6;
421
+ return (fn(r + delta) - fn(r - delta)) / (2 * delta);
422
+ };
423
+ const initialGuess = (targetFutureValue / (principal + contributionPerPeriod * periods)) ** (1 / periods) - 1;
424
+ const newton = newtonRaphson({
425
+ initialGuess: Number.isFinite(initialGuess) ? Math.max(initialGuess, lowerBound) : 0.01,
426
+ fn,
427
+ derivative,
428
+ tolerance,
429
+ maxIterations,
430
+ min: lowerBound,
431
+ max: upperBound
432
+ });
433
+ if (newton !== void 0) return newton;
434
+ const bracket = findBracket2(fn, lowerBound, upperBound);
435
+ if (bracket === void 0) {
436
+ throw new RangeError("rateToReachGoal did not converge within search bounds");
437
+ }
438
+ if (bracket.lower === bracket.upper) return bracket.lower;
439
+ const bisected = bisection({
440
+ fn,
441
+ lower: bracket.lower,
442
+ upper: bracket.upper,
443
+ tolerance,
444
+ maxIterations: maxIterations * 2
445
+ });
446
+ if (bisected !== void 0) return bisected;
447
+ throw new RangeError("rateToReachGoal did not converge");
448
+ }
449
+ function findBracket2(fn, lowerBound, upperBound) {
450
+ const scan = (start, end, segments = 200) => {
451
+ let prevX;
452
+ let prevValue;
453
+ for (let i = 0; i <= segments; i++) {
454
+ const x = start + (end - start) * i / segments;
455
+ const value = fn(x);
456
+ if (!Number.isFinite(value)) continue;
457
+ if (value === 0) return { lower: x, upper: x };
458
+ if (prevX !== void 0 && prevValue !== void 0 && prevValue * value < 0) {
459
+ return { lower: prevX, upper: x };
460
+ }
461
+ prevX = x;
462
+ prevValue = value;
463
+ }
464
+ return void 0;
465
+ };
466
+ let lower = lowerBound;
467
+ let upper = upperBound;
468
+ for (let i = 0; i < 20; i++) {
469
+ const bracket = scan(lower, upper);
470
+ if (bracket !== void 0) return bracket;
471
+ lower = Math.max(-0.999999999, (lower - 1) / 2);
472
+ upper = upper * 2 + 1;
473
+ }
474
+ return scan(lower, upper, 400);
270
475
  }
271
476
 
272
477
  // src/interest/realReturn.ts
@@ -280,23 +485,223 @@ function realReturn(params) {
280
485
 
281
486
  // src/interest/ruleOf72.ts
282
487
  function ruleOf72(params) {
283
- const { rate, constant = 72 } = params;
284
- assertFiniteNumber(rate, "rate");
285
- assertPositive(rate, "rate");
286
- return constant / 100 / rate;
488
+ const { rate: rate2, constant = 72 } = params;
489
+ assertFiniteNumber(rate2, "rate");
490
+ assertPositive(rate2, "rate");
491
+ return constant / 100 / rate2;
492
+ }
493
+
494
+ // src/interest/tvom.ts
495
+ function assertTiming(value, name) {
496
+ if (value !== "end" && value !== "begin") {
497
+ throw new RangeError(`${name} must be "end" or "begin"`);
498
+ }
499
+ }
500
+ function annuityDueFactor(ratePerPeriod, timing) {
501
+ return timing === "begin" ? 1 + ratePerPeriod : 1;
502
+ }
503
+ function futureValueFromCashFlows(ratePerPeriod, periods, payment, presentValue2, timing) {
504
+ if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
505
+ throw new RangeError("ratePerPeriod must be > -1");
506
+ }
507
+ if (ratePerPeriod === 0) return -(presentValue2 + payment * periods);
508
+ const growth = (1 + ratePerPeriod) ** periods;
509
+ if (!Number.isFinite(growth) || growth <= 0) return Number.NaN;
510
+ const paymentFv = payment * ((growth - 1) / ratePerPeriod) * annuityDueFactor(ratePerPeriod, timing);
511
+ return -(presentValue2 * growth + paymentFv);
512
+ }
513
+ function fv(params) {
514
+ const { ratePerPeriod, periods, payment = 0, presentValue: presentValue2, timing = "end" } = params;
515
+ assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
516
+ assertPositive(periods, "periods");
517
+ assertFiniteNumber(payment, "payment");
518
+ assertFiniteNumber(presentValue2, "presentValue");
519
+ assertTiming(timing, "timing");
520
+ const value = futureValueFromCashFlows(ratePerPeriod, periods, payment, presentValue2, timing);
521
+ if (!Number.isFinite(value)) {
522
+ throw new RangeError("Numerical instability for given inputs");
523
+ }
524
+ return value;
525
+ }
526
+ function pv(params) {
527
+ const { ratePerPeriod, periods, payment = 0, futureValue: futureValue2 = 0, timing = "end" } = params;
528
+ assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
529
+ assertPositive(periods, "periods");
530
+ assertFiniteNumber(payment, "payment");
531
+ assertFiniteNumber(futureValue2, "futureValue");
532
+ assertTiming(timing, "timing");
533
+ if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
534
+ throw new RangeError("ratePerPeriod must be > -1");
535
+ }
536
+ if (ratePerPeriod === 0) return -(futureValue2 + payment * periods);
537
+ const growth = (1 + ratePerPeriod) ** periods;
538
+ if (!Number.isFinite(growth) || growth <= 0) {
539
+ throw new RangeError("Numerical instability for given ratePerPeriod/periods");
540
+ }
541
+ const paymentPv = payment * (1 - 1 / growth) / ratePerPeriod * annuityDueFactor(ratePerPeriod, timing);
542
+ const value = -futureValue2 / growth - paymentPv;
543
+ if (!Number.isFinite(value)) {
544
+ throw new RangeError("Numerical instability for given inputs");
545
+ }
546
+ return value;
547
+ }
548
+ function pmt(params) {
549
+ const { ratePerPeriod, periods, presentValue: presentValue2, futureValue: futureValue2 = 0, timing = "end" } = params;
550
+ assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
551
+ assertPositive(periods, "periods");
552
+ assertFiniteNumber(presentValue2, "presentValue");
553
+ assertFiniteNumber(futureValue2, "futureValue");
554
+ assertTiming(timing, "timing");
555
+ if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
556
+ throw new RangeError("ratePerPeriod must be > -1");
557
+ }
558
+ if (ratePerPeriod === 0) return -(presentValue2 + futureValue2) / periods;
559
+ const growth = (1 + ratePerPeriod) ** periods;
560
+ if (!Number.isFinite(growth) || growth <= 0) {
561
+ throw new RangeError("Numerical instability for given ratePerPeriod/periods");
562
+ }
563
+ const numerator = -(futureValue2 + presentValue2 * growth) * ratePerPeriod;
564
+ const denominator = (growth - 1) * annuityDueFactor(ratePerPeriod, timing);
565
+ const value = numerator / denominator;
566
+ if (!Number.isFinite(value)) {
567
+ throw new RangeError("Numerical instability for given inputs");
568
+ }
569
+ return value;
570
+ }
571
+ function nper(params) {
572
+ const { ratePerPeriod, payment, presentValue: presentValue2, futureValue: futureValue2 = 0, timing = "end" } = params;
573
+ assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
574
+ assertFiniteNumber(payment, "payment");
575
+ assertFiniteNumber(presentValue2, "presentValue");
576
+ assertFiniteNumber(futureValue2, "futureValue");
577
+ assertTiming(timing, "timing");
578
+ if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
579
+ throw new RangeError("ratePerPeriod must be > -1");
580
+ }
581
+ if (ratePerPeriod === 0) {
582
+ const linear = -(presentValue2 + futureValue2) / payment;
583
+ return Number.isFinite(linear) && linear >= 0 ? linear : Number.NaN;
584
+ }
585
+ const adjustedPayment = payment * annuityDueFactor(ratePerPeriod, timing);
586
+ const numerator = adjustedPayment - futureValue2 * ratePerPeriod;
587
+ const denominator = adjustedPayment + presentValue2 * ratePerPeriod;
588
+ if (numerator === 0 || denominator === 0) return Number.NaN;
589
+ const ratio = numerator / denominator;
590
+ if (ratio <= 0) return Number.NaN;
591
+ const periods = Math.log(ratio) / Math.log(1 + ratePerPeriod);
592
+ return Number.isFinite(periods) ? periods : Number.NaN;
593
+ }
594
+ function rate(params) {
595
+ const {
596
+ periods,
597
+ payment,
598
+ presentValue: presentValue2,
599
+ futureValue: futureValue2 = 0,
600
+ timing = "end",
601
+ guess = 0.1,
602
+ tolerance = 1e-10,
603
+ maxIterations = 100,
604
+ lowerBound = -0.999999999,
605
+ upperBound = 10
606
+ } = params;
607
+ assertPositive(periods, "periods");
608
+ assertFiniteNumber(payment, "payment");
609
+ assertFiniteNumber(presentValue2, "presentValue");
610
+ assertFiniteNumber(futureValue2, "futureValue");
611
+ assertFiniteNumber(guess, "guess");
612
+ assertFiniteNumber(maxIterations, "maxIterations");
613
+ assertFiniteNumber(lowerBound, "lowerBound");
614
+ assertFiniteNumber(upperBound, "upperBound");
615
+ assertTiming(timing, "timing");
616
+ if (ratePerPeriodIsInvalidForDomain(lowerBound)) {
617
+ throw new RangeError("lowerBound must be > -1");
618
+ }
619
+ if (upperBound <= lowerBound) {
620
+ throw new RangeError("upperBound must be greater than lowerBound");
621
+ }
622
+ if (!Number.isInteger(maxIterations) || maxIterations <= 0) {
623
+ throw new RangeError("maxIterations must be a positive integer");
624
+ }
625
+ const fn = (r) => futureValueFromCashFlows(r, periods, payment, presentValue2, timing) - futureValue2;
626
+ const derivative = (r) => {
627
+ const delta = Math.abs(r) > 1e-5 ? Math.abs(r) * 1e-6 : 1e-6;
628
+ return (fn(r + delta) - fn(r - delta)) / (2 * delta);
629
+ };
630
+ const newton = newtonRaphson({
631
+ initialGuess: guess,
632
+ fn,
633
+ derivative,
634
+ tolerance,
635
+ maxIterations,
636
+ min: lowerBound,
637
+ max: upperBound
638
+ });
639
+ if (newton !== void 0) return newton;
640
+ const bracket = findRateBracket(fn, lowerBound, upperBound);
641
+ if (bracket === void 0) {
642
+ throw new RangeError("RATE did not converge within search bounds");
643
+ }
644
+ if (bracket.lower === bracket.upper) return bracket.lower;
645
+ const bisected = bisection({
646
+ fn,
647
+ lower: bracket.lower,
648
+ upper: bracket.upper,
649
+ tolerance,
650
+ maxIterations: maxIterations * 2
651
+ });
652
+ if (bisected !== void 0) return bisected;
653
+ throw new RangeError("RATE did not converge");
654
+ }
655
+ function ratePerPeriodIsInvalidForDomain(ratePerPeriod) {
656
+ return ratePerPeriod <= -1;
657
+ }
658
+ function findRateBracket(fn, lowerBound, upperBound) {
659
+ const scan = (start, end, segments = 200) => {
660
+ let prevX;
661
+ let prevValue;
662
+ for (let i = 0; i <= segments; i++) {
663
+ const x = start + (end - start) * i / segments;
664
+ const value = fn(x);
665
+ if (!Number.isFinite(value)) continue;
666
+ if (value === 0) return { lower: x, upper: x };
667
+ if (prevValue !== void 0 && prevX !== void 0 && prevValue * value < 0) {
668
+ return { lower: prevX, upper: x };
669
+ }
670
+ prevX = x;
671
+ prevValue = value;
672
+ }
673
+ return void 0;
674
+ };
675
+ let lower = lowerBound;
676
+ let upper = upperBound;
677
+ for (let i = 0; i < 20; i++) {
678
+ const bracket = scan(lower, upper);
679
+ if (bracket !== void 0) return bracket;
680
+ lower = Math.max(-0.999999999, (lower - 1) / 2);
681
+ upper = upper * 2 + 1;
682
+ }
683
+ return scan(lower, upper, 400);
287
684
  }
288
685
 
289
686
  // src/loans/loanPayment.ts
290
687
  function loanPayment(params) {
291
688
  const { principal, annualRate, paymentsPerYear, years } = params;
292
689
  assertNonNegative(principal, "principal");
690
+ assertFiniteNumber(annualRate, "annualRate");
293
691
  assertPositive(paymentsPerYear, "paymentsPerYear");
294
692
  assertNonNegative(years, "years");
295
693
  const n = Math.round(paymentsPerYear * years);
296
694
  if (n === 0) return 0;
297
695
  const r = annualRate / paymentsPerYear;
696
+ if (r <= -1) {
697
+ throw new RangeError("annualRate / paymentsPerYear must be > -1");
698
+ }
298
699
  if (r === 0) return principal / n;
299
- return r * principal / (1 - (1 + r) ** -n);
700
+ const value = r * principal / (1 - (1 + r) ** -n);
701
+ if (!Number.isFinite(value)) {
702
+ throw new RangeError("Numerical instability for given inputs");
703
+ }
704
+ return value;
300
705
  }
301
706
 
302
707
  // src/loans/amortizationSchedule.ts
@@ -394,17 +799,20 @@ function roundToCurrency(params) {
394
799
  const factor = 10 ** d;
395
800
  if (mode === "half-even") {
396
801
  const scaled = value * factor;
397
- const rounded = Math.round(scaled);
398
- const remainder = Math.abs(scaled - rounded);
399
- if (remainder === 0.5) {
400
- const down = Math.floor(scaled);
401
- return (down % 2 === 0 ? down : down + 1) / factor;
802
+ const sign = scaled < 0 ? -1 : 1;
803
+ const absScaled = Math.abs(scaled);
804
+ const floorAbs = Math.floor(absScaled);
805
+ const fraction = absScaled - floorAbs;
806
+ const epsilon = 1e-12;
807
+ if (Math.abs(fraction - 0.5) <= epsilon) {
808
+ const nearestEven = floorAbs % 2 === 0 ? floorAbs : floorAbs + 1;
809
+ return sign * nearestEven / factor;
402
810
  }
403
- return rounded / factor;
811
+ return Math.round(scaled) / factor;
404
812
  }
405
813
  return Math.round(value * factor) / factor;
406
814
  }
407
815
 
408
- export { amortizationSchedule, cagr, compound, effectiveAnnualRate, futureValue, inflationAdjustedAmount, investmentGrowth, irr, loanPayment, paymentFromPresentValue, payoffPeriodWithExtra, periodsToReachGoal, presentValue, presentValueOfAnnuity, rateToReachGoal, realReturn, remainingBalance, roundToCurrency, ruleOf72 };
816
+ export { amortizationSchedule, bisection, cagr, compound, effectiveAnnualRate, futureValue, fv, inflationAdjustedAmount, investmentGrowth, irr, loanPayment, newtonRaphson, nper, npv, paymentFromPresentValue, payoffPeriodWithExtra, periodsToReachGoal, pmt, presentValue, presentValueOfAnnuity, pv, rate, rateToReachGoal, realReturn, remainingBalance, roundToCurrency, ruleOf72 };
409
817
  //# sourceMappingURL=index.js.map
410
818
  //# sourceMappingURL=index.js.map