@pyverret/ratejs 1.0.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/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026, Pierre-Yves Verret
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
+
package/README.md ADDED
@@ -0,0 +1,220 @@
1
+ # @pyverret/ratejs
2
+
3
+ Lightweight, dependency-free TypeScript financial math library providing pure calculation utilities.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i @pyverret/ratejs
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ All functions take a single options object (no positional args). Rates are decimals (e.g. `0.05` = 5%).
14
+
15
+ ### Interest & growth
16
+
17
+ - **`compound`** - Final amount for a lump sum with compound interest.
18
+
19
+ ```ts
20
+ compound({ principal: 1000, rate: 0.05, timesPerYear: 12, years: 10 });
21
+ ```
22
+
23
+ - **`futureValue`** - Future value of a present lump sum.
24
+
25
+ ```ts
26
+ futureValue({ presentValue: 2500, rate: 0.07, timesPerYear: 4, years: 3 });
27
+ ```
28
+
29
+ - **`presentValue`** - Present value of a future lump sum.
30
+
31
+ ```ts
32
+ presentValue({ futureValue: 5000, rate: 0.06, timesPerYear: 12, years: 5 });
33
+ ```
34
+
35
+ - **`investmentGrowth`** - Future value with optional periodic contributions. Returns `{ futureValue, totalContributions, totalInterest }`.
36
+
37
+ ```ts
38
+ investmentGrowth({
39
+ initial: 1000,
40
+ contributionPerPeriod: 100,
41
+ rate: 0.06,
42
+ timesPerYear: 12,
43
+ years: 2,
44
+ contributionTiming: "end", // or "begin"
45
+ });
46
+ ```
47
+
48
+ - **`effectiveAnnualRate`** - Convert nominal rate + compounding frequency to effective annual rate (EAR).
49
+
50
+ ```ts
51
+ effectiveAnnualRate({ nominalRate: 0.06, timesPerYear: 12 });
52
+ ```
53
+
54
+ - **`periodsToReachGoal`** — Number of compounding periods until future value reaches a target (lump sum or with contributions).
55
+
56
+ ```ts
57
+ periodsToReachGoal({
58
+ principal: 1000,
59
+ targetFutureValue: 2000,
60
+ rate: 0.06,
61
+ timesPerYear: 12,
62
+ contributionPerPeriod: 0, // optional
63
+ contributionTiming: "end",
64
+ });
65
+ ```
66
+
67
+ Edge cases:
68
+ - Returns `Infinity` when the goal is unreachable.
69
+ - Throws `RangeError` when `rate / timesPerYear <= -1`.
70
+
71
+ - **`rateToReachGoal`** - Rate per period required to reach a target future value in a given number of periods.
72
+
73
+ ```ts
74
+ rateToReachGoal({
75
+ principal: 1000,
76
+ targetFutureValue: 1500,
77
+ periods: 24,
78
+ contributionPerPeriod: 0,
79
+ contributionTiming: "end",
80
+ });
81
+ ```
82
+
83
+ - **`ruleOf72`** - Approximate years to double a lump sum at a given annual rate. Optional `constant: 69` for rule of 69.
84
+
85
+ ```ts
86
+ ruleOf72({ rate: 0.07 }); // ~10.3 years
87
+ ruleOf72({ rate: 0.07, constant: 69 });
88
+ ```
89
+
90
+ ### Returns
91
+
92
+ - **`cagr`** - Compound annual growth rate between a start and end value over years.
93
+
94
+ ```ts
95
+ cagr({ startValue: 1000, endValue: 2000, years: 10 });
96
+ ```
97
+
98
+ - **`irr`** - Internal rate of return: discount rate that makes NPV of cash flows zero. `cashFlows[0]` is typically the initial outlay (negative).
99
+
100
+ ```ts
101
+ irr({ cashFlows: [-1000, 300, 400, 500], guess: 0.1, maxIterations: 100 });
102
+ ```
103
+
104
+ ### Inflation
105
+
106
+ - **`realReturn`** - Real (inflation-adjusted) return from nominal return and inflation rate.
107
+
108
+ ```ts
109
+ realReturn({ nominalReturn: 0.07, inflationRate: 0.02 });
110
+ ```
111
+
112
+ - **`inflationAdjustedAmount`** - Purchasing power across time. `toPast`: value years ago with same purchasing power; `toFuture`: nominal amount in the future with same purchasing power.
113
+
114
+ ```ts
115
+ inflationAdjustedAmount({
116
+ amount: 100,
117
+ annualInflationRate: 0.03,
118
+ years: 10,
119
+ direction: "toPast",
120
+ });
121
+ inflationAdjustedAmount({
122
+ amount: 100,
123
+ annualInflationRate: 0.03,
124
+ years: 10,
125
+ direction: "toFuture",
126
+ });
127
+ ```
128
+
129
+ ### Annuities
130
+
131
+ - **`presentValueOfAnnuity`** - Present value of a series of equal payments. `timing`: `"end"` (ordinary) or `"begin"` (annuity due).
132
+
133
+ ```ts
134
+ presentValueOfAnnuity({
135
+ paymentPerPeriod: 100,
136
+ ratePerPeriod: 0.01,
137
+ periods: 36,
138
+ timing: "end",
139
+ });
140
+ ```
141
+
142
+ - **`paymentFromPresentValue`** - Periodic payment to pay off a present value (e.g. loan) over a number of periods.
143
+
144
+ ```ts
145
+ paymentFromPresentValue({
146
+ presentValue: 100000,
147
+ ratePerPeriod: 0.005,
148
+ periods: 360,
149
+ timing: "end",
150
+ });
151
+ ```
152
+
153
+ ### Loans
154
+
155
+ - **`loanPayment`** - Fixed periodic payment for an amortizing loan (nominal annual rate, payments per year, years).
156
+
157
+ ```ts
158
+ loanPayment({
159
+ principal: 200000,
160
+ annualRate: 0.06,
161
+ paymentsPerYear: 12,
162
+ years: 30,
163
+ });
164
+ ```
165
+
166
+ - **`amortizationSchedule`** - Full schedule: `{ paymentPerPeriod, schedule, totalPaid, totalInterest }`. Optional `extraPaymentPerPeriod`.
167
+
168
+ ```ts
169
+ amortizationSchedule({
170
+ principal: 200000,
171
+ annualRate: 0.06,
172
+ paymentsPerYear: 12,
173
+ years: 30,
174
+ extraPaymentPerPeriod: 50,
175
+ });
176
+ ```
177
+
178
+ - **`remainingBalance`** - Balance remaining after a given number of payments (1-based `afterPeriodNumber`).
179
+
180
+ ```ts
181
+ remainingBalance({
182
+ principal: 200000,
183
+ annualRate: 0.06,
184
+ paymentsPerYear: 12,
185
+ years: 30,
186
+ afterPeriodNumber: 12,
187
+ });
188
+ ```
189
+
190
+ - **`payoffPeriodWithExtra`** — Number of periods until the loan is paid off with base payment + extra payment.
191
+
192
+ ```ts
193
+ payoffPeriodWithExtra({
194
+ principal: 100000,
195
+ annualRate: 0.06,
196
+ paymentsPerYear: 12,
197
+ basePaymentPerPeriod: 599.55,
198
+ extraPaymentPerPeriod: 100,
199
+ });
200
+ ```
201
+
202
+ Edge cases:
203
+ - Returns `Infinity` when payment is zero or does not cover per-period interest.
204
+
205
+ ### Utils
206
+
207
+ - **`roundToCurrency`** - Round to decimal places (default 2). `mode`: `"half-up"` or `"half-even"` (banker's rounding).
208
+
209
+ ```ts
210
+ roundToCurrency({ value: 2.125 }); // 2.13
211
+ roundToCurrency({ value: 2.125, decimals: 2, mode: "half-even" });
212
+ ```
213
+
214
+ ## Design
215
+
216
+ - Pure functions only
217
+ - No runtime dependencies
218
+ - Object-parameter inputs (no positional args)
219
+ - Deterministic outputs
220
+ - Tree-shakeable (import only what you use)
package/dist/index.cjs ADDED
@@ -0,0 +1,430 @@
1
+ 'use strict';
2
+
3
+ // src/utils/assertions.ts
4
+ function assertFiniteNumber(value, name) {
5
+ if (!Number.isFinite(value)) {
6
+ throw new RangeError(`${name} must be a finite number`);
7
+ }
8
+ }
9
+ function assertNonNegative(value, name) {
10
+ assertFiniteNumber(value, name);
11
+ if (value < 0) {
12
+ throw new RangeError(`${name} must be >= 0`);
13
+ }
14
+ }
15
+ function assertPositive(value, name) {
16
+ assertFiniteNumber(value, name);
17
+ if (value <= 0) {
18
+ throw new RangeError(`${name} must be > 0`);
19
+ }
20
+ }
21
+
22
+ // src/interest/cagr.ts
23
+ function cagr(params) {
24
+ const { startValue, endValue, years } = params;
25
+ assertPositive(startValue, "startValue");
26
+ assertPositive(years, "years");
27
+ if (endValue <= 0) return -1;
28
+ return (endValue / startValue) ** (1 / years) - 1;
29
+ }
30
+
31
+ // src/interest/compound.ts
32
+ function compound(params) {
33
+ const { principal, rate, timesPerYear, years } = params;
34
+ assertNonNegative(principal, "principal");
35
+ assertPositive(timesPerYear, "timesPerYear");
36
+ assertNonNegative(years, "years");
37
+ if (rate === 0 || years === 0) return principal;
38
+ const n = timesPerYear;
39
+ const t = years;
40
+ return principal * (1 + rate / n) ** (n * t);
41
+ }
42
+
43
+ // src/interest/effectiveAnnualRate.ts
44
+ function effectiveAnnualRate(params) {
45
+ const { nominalRate, timesPerYear } = params;
46
+ assertFiniteNumber(nominalRate, "nominalRate");
47
+ assertPositive(timesPerYear, "timesPerYear");
48
+ const r = nominalRate / timesPerYear;
49
+ return (1 + r) ** timesPerYear - 1;
50
+ }
51
+
52
+ // src/interest/futureValue.ts
53
+ function futureValue(params) {
54
+ const { presentValue: presentValue2, rate, timesPerYear, years } = params;
55
+ assertNonNegative(presentValue2, "presentValue");
56
+ assertPositive(timesPerYear, "timesPerYear");
57
+ assertNonNegative(years, "years");
58
+ if (rate === 0 || years === 0) return presentValue2;
59
+ const n = timesPerYear;
60
+ const t = years;
61
+ return presentValue2 * (1 + rate / n) ** (n * t);
62
+ }
63
+
64
+ // src/interest/inflationAdjustedAmount.ts
65
+ function inflationAdjustedAmount(params) {
66
+ const { amount, annualInflationRate, years, direction } = params;
67
+ assertFiniteNumber(amount, "amount");
68
+ assertFiniteNumber(annualInflationRate, "annualInflationRate");
69
+ assertNonNegative(years, "years");
70
+ const factor = (1 + annualInflationRate) ** years;
71
+ return direction === "toPast" ? amount / factor : amount * factor;
72
+ }
73
+
74
+ // src/interest/investmentGrowth.ts
75
+ function investmentGrowth(params) {
76
+ const {
77
+ initial,
78
+ contributionPerPeriod = 0,
79
+ rate,
80
+ timesPerYear,
81
+ years,
82
+ contributionTiming = "end"
83
+ } = params;
84
+ assertNonNegative(initial, "initial");
85
+ assertNonNegative(contributionPerPeriod, "contributionPerPeriod");
86
+ assertPositive(timesPerYear, "timesPerYear");
87
+ assertNonNegative(years, "years");
88
+ const periods = Math.round(timesPerYear * years);
89
+ if (periods === 0) {
90
+ return {
91
+ futureValue: initial,
92
+ totalContributions: 0,
93
+ totalInterest: 0
94
+ };
95
+ }
96
+ const r = rate / timesPerYear;
97
+ const fvInitial = rate === 0 ? initial : initial * (1 + r) ** periods;
98
+ let fvContrib = 0;
99
+ if (contributionPerPeriod !== 0) {
100
+ if (rate === 0) {
101
+ fvContrib = contributionPerPeriod * periods;
102
+ } else {
103
+ const ordinary = contributionPerPeriod * (((1 + r) ** periods - 1) / r);
104
+ fvContrib = contributionTiming === "begin" ? ordinary * (1 + r) : ordinary;
105
+ }
106
+ }
107
+ const futureValue2 = fvInitial + fvContrib;
108
+ const totalContributions = contributionPerPeriod * periods;
109
+ const totalInterest = futureValue2 - initial - totalContributions;
110
+ return { futureValue: futureValue2, totalContributions, totalInterest };
111
+ }
112
+
113
+ // src/interest/irr.ts
114
+ function npv(rate, cashFlows) {
115
+ let sum = 0;
116
+ for (let t = 0; t < cashFlows.length; t++) {
117
+ const cf = cashFlows[t];
118
+ if (cf !== void 0) sum += cf / (1 + rate) ** t;
119
+ }
120
+ return sum;
121
+ }
122
+ function npvDerivative(rate, cashFlows) {
123
+ let sum = 0;
124
+ for (let t = 1; t < cashFlows.length; t++) {
125
+ const cf = cashFlows[t];
126
+ if (cf !== void 0) sum -= t * cf / (1 + rate) ** (t + 1);
127
+ }
128
+ return sum;
129
+ }
130
+ function irr(params) {
131
+ const { cashFlows, guess = 0.1, maxIterations = 100 } = params;
132
+ if (cashFlows.length === 0) return NaN;
133
+ 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;
143
+ }
144
+ return r;
145
+ }
146
+
147
+ // src/interest/paymentFromPresentValue.ts
148
+ function paymentFromPresentValue(params) {
149
+ const { presentValue: presentValue2, ratePerPeriod, periods, timing = "end" } = params;
150
+ assertNonNegative(presentValue2, "presentValue");
151
+ assertNonNegative(periods, "periods");
152
+ if (presentValue2 === 0 || periods === 0) return 0;
153
+ if (ratePerPeriod === 0) {
154
+ const pmt2 = presentValue2 / periods;
155
+ return timing === "begin" ? pmt2 / (1 + ratePerPeriod) : pmt2;
156
+ }
157
+ let pmt = ratePerPeriod * presentValue2 / (1 - (1 + ratePerPeriod) ** -periods);
158
+ if (timing === "begin") pmt /= 1 + ratePerPeriod;
159
+ return pmt;
160
+ }
161
+
162
+ // src/interest/periodsToReachGoal.ts
163
+ function periodsToReachGoal(params) {
164
+ const {
165
+ principal,
166
+ targetFutureValue,
167
+ rate,
168
+ timesPerYear,
169
+ contributionPerPeriod = 0,
170
+ contributionTiming = "end"
171
+ } = params;
172
+ assertNonNegative(principal, "principal");
173
+ assertNonNegative(targetFutureValue, "targetFutureValue");
174
+ assertFiniteNumber(rate, "rate");
175
+ assertPositive(timesPerYear, "timesPerYear");
176
+ assertNonNegative(contributionPerPeriod, "contributionPerPeriod");
177
+ if (targetFutureValue <= principal) return 0;
178
+ const r = rate / timesPerYear;
179
+ if (r <= -1) {
180
+ throw new RangeError("rate / timesPerYear must be > -1");
181
+ }
182
+ if (contributionPerPeriod === 0) {
183
+ if (principal === 0) return Number.POSITIVE_INFINITY;
184
+ if (rate === 0) return targetFutureValue <= principal ? 0 : Number.POSITIVE_INFINITY;
185
+ if (r < 0) return Number.POSITIVE_INFINITY;
186
+ const n = Math.log(targetFutureValue / principal) / Math.log(1 + r);
187
+ return Number.isFinite(n) && n >= 0 ? Math.ceil(n) : Number.POSITIVE_INFINITY;
188
+ }
189
+ if (rate === 0) {
190
+ const needed = targetFutureValue - principal;
191
+ if (needed <= 0) return 0;
192
+ return Math.ceil(needed / contributionPerPeriod);
193
+ }
194
+ let fv = principal;
195
+ let periods = 0;
196
+ const maxPeriods = 1e4;
197
+ while (fv < targetFutureValue && periods < maxPeriods) {
198
+ if (contributionTiming === "begin") fv += contributionPerPeriod;
199
+ fv = fv * (1 + r) + (contributionTiming === "end" ? contributionPerPeriod : 0);
200
+ periods++;
201
+ }
202
+ return fv >= targetFutureValue ? periods : Number.POSITIVE_INFINITY;
203
+ }
204
+
205
+ // src/interest/presentValue.ts
206
+ function presentValue(params) {
207
+ const { futureValue: fv, rate, timesPerYear, years } = params;
208
+ assertNonNegative(fv, "futureValue");
209
+ assertPositive(timesPerYear, "timesPerYear");
210
+ assertNonNegative(years, "years");
211
+ if (rate === 0 || years === 0) return fv;
212
+ const n = timesPerYear;
213
+ const t = years;
214
+ return fv / (1 + rate / n) ** (n * t);
215
+ }
216
+
217
+ // src/interest/presentValueOfAnnuity.ts
218
+ function presentValueOfAnnuity(params) {
219
+ const { paymentPerPeriod, ratePerPeriod, periods, timing = "end" } = params;
220
+ assertNonNegative(paymentPerPeriod, "paymentPerPeriod");
221
+ assertNonNegative(periods, "periods");
222
+ if (periods === 0) return 0;
223
+ if (ratePerPeriod === 0) {
224
+ const pv2 = paymentPerPeriod * periods;
225
+ return timing === "begin" ? pv2 * (1 + ratePerPeriod) : pv2;
226
+ }
227
+ const pv = paymentPerPeriod * (1 - (1 + ratePerPeriod) ** -periods) / ratePerPeriod;
228
+ return timing === "begin" ? pv * (1 + ratePerPeriod) : pv;
229
+ }
230
+
231
+ // src/interest/rateToReachGoal.ts
232
+ function rateToReachGoal(params) {
233
+ const {
234
+ principal,
235
+ targetFutureValue,
236
+ periods,
237
+ contributionPerPeriod = 0,
238
+ contributionTiming = "end"
239
+ } = params;
240
+ assertNonNegative(principal, "principal");
241
+ assertNonNegative(targetFutureValue, "targetFutureValue");
242
+ assertPositive(periods, "periods");
243
+ assertNonNegative(contributionPerPeriod, "contributionPerPeriod");
244
+ if (periods === 0) return 0;
245
+ if (targetFutureValue <= principal && contributionPerPeriod === 0) return 0;
246
+ if (contributionPerPeriod === 0) {
247
+ if (targetFutureValue <= principal) return 0;
248
+ return (targetFutureValue / principal) ** (1 / periods) - 1;
249
+ }
250
+ 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++) {
254
+ const onePlusR = 1 + r;
255
+ const onePlusRN = onePlusR ** periods;
256
+ 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;
272
+ }
273
+
274
+ // src/interest/realReturn.ts
275
+ function realReturn(params) {
276
+ const { nominalReturn, inflationRate } = params;
277
+ assertFiniteNumber(nominalReturn, "nominalReturn");
278
+ assertFiniteNumber(inflationRate, "inflationRate");
279
+ if (inflationRate <= -1) return NaN;
280
+ return (1 + nominalReturn) / (1 + inflationRate) - 1;
281
+ }
282
+
283
+ // src/interest/ruleOf72.ts
284
+ function ruleOf72(params) {
285
+ const { rate, constant = 72 } = params;
286
+ assertFiniteNumber(rate, "rate");
287
+ assertPositive(rate, "rate");
288
+ return constant / 100 / rate;
289
+ }
290
+
291
+ // src/loans/loanPayment.ts
292
+ function loanPayment(params) {
293
+ const { principal, annualRate, paymentsPerYear, years } = params;
294
+ assertNonNegative(principal, "principal");
295
+ assertPositive(paymentsPerYear, "paymentsPerYear");
296
+ assertNonNegative(years, "years");
297
+ const n = Math.round(paymentsPerYear * years);
298
+ if (n === 0) return 0;
299
+ const r = annualRate / paymentsPerYear;
300
+ if (r === 0) return principal / n;
301
+ return r * principal / (1 - (1 + r) ** -n);
302
+ }
303
+
304
+ // src/loans/amortizationSchedule.ts
305
+ function amortizationSchedule(params) {
306
+ const { principal, annualRate, paymentsPerYear, years, extraPaymentPerPeriod = 0 } = params;
307
+ assertNonNegative(principal, "principal");
308
+ assertPositive(paymentsPerYear, "paymentsPerYear");
309
+ assertNonNegative(years, "years");
310
+ assertNonNegative(extraPaymentPerPeriod, "extraPaymentPerPeriod");
311
+ const scheduledN = Math.round(paymentsPerYear * years);
312
+ if (scheduledN === 0 || principal === 0) {
313
+ return { paymentPerPeriod: 0, schedule: [], totalPaid: 0, totalInterest: 0 };
314
+ }
315
+ const basePayment = loanPayment({ principal, annualRate, paymentsPerYear, years });
316
+ const r = annualRate / paymentsPerYear;
317
+ const schedule = [];
318
+ let balance = principal;
319
+ let totalPaid = 0;
320
+ let totalInterest = 0;
321
+ for (let period = 1; period <= scheduledN && balance > 0; period++) {
322
+ const interestPayment = r === 0 ? 0 : balance * r;
323
+ let payment = basePayment + extraPaymentPerPeriod;
324
+ let principalPayment = payment - interestPayment;
325
+ if (principalPayment > balance) {
326
+ principalPayment = balance;
327
+ payment = principalPayment + interestPayment;
328
+ }
329
+ balance = balance - principalPayment;
330
+ if (Math.abs(balance) < 1e-12) balance = 0;
331
+ totalPaid += payment;
332
+ totalInterest += interestPayment;
333
+ schedule.push({
334
+ period,
335
+ payment,
336
+ principalPayment,
337
+ interestPayment,
338
+ balance
339
+ });
340
+ }
341
+ return {
342
+ paymentPerPeriod: basePayment,
343
+ schedule,
344
+ totalPaid,
345
+ totalInterest
346
+ };
347
+ }
348
+
349
+ // src/loans/payoffPeriodWithExtra.ts
350
+ function payoffPeriodWithExtra(params) {
351
+ const { principal, annualRate, paymentsPerYear, basePaymentPerPeriod, extraPaymentPerPeriod } = params;
352
+ assertNonNegative(principal, "principal");
353
+ assertFiniteNumber(annualRate, "annualRate");
354
+ assertPositive(paymentsPerYear, "paymentsPerYear");
355
+ assertNonNegative(basePaymentPerPeriod, "basePaymentPerPeriod");
356
+ assertNonNegative(extraPaymentPerPeriod, "extraPaymentPerPeriod");
357
+ if (principal <= 0) return 0;
358
+ const payment = basePaymentPerPeriod + extraPaymentPerPeriod;
359
+ if (payment <= 0) return Number.POSITIVE_INFINITY;
360
+ const r = annualRate / paymentsPerYear;
361
+ let balance = principal;
362
+ let period = 0;
363
+ while (balance > 1e-12 && period < 1e5) {
364
+ const interest = r > 0 ? balance * r : 0;
365
+ if (payment <= interest) return Number.POSITIVE_INFINITY;
366
+ const principalPaid = Math.min(payment - interest, balance);
367
+ balance -= principalPaid;
368
+ period++;
369
+ }
370
+ return balance <= 1e-12 ? period : Number.POSITIVE_INFINITY;
371
+ }
372
+
373
+ // src/loans/remainingBalance.ts
374
+ function remainingBalance(params) {
375
+ const { principal, annualRate, paymentsPerYear, years, afterPeriodNumber } = params;
376
+ assertNonNegative(principal, "principal");
377
+ assertPositive(paymentsPerYear, "paymentsPerYear");
378
+ assertNonNegative(years, "years");
379
+ assertNonNegative(afterPeriodNumber, "afterPeriodNumber");
380
+ const n = Math.round(paymentsPerYear * years);
381
+ if (n === 0 || afterPeriodNumber >= n) return 0;
382
+ if (afterPeriodNumber <= 0) return principal;
383
+ const payment = loanPayment({ principal, annualRate, paymentsPerYear, years });
384
+ const r = annualRate / paymentsPerYear;
385
+ const k = afterPeriodNumber;
386
+ if (r === 0) return Math.max(0, principal - payment * k);
387
+ const balance = principal * (1 + r) ** k - payment * (((1 + r) ** k - 1) / r);
388
+ return Math.max(0, balance);
389
+ }
390
+
391
+ // src/utils/roundToCurrency.ts
392
+ function roundToCurrency(params) {
393
+ const { value, decimals = 2, mode = "half-up" } = params;
394
+ assertFiniteNumber(value, "value");
395
+ const d = Math.max(0, Math.floor(decimals));
396
+ const factor = 10 ** d;
397
+ if (mode === "half-even") {
398
+ const scaled = value * factor;
399
+ const rounded = Math.round(scaled);
400
+ const remainder = Math.abs(scaled - rounded);
401
+ if (remainder === 0.5) {
402
+ const down = Math.floor(scaled);
403
+ return (down % 2 === 0 ? down : down + 1) / factor;
404
+ }
405
+ return rounded / factor;
406
+ }
407
+ return Math.round(value * factor) / factor;
408
+ }
409
+
410
+ exports.amortizationSchedule = amortizationSchedule;
411
+ exports.cagr = cagr;
412
+ exports.compound = compound;
413
+ exports.effectiveAnnualRate = effectiveAnnualRate;
414
+ exports.futureValue = futureValue;
415
+ exports.inflationAdjustedAmount = inflationAdjustedAmount;
416
+ exports.investmentGrowth = investmentGrowth;
417
+ exports.irr = irr;
418
+ exports.loanPayment = loanPayment;
419
+ exports.paymentFromPresentValue = paymentFromPresentValue;
420
+ exports.payoffPeriodWithExtra = payoffPeriodWithExtra;
421
+ exports.periodsToReachGoal = periodsToReachGoal;
422
+ exports.presentValue = presentValue;
423
+ exports.presentValueOfAnnuity = presentValueOfAnnuity;
424
+ exports.rateToReachGoal = rateToReachGoal;
425
+ exports.realReturn = realReturn;
426
+ exports.remainingBalance = remainingBalance;
427
+ exports.roundToCurrency = roundToCurrency;
428
+ exports.ruleOf72 = ruleOf72;
429
+ //# sourceMappingURL=index.cjs.map
430
+ //# sourceMappingURL=index.cjs.map