Completed
Branch develop (30080e)
by
unknown
16:47
created
htdocs/includes/markrogoyski/math-php/src/Finance.php 1 patch
Indentation   +770 added lines, -770 removed lines patch added patch discarded remove patch
@@ -5,776 +5,776 @@
 block discarded – undo
5 5
 use MathPHP\Exception\OutOfBoundsException;
6 6
 
7 7
 /**
8
-  * General references on financial functions and formulas:
9
-  * - Open Document Format for Office Applications (OpenDocument) Version 1.2 Part 2:
10
-  *   Recalculated Formula (OpenFormula) Format. 29 September 2011. OASIS Standard.
11
-  *   http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part2.html#__RefHeading__1018228_715980110
12
-  * - https://wiki.openoffice.org/wiki/Documentation/How_Tos/Calc:_Derivation_of_Financial_Formulas#Loans_and_Annuities
13
-  */
8
+ * General references on financial functions and formulas:
9
+ * - Open Document Format for Office Applications (OpenDocument) Version 1.2 Part 2:
10
+ *   Recalculated Formula (OpenFormula) Format. 29 September 2011. OASIS Standard.
11
+ *   http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part2.html#__RefHeading__1018228_715980110
12
+ * - https://wiki.openoffice.org/wiki/Documentation/How_Tos/Calc:_Derivation_of_Financial_Formulas#Loans_and_Annuities
13
+ */
14 14
 class Finance
15 15
 {
16
-    /**
17
-     * Floating-point range near zero to consider insignificant.
18
-     */
19
-    public const EPSILON = 1e-6;
20
-
21
-    /**
22
-     * Consider any floating-point value less than epsilon from zero as zero,
23
-     * ie any value in the range [-epsilon < 0 < epsilon] is considered zero.
24
-     * Also used to convert -0.0 to 0.0.
25
-     *
26
-     * @param float $value
27
-     * @param float $epsilon
28
-     *
29
-     * @return float
30
-     */
31
-    private static function checkZero(float $value, float $epsilon = self::EPSILON): float
32
-    {
33
-        return \abs($value) < $epsilon ? 0.0 : $value;
34
-    }
35
-
36
-    /**
37
-     * Financial payment for a loan or annuity with compound interest.
38
-     * Determines the periodic payment amount for a given interest rate,
39
-     * principal, targeted payment goal, life of the annuity as number
40
-     * of payments, and whether the payments are made at the start or end
41
-     * of each payment period.
42
-     *
43
-     * Same as the =PMT() function in most spreadsheet software.
44
-     *
45
-     * The basic monthly payment formula derivation:
46
-     * https://en.wikipedia.org/wiki/Mortgage_calculator#Monthly_payment_formula
47
-     *
48
-     *       rP(1+r)ᴺ
49
-     * PMT = --------
50
-     *       (1+r)ᴺ-1
51
-     *
52
-     * The formula is adjusted to allow targeting any future value rather than 0.
53
-     * The 1/(1+r*when) factor adjusts the payment to the beginning or end
54
-     * of the period. In the common case of a payment at the end of a period,
55
-     * the factor is 1 and reduces to the formula above. Setting when=1 computes
56
-     * an "annuity due" with an immediate payment.
57
-     *
58
-     * Examples:
59
-     * The payment on a 30-year fixed mortgage note of $265000 at 3.5% interest
60
-     * paid at the end of every month.
61
-     *   pmt(0.035/12, 30*12, 265000, 0, false)
62
-     *
63
-     * The payment on a 30-year fixed mortgage note of $265000 at 3.5% interest
64
-     * needed to half the principal in half in 5 years:
65
-     *   pmt(0.035/12, 5*12, 265000, 265000/2, false)
66
-     *
67
-     * The weekly payment into a savings account with 1% interest rate and current
68
-     * balance of $1500 needed to reach $10000 after 3 years:
69
-     *   pmt(0.01/52, 3*52, -1500, 10000, false)
70
-     * The present_value is negative indicating money put into the savings account,
71
-     * whereas future_value is positive, indicating money that will be withdrawn from
72
-     * the account. Similarly, the payment value is negative
73
-     *
74
-     * How much money can be withdrawn at the end of every quarter from an account
75
-     * with $1000000 earning 4% so the money lasts 20 years:
76
-     *  pmt(0.04/4, 20*4, 1000000, 0, false)
77
-     *
78
-     * @param  float $rate
79
-     * @param  int   $periods
80
-     * @param  float $present_value
81
-     * @param  float $future_value
82
-     * @param  bool  $beginning adjust the payment to the beginning or end of the period
83
-     *
84
-     * @return float
85
-     */
86
-    public static function pmt(float $rate, int $periods, float $present_value, float $future_value = 0.0, bool $beginning = false): float
87
-    {
88
-        $when = $beginning ? 1 : 0;
89
-
90
-        if ($rate == 0) {
91
-            return - ($future_value + $present_value) / $periods;
92
-        }
93
-
94
-        return - ($future_value + ($present_value * \pow(1 + $rate, $periods)))
95
-            /
96
-            ((1 + $rate * $when) / $rate * (\pow(1 + $rate, $periods) - 1));
97
-    }
98
-
99
-    /**
100
-     * Interest on a financial payment for a loan or annuity with compound interest.
101
-     * Determines the interest payment at a particular period of the annuity. For
102
-     * a typical loan paid down to zero, the amount of interest and principle paid
103
-     * throughout the lifetime of the loan will change, with the interest portion
104
-     * of the payment decreasing over time as the loan principle decreases.
105
-     *
106
-     * Same as the =IPMT() function in most spreadsheet software.
107
-     *
108
-     * See the PMT function for derivation of the formula. For IPMT, we have
109
-     * the payment equal to the interest portion and principle portion of the payment:
110
-     *
111
-     * PMT = IPMT + PPMT
112
-     *
113
-     * The interest portion IPMT on a regular annuity can be calculated by computing
114
-     * the future value of the annuity for the prior period and computing the compound
115
-     * interest for one period:
116
-     *
117
-     * IPMT = FV(p=n-1) * rate
118
-     *
119
-     * For an "annuity due" where payment is at the start of the period, period=1 has
120
-     * no interest portion of the payment because no time has elapsed for compounding.
121
-     * To compute the interest portion of the payment, the future value of 2 periods
122
-     * back needs to be computed, as the definition of a period is different, giving:
123
-     *
124
-     * IPMT = (FV(p=n-2) - PMT) * rate
125
-     *
126
-     * By thinking of the future value at period 0 instead of the present value, the
127
-     * given formulas are computed.
128
-     *
129
-     * Example of regular annuity and annuity due for a loan of $10.00 paid back in 3 periods.
130
-     * Although the principle payments are equal, the total payment and interest portion are
131
-     * lower with the annuity due because a principle payment is made immediately.
132
-     *
133
-     *                       Regular Annuity |  Annuity Due
134
-     * Period   FV       PMT    IPMT   PPMT  |   PMT    IPMT    PPMT
135
-     *   0     -10.00                        |
136
-     *   1      -6.83   -3.67  -0.50  -3.17  |  -3.50   0.00   -3.50
137
-     *   2      -3.50   -3.67  -0.34  -3.33  |  -3.50  -0.33   -3.17
138
-     *   3       0.00   -3.67  -0.17  -3.50  |  -3.50  -0.17   -3.33
139
-     *                -----------------------|----------------------
140
-     *             SUM -11.01  -1.01 -10.00  | -10.50  -0.50  -10.00
141
-     *
142
-     * Examples:
143
-     * The interest on a payment on a 30-year fixed mortgage note of $265000 at 3.5% interest
144
-     * paid at the end of every month, looking at the first payment:
145
-     *   ipmt(0.035/12, 1, 30*12, 265000, 0, false)
146
-     *
147
-     * @param  float $rate
148
-     * @param  int   $period
149
-     * @param  int   $periods
150
-     * @param  float $present_value
151
-     * @param  float $future_value
152
-     * @param  bool  $beginning adjust the payment to the beginning or end of the period
153
-     *
154
-     * @return float
155
-     */
156
-    public static function ipmt(float $rate, int $period, int $periods, float $present_value, float $future_value = 0.0, bool $beginning = false): float
157
-    {
158
-        if ($period < 1 || $period > $periods) {
159
-            return \NAN;
160
-        }
161
-
162
-        if ($rate == 0) {
163
-            return 0;
164
-        }
165
-
166
-        if ($beginning && $period == 1) {
167
-            return 0.0;
168
-        }
169
-
170
-        $payment = self::pmt($rate, $periods, $present_value, $future_value, $beginning);
171
-        if ($beginning) {
172
-            $interest = (self::fv($rate, $period - 2, $payment, $present_value, $beginning) - $payment) * $rate;
173
-        } else {
174
-            $interest = self::fv($rate, $period - 1, $payment, $present_value, $beginning) * $rate;
175
-        }
176
-
177
-        return self::checkZero($interest);
178
-    }
179
-
180
-    /**
181
-     * Principle on a financial payment for a loan or annuity with compound interest.
182
-     * Determines the principle payment at a particular period of the annuity. For
183
-     * a typical loan paid down to zero, the amount of interest and principle paid
184
-     * throughout the lifetime of the loan will change, with the principle portion
185
-     * of the payment increasing over time as the loan principle decreases.
186
-     *
187
-     * Same as the =PPMT() function in most spreadsheet software.
188
-     *
189
-     * See the PMT function for derivation of the formula.
190
-     * See the IPMT function for derivation and use of PMT, IPMT, and PPMT.
191
-     *
192
-     * With derivations for PMT and IPMT, we simply compute:
193
-     *
194
-     * PPMT = PMT - IPMT
195
-     *
196
-     * Examples:
197
-     * The principle on a payment on a 30-year fixed mortgage note of $265000 at 3.5% interest
198
-     * paid at the end of every month, looking at the first payment:
199
-     *   ppmt(0.035/12, 1, 30*12, 265000, 0, false)
200
-     *
201
-     * @param  float $rate
202
-     * @param  int   $period
203
-     * @param  int   $periods
204
-     * @param  float $present_value
205
-     * @param  float $future_value
206
-     * @param  bool  $beginning adjust the payment to the beginning or end of the period
207
-     *
208
-     * @return float
209
-     */
210
-    public static function ppmt(float $rate, int $period, int $periods, float $present_value, float $future_value = 0.0, bool $beginning = false): float
211
-    {
212
-        $payment = self::pmt($rate, $periods, $present_value, $future_value, $beginning);
213
-        $ipmt    = self::ipmt($rate, $period, $periods, $present_value, $future_value, $beginning);
214
-
215
-        return $payment - $ipmt;
216
-    }
217
-
218
-    /**
219
-     * Number of payment periods of an annuity.
220
-     * Solves for the number of periods in the annuity formula.
221
-     *
222
-     * Same as the =NPER() function in most spreadsheet software.
223
-     *
224
-     * Solving the basic annuity formula for number of periods:
225
-     *        log(PMT - FV*r)
226
-     *        ---------------
227
-     *        log(PMT + PV*r)
228
-     * n = --------------------
229
-     *          log(1 + r)
230
-     *
231
-     * The (1+r*when) factor adjusts the payment to the beginning or end
232
-     * of the period. In the common case of a payment at the end of a period,
233
-     * the factor is 1 and reduces to the formula above. Setting when=1 computes
234
-     * an "annuity due" with an immediate payment.
235
-     *
236
-     * Examples:
237
-     * The number of periods of a $475000 mortgage with interest rate 3.5% and monthly
238
-     * payment of $2132.96  paid in full:
239
-     *   nper(0.035/12, -2132.96, 475000, 0)
240
-     *
241
-     * @param  float $rate
242
-     * @param  float $payment
243
-     * @param  float $present_value
244
-     * @param  float $future_value
245
-     * @param  bool  $beginning adjust the payment to the beginning or end of the period
246
-     *
247
-     * @return float
248
-     */
249
-    public static function periods(float $rate, float $payment, float $present_value, float $future_value, bool $beginning = false): float
250
-    {
251
-        $when = $beginning ? 1 : 0;
252
-
253
-        if ($rate == 0) {
254
-            return - ($present_value + $future_value) / $payment;
255
-        }
256
-
257
-        $initial = $payment * (1.0 + $rate * $when);
258
-        return \log(($initial - $future_value * $rate) / ($initial + $present_value * $rate)) / \log(1.0 + $rate);
259
-    }
260
-
261
-    /**
262
-     * Annual Equivalent Rate (AER) of an annual percentage rate (APR).
263
-     * The effective yearly rate of an annual percentage rate when the
264
-     * annual percentage rate is compounded periodically within the year.
265
-     *
266
-     * Same as the =EFFECT() function in most spreadsheet software.
267
-     *
268
-     * The formula:
269
-     * https://en.wikipedia.org/wiki/Effective_interest_rate
270
-     *
271
-     *        /     i \ ᴺ
272
-     * AER = | 1 +  -  |  - 1
273
-     *        \     n /
274
-     *
275
-     * Examples:
276
-     * The AER of APR 3.5% interest compounded monthly.
277
-     *   aer(0.035, 12)
278
-     *
279
-     * @param  float $nominal
280
-     * @param  int $periods
281
-     *
282
-     * @return float
283
-     */
284
-    public static function aer(float $nominal, int $periods): float
285
-    {
286
-        if ($periods == 1) {
287
-            return $nominal;
288
-        }
289
-
290
-        return \pow(1 + ($nominal / $periods), $periods) - 1;
291
-    }
292
-
293
-    /**
294
-     * Annual Nominal Rate of an annual effective rate (AER).
295
-     * The nominal yearly rate of an annual effective rate when the
296
-     * annual effective rate is compounded periodically within the year.
297
-     *
298
-     * Same as the =NOMINAL() function in most spreadsheet software.
299
-     *
300
-     * See:
301
-     * https://en.wikipedia.org/wiki/Nominal_interest_rate
302
-     *
303
-     *           /          1/N    \
304
-     * NOMINAL = | (AER + 1)    -1 | * N
305
-     *           \                 /
306
-     *
307
-     * Examples:
308
-     * The nominal rate of AER 3.557% interest compounded monthly.
309
-     *   nominal(0.03557, 12)
310
-     *
311
-     * @param  float $aer
312
-     * @param  int $periods
313
-     *
314
-     * @return float
315
-     */
316
-    public static function nominal(float $aer, int $periods): float
317
-    {
318
-        if ($periods == 1) {
319
-            return $aer;
320
-        }
321
-
322
-        return (\pow($aer + 1, 1 / $periods) - 1) * $periods;
323
-    }
324
-
325
-    /**
326
-     * Future value for a loan or annuity with compound interest.
327
-     *
328
-     * Same as the =FV() function in most spreadsheet software.
329
-     *
330
-     * The basic future-value formula derivation:
331
-     * https://en.wikipedia.org/wiki/Future_value
332
-     *
333
-     *                   PMT*((1+r)ᴺ - 1)
334
-     * FV = -PV*(1+r)ᴺ - ----------------
335
-     *                          r
336
-     *
337
-     * The (1+r*when) factor adjusts the payment to the beginning or end
338
-     * of the period. In the common case of a payment at the end of a period,
339
-     * the factor is 1 and reduces to the formula above. Setting when=1 computes
340
-     * an "annuity due" with an immediate payment.
341
-     *
342
-     * Examples:
343
-     * The future value in 5 years on a 30-year fixed mortgage note of $265000
344
-     * at 3.5% interest paid at the end of every month. This is how much loan
345
-     * principle would be outstanding:
346
-     *   fv(0.035/12, 5*12, 1189.97, -265000, false)
347
-     *
348
-     * The present_value is negative indicating money borrowed for the mortgage,
349
-     * whereas payment is positive, indicating money that will be paid to the
350
-     * mortgage.
351
-     *
352
-     * @param  float $rate
353
-     * @param  int   $periods
354
-     * @param  float $payment
355
-     * @param  float $present_value
356
-     * @param  bool  $beginning adjust the payment to the beginning or end of the period
357
-     *
358
-     * @return float
359
-     */
360
-    public static function fv(float $rate, int $periods, float $payment, float $present_value, bool $beginning = false): float
361
-    {
362
-        $when = $beginning ? 1 : 0;
363
-
364
-        if ($rate == 0) {
365
-            $fv = -($present_value + ($payment * $periods));
366
-            return self::checkZero($fv);
367
-        }
368
-
369
-        $initial  = 1 + ($rate * $when);
370
-        $compound = \pow(1 + $rate, $periods);
371
-        $fv       = - (($present_value * $compound) + (($payment * $initial * ($compound - 1)) / $rate));
372
-
373
-        return self::checkZero($fv);
374
-    }
375
-
376
-    /**
377
-     * Present value for a loan or annuity with compound interest.
378
-     *
379
-     * Same as the =PV() function in most spreadsheet software.
380
-     *
381
-     * The basic present-value formula derivation:
382
-     * https://en.wikipedia.org/wiki/Present_value
383
-     *
384
-     *            PMT*((1+r)ᴺ - 1)
385
-     * PV = -FV - ----------------
386
-     *                   r
387
-     *      ---------------------
388
-     *             (1 + r)ᴺ
389
-     *
390
-     * The (1+r*when) factor adjusts the payment to the beginning or end
391
-     * of the period. In the common case of a payment at the end of a period,
392
-     * the factor is 1 and reduces to the formula above. Setting when=1 computes
393
-     * an "annuity due" with an immediate payment.
394
-     *
395
-     * Examples:
396
-     * The present value of a bond's $1000 face value paid in 5 year's time
397
-     * with a constant discount rate of 3.5% compounded monthly:
398
-     *   pv(0.035/12, 5*12, 0, -1000, false)
399
-     *
400
-     * The present value of a $1000 5-year bond that pays a fixed 7% ($70)
401
-     * coupon at the end of each year with a discount rate of 5%:
402
-     *   pv(0.5, 5, -70, -1000, false)
403
-     *
404
-     * The payment and future_value is negative indicating money paid out.
405
-     *
406
-     * @param  float $rate
407
-     * @param  int   $periods
408
-     * @param  float $payment
409
-     * @param  float $future_value
410
-     * @param  bool  $beginning adjust the payment to the beginning or end of the period
411
-     *
412
-     * @return float
413
-     */
414
-    public static function pv(float $rate, int $periods, float $payment, float $future_value, bool $beginning = false): float
415
-    {
416
-        $when = $beginning ? 1 : 0;
417
-
418
-        if ($rate == 0) {
419
-            $pv = -$future_value - ($payment * $periods);
420
-            return self::checkZero($pv);
421
-        }
422
-
423
-        $initial  = 1 + ($rate * $when);
424
-        $compound = \pow(1 + $rate, $periods);
425
-        $pv       = (-$future_value - (($payment * $initial * ($compound - 1)) / $rate)) / $compound;
426
-
427
-        return self::checkZero($pv);
428
-    }
429
-
430
-    /**
431
-     * Net present value of cash flows. Cash flows are periodic starting
432
-     * from an initial time and with a uniform discount rate.
433
-     *
434
-     * Similar to the =NPV() function in most spreadsheet software, except
435
-     * the initial (usually negative) cash flow at time 0 is given as the
436
-     * first element of the array rather than subtracted. For example,
437
-     *   spreadsheet: =NPV(0.01, 100, 200, 300, 400) - 1000
438
-     * is done as
439
-     *   MathPHP::npv(0.01, [-1000, 100, 200, 300, 400])
440
-     *
441
-     * The basic net-present-value formula derivation:
442
-     * https://en.wikipedia.org/wiki/Net_present_value
443
-     *
444
-     *  n      Rt
445
-     *  Σ   --------
446
-     * t=0  (1 / r)ᵗ
447
-     *
448
-     * Examples:
449
-     * The net present value of 5 yearly cash flows after an initial $1000
450
-     * investment with a 3% discount rate:
451
-     *  npv(0.03, [-1000, 100, 500, 300, 700, 700])
452
-     *
453
-     * @param  float $rate
454
-     * @param  array<float> $values
455
-     *
456
-     * @return float
457
-     */
458
-    public static function npv(float $rate, array $values): float
459
-    {
460
-        $result = 0.0;
461
-
462
-        for ($i = 0; $i < \count($values); ++$i) {
463
-            $result += $values[$i] / (1 + $rate) ** $i;
464
-        }
465
-
466
-        return $result;
467
-    }
468
-
469
-    /**
470
-     * Interest rate per period of an Annuity.
471
-     *
472
-     * Same as the =RATE() formula in most spreadsheet software.
473
-     *
474
-     * The basic rate formula derivation is to solve for the future value
475
-     * taking into account the present value:
476
-     * https://en.wikipedia.org/wiki/Future_value
477
-     *
478
-     *                        ((1+r)ᴺ - 1)
479
-     * FV + PV*(1+r)ᴺ + PMT * ------------ = 0
480
-     *                             r
481
-     * The (1+r*when) factor adjusts the payment to the beginning or end
482
-     * of the period. In the common case of a payment at the end of a period,
483
-     * the factor is 1 and reduces to the formula above. Setting when=1 computes
484
-     * an "annuity due" with an immediate payment.
485
-     *
486
-     * Not all solutions for the rate have real-value solutions or converge.
487
-     * In these cases, NAN is returned.
488
-     *
489
-     * @param  float $periods
490
-     * @param  float $payment
491
-     * @param  float $present_value
492
-     * @param  float $future_value
493
-     * @param  bool  $beginning
494
-     * @param  float $initial_guess
495
-     *
496
-     * @return float
497
-     */
498
-    public static function rate(float $periods, float $payment, float $present_value, float $future_value, bool $beginning = false, float $initial_guess = 0.1): float
499
-    {
500
-        $when = $beginning ? 1 : 0;
501
-
502
-        $func = function ($x, $periods, $payment, $present_value, $future_value, $when) {
503
-            return $future_value + $present_value * (1 + $x) ** $periods + $payment * (1 + $x * $when) / $x * ((1 + $x) ** $periods - 1);
504
-        };
505
-
506
-        return self::checkZero(NumericalAnalysis\RootFinding\NewtonsMethod::solve($func, [$initial_guess, $periods, $payment, $present_value, $future_value, $when], 0, self::EPSILON, 0));
507
-    }
508
-
509
-    /**
510
-     * Internal rate of return.
511
-     * Periodic rate of return that would provide a net-present value (NPV) of 0.
512
-     *
513
-     * Same as =IRR formula in most spreadsheet software.
514
-     *
515
-     * Reference:
516
-     * https://en.wikipedia.org/wiki/Internal_rate_of_return
517
-     *
518
-     * Examples:
519
-     * The rate of return of an initial investment of $100 with returns
520
-     * of $50, $40, and $30:
521
-     *  irr([-100, 50, 40, 30])
522
-     *
523
-     * Solves for NPV=0 using Newton's Method.
524
-     * @param array<float> $values
525
-     * @param float $initial_guess
526
-     *
527
-     * @return float
528
-     *
529
-     * @throws OutOfBoundsException
530
-     *
531
-     * @todo: Use eigenvalues to find the roots of a characteristic polynomial.
532
-     * This will allow finding all solutions and eliminate the need of the initial_guess.
533
-     */
534
-    public static function irr(array $values, float $initial_guess = 0.1): float
535
-    {
536
-        $func = function ($x, $values) {
537
-            return Finance::npv($x, $values);
538
-        };
539
-
540
-        if (\count($values) <= 1) {
541
-            return \NAN;
542
-        }
543
-
544
-        $root = NumericalAnalysis\RootFinding\NewtonsMethod::solve($func, [$initial_guess, $values], 0, self::EPSILON, 0);
545
-        if (!\is_nan($root)) {
546
-            return self::CheckZero($root);
547
-        }
548
-        return self::checkZero(self::alternateIrr($values));
549
-    }
550
-
551
-    /**
552
-     * Alternate IRR implementation.
553
-     *
554
-     * A more numerically stable implementation that converges to only one value.
555
-     *
556
-     * Based off of Better: https://github.com/better/irr
557
-     *
558
-     * @param  array<float> $values
559
-     *
560
-     * @return float
561
-     */
562
-    private static function alternateIrr(array $values): float
563
-    {
564
-        $rate = 0.0;
565
-        for ($iter = 0; $iter < 100; $iter++) {
566
-            $m = -1000;
567
-            for ($i = 0; $i < \count($values); $i++) {
568
-                $m = \max($m, -$rate * $i);
569
-            }
570
-            $f = [];
571
-            for ($i = 0; $i < \count($values); $i++) {
572
-                $f[$i] = \exp(-$rate * $i - $m);
573
-            }
574
-            $t = 0;
575
-            for ($i = 0; $i < \count($values); $i++) {
576
-                $t += $f[$i] * $values[$i];
577
-            }
578
-            if (\abs($t) < (self::EPSILON * \exp($m))) {
579
-                break;
580
-            }
581
-            $u = 0;
582
-            for ($i = 0; $i < \count($values); $i++) {
583
-                $u += $f[$i] * $i * $values[$i];
584
-            }
585
-            if ($u == 0) {
586
-                return \NAN;
587
-            }
588
-            $rate += $t / $u;
589
-        }
590
-        return \exp($rate) - 1;
591
-    }
592
-
593
-    /**
594
-     * Modified internal rate of return.
595
-     * Rate of return that discounts outflows (investments) at the financing rate,
596
-     * and reinvests inflows with an expected rate of return.
597
-     *
598
-     * Same as =MIRR formula in most spreadsheet software.
599
-     *
600
-     * The formula derivation:
601
-     * https://en.wikipedia.org/wiki/Modified_internal_rate_of_return
602
-     *
603
-     *       _____________________________
604
-     *     n/ FV(re-invested cash inflows)
605
-     *  -  /  ----------------------------  - 1.0
606
-     *   \/   PV(discounted cash outflows)
607
-     *
608
-     * Examples:
609
-     * The rate of return of an initial investment of $100 at 5% financing
610
-     * with returns of $50, $40, and $30 reinvested at 10%:
611
-     *  mirr([-100, 50, 40, 30], 0.05, 0.10)
612
-     *
613
-     * @param  array<float> $values
614
-     * @param  float $finance_rate
615
-     * @param  float $reinvestment_rate
616
-     *
617
-     * @return float
618
-     */
619
-    public static function mirr(array $values, float $finance_rate, float $reinvestment_rate): float
620
-    {
621
-        $inflows  = array();
622
-        $outflows = array();
623
-
624
-        for ($i = 0; $i < \count($values); $i++) {
625
-            if ($values[$i] >= 0) {
626
-                $inflows[]  = $values[$i];
627
-                $outflows[] = 0;
628
-            } else {
629
-                $inflows[]  = 0;
630
-                $outflows[] = $values[$i];
631
-            }
632
-        }
633
-
634
-        $nonzero = function ($x) {
635
-            return $x != 0;
636
-        };
637
-
638
-        if (\count(\array_filter($inflows, $nonzero)) == 0 || \count(\array_filter($outflows, $nonzero)) == 0) {
639
-            return \NAN;
640
-        }
641
-
642
-        $root        = \count($values) - 1;
643
-        $pv_inflows  = self::npv($reinvestment_rate, $inflows);
644
-        $fv_inflows  = self::fv($reinvestment_rate, $root, 0, -$pv_inflows);
645
-        $pv_outflows = self::npv($finance_rate, $outflows);
646
-
647
-        return self::checkZero(\pow($fv_inflows / -$pv_outflows, 1 / $root) - 1);
648
-    }
649
-
650
-    /**
651
-     * Discounted Payback of an investment.
652
-     * The number of periods to recoup cash outlays of an investment.
653
-     *
654
-     * This is commonly used with discount rate=0 as simple payback period,
655
-     * but it is not a real financial measurement when it doesn't consider the
656
-     * discount rate. Even with a discount rate, it doesn't consider the cost
657
-     * of capital or re-investment of returns.
658
-     *
659
-     * Avoid this when possible. Consider NPV, MIRR, IRR, and other financial
660
-     * functions.
661
-     *
662
-     * Reference:
663
-     * https://en.wikipedia.org/wiki/Payback_period
664
-     *
665
-     * The result is given assuming cash flows are continous throughout a period.
666
-     * To compute payback in terms of whole periods, use ceil() on the result.
667
-     *
668
-     * An investment could reach its payback period before future cash outlays occur.
669
-     * The payback period returned is defined to be the final point at which the
670
-     * sum of returns becomes positive.
671
-     *
672
-     * Examples:
673
-     * The payback period of an investment with a $1,000 investment and future returns
674
-     * of $100, $200, $300, $400, $500:
675
-     *  payback([-1000, 100, 200, 300, 400, 500])
676
-     *
677
-     * The discounted payback period of an investment with a $1,000 investment, future returns
678
-     * of $100, $200, $300, $400, $500, and a discount rate of 0.10:
679
-     *  payback([-1000, 100, 200, 300, 400, 500], 0.1)
680
-     *
681
-     * @param  array<float> $values
682
-     * @param  float $rate
683
-     *
684
-     * @return float
685
-     */
686
-    public static function payback(array $values, float $rate = 0.0): float
687
-    {
688
-        $last_outflow = -1;
689
-        for ($i = 0; $i < \count($values); $i++) {
690
-            if ($values[$i] < 0) {
691
-                $last_outflow = $i;
692
-            }
693
-        }
694
-
695
-        if ($last_outflow < 0) {
696
-            return 0.0;
697
-        }
698
-
699
-        $sum            = $values[0];
700
-        $payback_period = -1;
701
-
702
-        for ($i = 1; $i < \count($values); $i++) {
703
-            $prevsum         = $sum;
704
-            $discounted_flow = $values[$i] / (1 + $rate) ** $i;
705
-            $sum            += $discounted_flow;
706
-            if ($sum >= 0) {
707
-                if ($i > $last_outflow) {
708
-                    return ($i - 1) + (-$prevsum / $discounted_flow);
709
-                }
710
-                if ($payback_period == -1) {
711
-                    $payback_period = ($i - 1) + (-$prevsum / $discounted_flow);
712
-                }
713
-            } else {
714
-                $payback_period = -1;
715
-            }
716
-        }
717
-        if ($sum >= 0) {
718
-            return $payback_period;
719
-        }
720
-
721
-        return \NAN;
722
-    }
723
-
724
-    /**
725
-     * Profitability Index.
726
-     * The Profitability Index, also referred to as Profit Investment
727
-     * Ratio (PIR) and Value Investment Ratio (VIR), is a comparison of
728
-     * discounted cash inflows to discounted cash outflows. It can be
729
-     * used as a decision criteria of an investment, using larger than 1
730
-     * to choose an investment, and less than 1 to pass.
731
-     *
732
-     * The formula derivation:
733
-     * https://en.wikipedia.org/wiki/Profitability_index
734
-     *
735
-     * PV(cash inflows)
736
-     * ----------------
737
-     * PV(cash outflows)
738
-     *
739
-     * The formula is usually stated in terms of the initial investmest,
740
-     * but it is generalized here to discount all future outflows.
741
-     *
742
-     * Examples:
743
-     * The profitability index of an initial $100 investment with future
744
-     * returns of $50, $50, $50 with a 10% discount rate:
745
-     *  profitabilityIndex([-100, 50, 50, 50], 0.10)
746
-     *
747
-     * @param  array<float> $values
748
-     * @param  float $rate
749
-     *
750
-     * @return float
751
-     */
752
-    public static function profitabilityIndex(array $values, float $rate): float
753
-    {
754
-        $inflows  = array();
755
-        $outflows = array();
756
-
757
-        for ($i = 0; $i < \count($values); $i++) {
758
-            if ($values[$i] >= 0) {
759
-                $inflows[]  = $values[$i];
760
-                $outflows[] = 0;
761
-            } else {
762
-                $inflows[]  = 0;
763
-                $outflows[] = -$values[$i];
764
-            }
765
-        }
766
-
767
-        $nonzero = function ($x) {
768
-            return $x != 0;
769
-        };
770
-
771
-        if (\count(\array_filter($outflows, $nonzero)) == 0) {
772
-            return \NAN;
773
-        }
774
-
775
-        $pv_inflows  = self::npv($rate, $inflows);
776
-        $pv_outflows = self::npv($rate, $outflows);
777
-
778
-        return $pv_inflows / $pv_outflows;
779
-    }
16
+	/**
17
+	 * Floating-point range near zero to consider insignificant.
18
+	 */
19
+	public const EPSILON = 1e-6;
20
+
21
+	/**
22
+	 * Consider any floating-point value less than epsilon from zero as zero,
23
+	 * ie any value in the range [-epsilon < 0 < epsilon] is considered zero.
24
+	 * Also used to convert -0.0 to 0.0.
25
+	 *
26
+	 * @param float $value
27
+	 * @param float $epsilon
28
+	 *
29
+	 * @return float
30
+	 */
31
+	private static function checkZero(float $value, float $epsilon = self::EPSILON): float
32
+	{
33
+		return \abs($value) < $epsilon ? 0.0 : $value;
34
+	}
35
+
36
+	/**
37
+	 * Financial payment for a loan or annuity with compound interest.
38
+	 * Determines the periodic payment amount for a given interest rate,
39
+	 * principal, targeted payment goal, life of the annuity as number
40
+	 * of payments, and whether the payments are made at the start or end
41
+	 * of each payment period.
42
+	 *
43
+	 * Same as the =PMT() function in most spreadsheet software.
44
+	 *
45
+	 * The basic monthly payment formula derivation:
46
+	 * https://en.wikipedia.org/wiki/Mortgage_calculator#Monthly_payment_formula
47
+	 *
48
+	 *       rP(1+r)ᴺ
49
+	 * PMT = --------
50
+	 *       (1+r)ᴺ-1
51
+	 *
52
+	 * The formula is adjusted to allow targeting any future value rather than 0.
53
+	 * The 1/(1+r*when) factor adjusts the payment to the beginning or end
54
+	 * of the period. In the common case of a payment at the end of a period,
55
+	 * the factor is 1 and reduces to the formula above. Setting when=1 computes
56
+	 * an "annuity due" with an immediate payment.
57
+	 *
58
+	 * Examples:
59
+	 * The payment on a 30-year fixed mortgage note of $265000 at 3.5% interest
60
+	 * paid at the end of every month.
61
+	 *   pmt(0.035/12, 30*12, 265000, 0, false)
62
+	 *
63
+	 * The payment on a 30-year fixed mortgage note of $265000 at 3.5% interest
64
+	 * needed to half the principal in half in 5 years:
65
+	 *   pmt(0.035/12, 5*12, 265000, 265000/2, false)
66
+	 *
67
+	 * The weekly payment into a savings account with 1% interest rate and current
68
+	 * balance of $1500 needed to reach $10000 after 3 years:
69
+	 *   pmt(0.01/52, 3*52, -1500, 10000, false)
70
+	 * The present_value is negative indicating money put into the savings account,
71
+	 * whereas future_value is positive, indicating money that will be withdrawn from
72
+	 * the account. Similarly, the payment value is negative
73
+	 *
74
+	 * How much money can be withdrawn at the end of every quarter from an account
75
+	 * with $1000000 earning 4% so the money lasts 20 years:
76
+	 *  pmt(0.04/4, 20*4, 1000000, 0, false)
77
+	 *
78
+	 * @param  float $rate
79
+	 * @param  int   $periods
80
+	 * @param  float $present_value
81
+	 * @param  float $future_value
82
+	 * @param  bool  $beginning adjust the payment to the beginning or end of the period
83
+	 *
84
+	 * @return float
85
+	 */
86
+	public static function pmt(float $rate, int $periods, float $present_value, float $future_value = 0.0, bool $beginning = false): float
87
+	{
88
+		$when = $beginning ? 1 : 0;
89
+
90
+		if ($rate == 0) {
91
+			return - ($future_value + $present_value) / $periods;
92
+		}
93
+
94
+		return - ($future_value + ($present_value * \pow(1 + $rate, $periods)))
95
+			/
96
+			((1 + $rate * $when) / $rate * (\pow(1 + $rate, $periods) - 1));
97
+	}
98
+
99
+	/**
100
+	 * Interest on a financial payment for a loan or annuity with compound interest.
101
+	 * Determines the interest payment at a particular period of the annuity. For
102
+	 * a typical loan paid down to zero, the amount of interest and principle paid
103
+	 * throughout the lifetime of the loan will change, with the interest portion
104
+	 * of the payment decreasing over time as the loan principle decreases.
105
+	 *
106
+	 * Same as the =IPMT() function in most spreadsheet software.
107
+	 *
108
+	 * See the PMT function for derivation of the formula. For IPMT, we have
109
+	 * the payment equal to the interest portion and principle portion of the payment:
110
+	 *
111
+	 * PMT = IPMT + PPMT
112
+	 *
113
+	 * The interest portion IPMT on a regular annuity can be calculated by computing
114
+	 * the future value of the annuity for the prior period and computing the compound
115
+	 * interest for one period:
116
+	 *
117
+	 * IPMT = FV(p=n-1) * rate
118
+	 *
119
+	 * For an "annuity due" where payment is at the start of the period, period=1 has
120
+	 * no interest portion of the payment because no time has elapsed for compounding.
121
+	 * To compute the interest portion of the payment, the future value of 2 periods
122
+	 * back needs to be computed, as the definition of a period is different, giving:
123
+	 *
124
+	 * IPMT = (FV(p=n-2) - PMT) * rate
125
+	 *
126
+	 * By thinking of the future value at period 0 instead of the present value, the
127
+	 * given formulas are computed.
128
+	 *
129
+	 * Example of regular annuity and annuity due for a loan of $10.00 paid back in 3 periods.
130
+	 * Although the principle payments are equal, the total payment and interest portion are
131
+	 * lower with the annuity due because a principle payment is made immediately.
132
+	 *
133
+	 *                       Regular Annuity |  Annuity Due
134
+	 * Period   FV       PMT    IPMT   PPMT  |   PMT    IPMT    PPMT
135
+	 *   0     -10.00                        |
136
+	 *   1      -6.83   -3.67  -0.50  -3.17  |  -3.50   0.00   -3.50
137
+	 *   2      -3.50   -3.67  -0.34  -3.33  |  -3.50  -0.33   -3.17
138
+	 *   3       0.00   -3.67  -0.17  -3.50  |  -3.50  -0.17   -3.33
139
+	 *                -----------------------|----------------------
140
+	 *             SUM -11.01  -1.01 -10.00  | -10.50  -0.50  -10.00
141
+	 *
142
+	 * Examples:
143
+	 * The interest on a payment on a 30-year fixed mortgage note of $265000 at 3.5% interest
144
+	 * paid at the end of every month, looking at the first payment:
145
+	 *   ipmt(0.035/12, 1, 30*12, 265000, 0, false)
146
+	 *
147
+	 * @param  float $rate
148
+	 * @param  int   $period
149
+	 * @param  int   $periods
150
+	 * @param  float $present_value
151
+	 * @param  float $future_value
152
+	 * @param  bool  $beginning adjust the payment to the beginning or end of the period
153
+	 *
154
+	 * @return float
155
+	 */
156
+	public static function ipmt(float $rate, int $period, int $periods, float $present_value, float $future_value = 0.0, bool $beginning = false): float
157
+	{
158
+		if ($period < 1 || $period > $periods) {
159
+			return \NAN;
160
+		}
161
+
162
+		if ($rate == 0) {
163
+			return 0;
164
+		}
165
+
166
+		if ($beginning && $period == 1) {
167
+			return 0.0;
168
+		}
169
+
170
+		$payment = self::pmt($rate, $periods, $present_value, $future_value, $beginning);
171
+		if ($beginning) {
172
+			$interest = (self::fv($rate, $period - 2, $payment, $present_value, $beginning) - $payment) * $rate;
173
+		} else {
174
+			$interest = self::fv($rate, $period - 1, $payment, $present_value, $beginning) * $rate;
175
+		}
176
+
177
+		return self::checkZero($interest);
178
+	}
179
+
180
+	/**
181
+	 * Principle on a financial payment for a loan or annuity with compound interest.
182
+	 * Determines the principle payment at a particular period of the annuity. For
183
+	 * a typical loan paid down to zero, the amount of interest and principle paid
184
+	 * throughout the lifetime of the loan will change, with the principle portion
185
+	 * of the payment increasing over time as the loan principle decreases.
186
+	 *
187
+	 * Same as the =PPMT() function in most spreadsheet software.
188
+	 *
189
+	 * See the PMT function for derivation of the formula.
190
+	 * See the IPMT function for derivation and use of PMT, IPMT, and PPMT.
191
+	 *
192
+	 * With derivations for PMT and IPMT, we simply compute:
193
+	 *
194
+	 * PPMT = PMT - IPMT
195
+	 *
196
+	 * Examples:
197
+	 * The principle on a payment on a 30-year fixed mortgage note of $265000 at 3.5% interest
198
+	 * paid at the end of every month, looking at the first payment:
199
+	 *   ppmt(0.035/12, 1, 30*12, 265000, 0, false)
200
+	 *
201
+	 * @param  float $rate
202
+	 * @param  int   $period
203
+	 * @param  int   $periods
204
+	 * @param  float $present_value
205
+	 * @param  float $future_value
206
+	 * @param  bool  $beginning adjust the payment to the beginning or end of the period
207
+	 *
208
+	 * @return float
209
+	 */
210
+	public static function ppmt(float $rate, int $period, int $periods, float $present_value, float $future_value = 0.0, bool $beginning = false): float
211
+	{
212
+		$payment = self::pmt($rate, $periods, $present_value, $future_value, $beginning);
213
+		$ipmt    = self::ipmt($rate, $period, $periods, $present_value, $future_value, $beginning);
214
+
215
+		return $payment - $ipmt;
216
+	}
217
+
218
+	/**
219
+	 * Number of payment periods of an annuity.
220
+	 * Solves for the number of periods in the annuity formula.
221
+	 *
222
+	 * Same as the =NPER() function in most spreadsheet software.
223
+	 *
224
+	 * Solving the basic annuity formula for number of periods:
225
+	 *        log(PMT - FV*r)
226
+	 *        ---------------
227
+	 *        log(PMT + PV*r)
228
+	 * n = --------------------
229
+	 *          log(1 + r)
230
+	 *
231
+	 * The (1+r*when) factor adjusts the payment to the beginning or end
232
+	 * of the period. In the common case of a payment at the end of a period,
233
+	 * the factor is 1 and reduces to the formula above. Setting when=1 computes
234
+	 * an "annuity due" with an immediate payment.
235
+	 *
236
+	 * Examples:
237
+	 * The number of periods of a $475000 mortgage with interest rate 3.5% and monthly
238
+	 * payment of $2132.96  paid in full:
239
+	 *   nper(0.035/12, -2132.96, 475000, 0)
240
+	 *
241
+	 * @param  float $rate
242
+	 * @param  float $payment
243
+	 * @param  float $present_value
244
+	 * @param  float $future_value
245
+	 * @param  bool  $beginning adjust the payment to the beginning or end of the period
246
+	 *
247
+	 * @return float
248
+	 */
249
+	public static function periods(float $rate, float $payment, float $present_value, float $future_value, bool $beginning = false): float
250
+	{
251
+		$when = $beginning ? 1 : 0;
252
+
253
+		if ($rate == 0) {
254
+			return - ($present_value + $future_value) / $payment;
255
+		}
256
+
257
+		$initial = $payment * (1.0 + $rate * $when);
258
+		return \log(($initial - $future_value * $rate) / ($initial + $present_value * $rate)) / \log(1.0 + $rate);
259
+	}
260
+
261
+	/**
262
+	 * Annual Equivalent Rate (AER) of an annual percentage rate (APR).
263
+	 * The effective yearly rate of an annual percentage rate when the
264
+	 * annual percentage rate is compounded periodically within the year.
265
+	 *
266
+	 * Same as the =EFFECT() function in most spreadsheet software.
267
+	 *
268
+	 * The formula:
269
+	 * https://en.wikipedia.org/wiki/Effective_interest_rate
270
+	 *
271
+	 *        /     i \ ᴺ
272
+	 * AER = | 1 +  -  |  - 1
273
+	 *        \     n /
274
+	 *
275
+	 * Examples:
276
+	 * The AER of APR 3.5% interest compounded monthly.
277
+	 *   aer(0.035, 12)
278
+	 *
279
+	 * @param  float $nominal
280
+	 * @param  int $periods
281
+	 *
282
+	 * @return float
283
+	 */
284
+	public static function aer(float $nominal, int $periods): float
285
+	{
286
+		if ($periods == 1) {
287
+			return $nominal;
288
+		}
289
+
290
+		return \pow(1 + ($nominal / $periods), $periods) - 1;
291
+	}
292
+
293
+	/**
294
+	 * Annual Nominal Rate of an annual effective rate (AER).
295
+	 * The nominal yearly rate of an annual effective rate when the
296
+	 * annual effective rate is compounded periodically within the year.
297
+	 *
298
+	 * Same as the =NOMINAL() function in most spreadsheet software.
299
+	 *
300
+	 * See:
301
+	 * https://en.wikipedia.org/wiki/Nominal_interest_rate
302
+	 *
303
+	 *           /          1/N    \
304
+	 * NOMINAL = | (AER + 1)    -1 | * N
305
+	 *           \                 /
306
+	 *
307
+	 * Examples:
308
+	 * The nominal rate of AER 3.557% interest compounded monthly.
309
+	 *   nominal(0.03557, 12)
310
+	 *
311
+	 * @param  float $aer
312
+	 * @param  int $periods
313
+	 *
314
+	 * @return float
315
+	 */
316
+	public static function nominal(float $aer, int $periods): float
317
+	{
318
+		if ($periods == 1) {
319
+			return $aer;
320
+		}
321
+
322
+		return (\pow($aer + 1, 1 / $periods) - 1) * $periods;
323
+	}
324
+
325
+	/**
326
+	 * Future value for a loan or annuity with compound interest.
327
+	 *
328
+	 * Same as the =FV() function in most spreadsheet software.
329
+	 *
330
+	 * The basic future-value formula derivation:
331
+	 * https://en.wikipedia.org/wiki/Future_value
332
+	 *
333
+	 *                   PMT*((1+r)ᴺ - 1)
334
+	 * FV = -PV*(1+r)ᴺ - ----------------
335
+	 *                          r
336
+	 *
337
+	 * The (1+r*when) factor adjusts the payment to the beginning or end
338
+	 * of the period. In the common case of a payment at the end of a period,
339
+	 * the factor is 1 and reduces to the formula above. Setting when=1 computes
340
+	 * an "annuity due" with an immediate payment.
341
+	 *
342
+	 * Examples:
343
+	 * The future value in 5 years on a 30-year fixed mortgage note of $265000
344
+	 * at 3.5% interest paid at the end of every month. This is how much loan
345
+	 * principle would be outstanding:
346
+	 *   fv(0.035/12, 5*12, 1189.97, -265000, false)
347
+	 *
348
+	 * The present_value is negative indicating money borrowed for the mortgage,
349
+	 * whereas payment is positive, indicating money that will be paid to the
350
+	 * mortgage.
351
+	 *
352
+	 * @param  float $rate
353
+	 * @param  int   $periods
354
+	 * @param  float $payment
355
+	 * @param  float $present_value
356
+	 * @param  bool  $beginning adjust the payment to the beginning or end of the period
357
+	 *
358
+	 * @return float
359
+	 */
360
+	public static function fv(float $rate, int $periods, float $payment, float $present_value, bool $beginning = false): float
361
+	{
362
+		$when = $beginning ? 1 : 0;
363
+
364
+		if ($rate == 0) {
365
+			$fv = -($present_value + ($payment * $periods));
366
+			return self::checkZero($fv);
367
+		}
368
+
369
+		$initial  = 1 + ($rate * $when);
370
+		$compound = \pow(1 + $rate, $periods);
371
+		$fv       = - (($present_value * $compound) + (($payment * $initial * ($compound - 1)) / $rate));
372
+
373
+		return self::checkZero($fv);
374
+	}
375
+
376
+	/**
377
+	 * Present value for a loan or annuity with compound interest.
378
+	 *
379
+	 * Same as the =PV() function in most spreadsheet software.
380
+	 *
381
+	 * The basic present-value formula derivation:
382
+	 * https://en.wikipedia.org/wiki/Present_value
383
+	 *
384
+	 *            PMT*((1+r)ᴺ - 1)
385
+	 * PV = -FV - ----------------
386
+	 *                   r
387
+	 *      ---------------------
388
+	 *             (1 + r)ᴺ
389
+	 *
390
+	 * The (1+r*when) factor adjusts the payment to the beginning or end
391
+	 * of the period. In the common case of a payment at the end of a period,
392
+	 * the factor is 1 and reduces to the formula above. Setting when=1 computes
393
+	 * an "annuity due" with an immediate payment.
394
+	 *
395
+	 * Examples:
396
+	 * The present value of a bond's $1000 face value paid in 5 year's time
397
+	 * with a constant discount rate of 3.5% compounded monthly:
398
+	 *   pv(0.035/12, 5*12, 0, -1000, false)
399
+	 *
400
+	 * The present value of a $1000 5-year bond that pays a fixed 7% ($70)
401
+	 * coupon at the end of each year with a discount rate of 5%:
402
+	 *   pv(0.5, 5, -70, -1000, false)
403
+	 *
404
+	 * The payment and future_value is negative indicating money paid out.
405
+	 *
406
+	 * @param  float $rate
407
+	 * @param  int   $periods
408
+	 * @param  float $payment
409
+	 * @param  float $future_value
410
+	 * @param  bool  $beginning adjust the payment to the beginning or end of the period
411
+	 *
412
+	 * @return float
413
+	 */
414
+	public static function pv(float $rate, int $periods, float $payment, float $future_value, bool $beginning = false): float
415
+	{
416
+		$when = $beginning ? 1 : 0;
417
+
418
+		if ($rate == 0) {
419
+			$pv = -$future_value - ($payment * $periods);
420
+			return self::checkZero($pv);
421
+		}
422
+
423
+		$initial  = 1 + ($rate * $when);
424
+		$compound = \pow(1 + $rate, $periods);
425
+		$pv       = (-$future_value - (($payment * $initial * ($compound - 1)) / $rate)) / $compound;
426
+
427
+		return self::checkZero($pv);
428
+	}
429
+
430
+	/**
431
+	 * Net present value of cash flows. Cash flows are periodic starting
432
+	 * from an initial time and with a uniform discount rate.
433
+	 *
434
+	 * Similar to the =NPV() function in most spreadsheet software, except
435
+	 * the initial (usually negative) cash flow at time 0 is given as the
436
+	 * first element of the array rather than subtracted. For example,
437
+	 *   spreadsheet: =NPV(0.01, 100, 200, 300, 400) - 1000
438
+	 * is done as
439
+	 *   MathPHP::npv(0.01, [-1000, 100, 200, 300, 400])
440
+	 *
441
+	 * The basic net-present-value formula derivation:
442
+	 * https://en.wikipedia.org/wiki/Net_present_value
443
+	 *
444
+	 *  n      Rt
445
+	 *  Σ   --------
446
+	 * t=0  (1 / r)ᵗ
447
+	 *
448
+	 * Examples:
449
+	 * The net present value of 5 yearly cash flows after an initial $1000
450
+	 * investment with a 3% discount rate:
451
+	 *  npv(0.03, [-1000, 100, 500, 300, 700, 700])
452
+	 *
453
+	 * @param  float $rate
454
+	 * @param  array<float> $values
455
+	 *
456
+	 * @return float
457
+	 */
458
+	public static function npv(float $rate, array $values): float
459
+	{
460
+		$result = 0.0;
461
+
462
+		for ($i = 0; $i < \count($values); ++$i) {
463
+			$result += $values[$i] / (1 + $rate) ** $i;
464
+		}
465
+
466
+		return $result;
467
+	}
468
+
469
+	/**
470
+	 * Interest rate per period of an Annuity.
471
+	 *
472
+	 * Same as the =RATE() formula in most spreadsheet software.
473
+	 *
474
+	 * The basic rate formula derivation is to solve for the future value
475
+	 * taking into account the present value:
476
+	 * https://en.wikipedia.org/wiki/Future_value
477
+	 *
478
+	 *                        ((1+r)ᴺ - 1)
479
+	 * FV + PV*(1+r)ᴺ + PMT * ------------ = 0
480
+	 *                             r
481
+	 * The (1+r*when) factor adjusts the payment to the beginning or end
482
+	 * of the period. In the common case of a payment at the end of a period,
483
+	 * the factor is 1 and reduces to the formula above. Setting when=1 computes
484
+	 * an "annuity due" with an immediate payment.
485
+	 *
486
+	 * Not all solutions for the rate have real-value solutions or converge.
487
+	 * In these cases, NAN is returned.
488
+	 *
489
+	 * @param  float $periods
490
+	 * @param  float $payment
491
+	 * @param  float $present_value
492
+	 * @param  float $future_value
493
+	 * @param  bool  $beginning
494
+	 * @param  float $initial_guess
495
+	 *
496
+	 * @return float
497
+	 */
498
+	public static function rate(float $periods, float $payment, float $present_value, float $future_value, bool $beginning = false, float $initial_guess = 0.1): float
499
+	{
500
+		$when = $beginning ? 1 : 0;
501
+
502
+		$func = function ($x, $periods, $payment, $present_value, $future_value, $when) {
503
+			return $future_value + $present_value * (1 + $x) ** $periods + $payment * (1 + $x * $when) / $x * ((1 + $x) ** $periods - 1);
504
+		};
505
+
506
+		return self::checkZero(NumericalAnalysis\RootFinding\NewtonsMethod::solve($func, [$initial_guess, $periods, $payment, $present_value, $future_value, $when], 0, self::EPSILON, 0));
507
+	}
508
+
509
+	/**
510
+	 * Internal rate of return.
511
+	 * Periodic rate of return that would provide a net-present value (NPV) of 0.
512
+	 *
513
+	 * Same as =IRR formula in most spreadsheet software.
514
+	 *
515
+	 * Reference:
516
+	 * https://en.wikipedia.org/wiki/Internal_rate_of_return
517
+	 *
518
+	 * Examples:
519
+	 * The rate of return of an initial investment of $100 with returns
520
+	 * of $50, $40, and $30:
521
+	 *  irr([-100, 50, 40, 30])
522
+	 *
523
+	 * Solves for NPV=0 using Newton's Method.
524
+	 * @param array<float> $values
525
+	 * @param float $initial_guess
526
+	 *
527
+	 * @return float
528
+	 *
529
+	 * @throws OutOfBoundsException
530
+	 *
531
+	 * @todo: Use eigenvalues to find the roots of a characteristic polynomial.
532
+	 * This will allow finding all solutions and eliminate the need of the initial_guess.
533
+	 */
534
+	public static function irr(array $values, float $initial_guess = 0.1): float
535
+	{
536
+		$func = function ($x, $values) {
537
+			return Finance::npv($x, $values);
538
+		};
539
+
540
+		if (\count($values) <= 1) {
541
+			return \NAN;
542
+		}
543
+
544
+		$root = NumericalAnalysis\RootFinding\NewtonsMethod::solve($func, [$initial_guess, $values], 0, self::EPSILON, 0);
545
+		if (!\is_nan($root)) {
546
+			return self::CheckZero($root);
547
+		}
548
+		return self::checkZero(self::alternateIrr($values));
549
+	}
550
+
551
+	/**
552
+	 * Alternate IRR implementation.
553
+	 *
554
+	 * A more numerically stable implementation that converges to only one value.
555
+	 *
556
+	 * Based off of Better: https://github.com/better/irr
557
+	 *
558
+	 * @param  array<float> $values
559
+	 *
560
+	 * @return float
561
+	 */
562
+	private static function alternateIrr(array $values): float
563
+	{
564
+		$rate = 0.0;
565
+		for ($iter = 0; $iter < 100; $iter++) {
566
+			$m = -1000;
567
+			for ($i = 0; $i < \count($values); $i++) {
568
+				$m = \max($m, -$rate * $i);
569
+			}
570
+			$f = [];
571
+			for ($i = 0; $i < \count($values); $i++) {
572
+				$f[$i] = \exp(-$rate * $i - $m);
573
+			}
574
+			$t = 0;
575
+			for ($i = 0; $i < \count($values); $i++) {
576
+				$t += $f[$i] * $values[$i];
577
+			}
578
+			if (\abs($t) < (self::EPSILON * \exp($m))) {
579
+				break;
580
+			}
581
+			$u = 0;
582
+			for ($i = 0; $i < \count($values); $i++) {
583
+				$u += $f[$i] * $i * $values[$i];
584
+			}
585
+			if ($u == 0) {
586
+				return \NAN;
587
+			}
588
+			$rate += $t / $u;
589
+		}
590
+		return \exp($rate) - 1;
591
+	}
592
+
593
+	/**
594
+	 * Modified internal rate of return.
595
+	 * Rate of return that discounts outflows (investments) at the financing rate,
596
+	 * and reinvests inflows with an expected rate of return.
597
+	 *
598
+	 * Same as =MIRR formula in most spreadsheet software.
599
+	 *
600
+	 * The formula derivation:
601
+	 * https://en.wikipedia.org/wiki/Modified_internal_rate_of_return
602
+	 *
603
+	 *       _____________________________
604
+	 *     n/ FV(re-invested cash inflows)
605
+	 *  -  /  ----------------------------  - 1.0
606
+	 *   \/   PV(discounted cash outflows)
607
+	 *
608
+	 * Examples:
609
+	 * The rate of return of an initial investment of $100 at 5% financing
610
+	 * with returns of $50, $40, and $30 reinvested at 10%:
611
+	 *  mirr([-100, 50, 40, 30], 0.05, 0.10)
612
+	 *
613
+	 * @param  array<float> $values
614
+	 * @param  float $finance_rate
615
+	 * @param  float $reinvestment_rate
616
+	 *
617
+	 * @return float
618
+	 */
619
+	public static function mirr(array $values, float $finance_rate, float $reinvestment_rate): float
620
+	{
621
+		$inflows  = array();
622
+		$outflows = array();
623
+
624
+		for ($i = 0; $i < \count($values); $i++) {
625
+			if ($values[$i] >= 0) {
626
+				$inflows[]  = $values[$i];
627
+				$outflows[] = 0;
628
+			} else {
629
+				$inflows[]  = 0;
630
+				$outflows[] = $values[$i];
631
+			}
632
+		}
633
+
634
+		$nonzero = function ($x) {
635
+			return $x != 0;
636
+		};
637
+
638
+		if (\count(\array_filter($inflows, $nonzero)) == 0 || \count(\array_filter($outflows, $nonzero)) == 0) {
639
+			return \NAN;
640
+		}
641
+
642
+		$root        = \count($values) - 1;
643
+		$pv_inflows  = self::npv($reinvestment_rate, $inflows);
644
+		$fv_inflows  = self::fv($reinvestment_rate, $root, 0, -$pv_inflows);
645
+		$pv_outflows = self::npv($finance_rate, $outflows);
646
+
647
+		return self::checkZero(\pow($fv_inflows / -$pv_outflows, 1 / $root) - 1);
648
+	}
649
+
650
+	/**
651
+	 * Discounted Payback of an investment.
652
+	 * The number of periods to recoup cash outlays of an investment.
653
+	 *
654
+	 * This is commonly used with discount rate=0 as simple payback period,
655
+	 * but it is not a real financial measurement when it doesn't consider the
656
+	 * discount rate. Even with a discount rate, it doesn't consider the cost
657
+	 * of capital or re-investment of returns.
658
+	 *
659
+	 * Avoid this when possible. Consider NPV, MIRR, IRR, and other financial
660
+	 * functions.
661
+	 *
662
+	 * Reference:
663
+	 * https://en.wikipedia.org/wiki/Payback_period
664
+	 *
665
+	 * The result is given assuming cash flows are continous throughout a period.
666
+	 * To compute payback in terms of whole periods, use ceil() on the result.
667
+	 *
668
+	 * An investment could reach its payback period before future cash outlays occur.
669
+	 * The payback period returned is defined to be the final point at which the
670
+	 * sum of returns becomes positive.
671
+	 *
672
+	 * Examples:
673
+	 * The payback period of an investment with a $1,000 investment and future returns
674
+	 * of $100, $200, $300, $400, $500:
675
+	 *  payback([-1000, 100, 200, 300, 400, 500])
676
+	 *
677
+	 * The discounted payback period of an investment with a $1,000 investment, future returns
678
+	 * of $100, $200, $300, $400, $500, and a discount rate of 0.10:
679
+	 *  payback([-1000, 100, 200, 300, 400, 500], 0.1)
680
+	 *
681
+	 * @param  array<float> $values
682
+	 * @param  float $rate
683
+	 *
684
+	 * @return float
685
+	 */
686
+	public static function payback(array $values, float $rate = 0.0): float
687
+	{
688
+		$last_outflow = -1;
689
+		for ($i = 0; $i < \count($values); $i++) {
690
+			if ($values[$i] < 0) {
691
+				$last_outflow = $i;
692
+			}
693
+		}
694
+
695
+		if ($last_outflow < 0) {
696
+			return 0.0;
697
+		}
698
+
699
+		$sum            = $values[0];
700
+		$payback_period = -1;
701
+
702
+		for ($i = 1; $i < \count($values); $i++) {
703
+			$prevsum         = $sum;
704
+			$discounted_flow = $values[$i] / (1 + $rate) ** $i;
705
+			$sum            += $discounted_flow;
706
+			if ($sum >= 0) {
707
+				if ($i > $last_outflow) {
708
+					return ($i - 1) + (-$prevsum / $discounted_flow);
709
+				}
710
+				if ($payback_period == -1) {
711
+					$payback_period = ($i - 1) + (-$prevsum / $discounted_flow);
712
+				}
713
+			} else {
714
+				$payback_period = -1;
715
+			}
716
+		}
717
+		if ($sum >= 0) {
718
+			return $payback_period;
719
+		}
720
+
721
+		return \NAN;
722
+	}
723
+
724
+	/**
725
+	 * Profitability Index.
726
+	 * The Profitability Index, also referred to as Profit Investment
727
+	 * Ratio (PIR) and Value Investment Ratio (VIR), is a comparison of
728
+	 * discounted cash inflows to discounted cash outflows. It can be
729
+	 * used as a decision criteria of an investment, using larger than 1
730
+	 * to choose an investment, and less than 1 to pass.
731
+	 *
732
+	 * The formula derivation:
733
+	 * https://en.wikipedia.org/wiki/Profitability_index
734
+	 *
735
+	 * PV(cash inflows)
736
+	 * ----------------
737
+	 * PV(cash outflows)
738
+	 *
739
+	 * The formula is usually stated in terms of the initial investmest,
740
+	 * but it is generalized here to discount all future outflows.
741
+	 *
742
+	 * Examples:
743
+	 * The profitability index of an initial $100 investment with future
744
+	 * returns of $50, $50, $50 with a 10% discount rate:
745
+	 *  profitabilityIndex([-100, 50, 50, 50], 0.10)
746
+	 *
747
+	 * @param  array<float> $values
748
+	 * @param  float $rate
749
+	 *
750
+	 * @return float
751
+	 */
752
+	public static function profitabilityIndex(array $values, float $rate): float
753
+	{
754
+		$inflows  = array();
755
+		$outflows = array();
756
+
757
+		for ($i = 0; $i < \count($values); $i++) {
758
+			if ($values[$i] >= 0) {
759
+				$inflows[]  = $values[$i];
760
+				$outflows[] = 0;
761
+			} else {
762
+				$inflows[]  = 0;
763
+				$outflows[] = -$values[$i];
764
+			}
765
+		}
766
+
767
+		$nonzero = function ($x) {
768
+			return $x != 0;
769
+		};
770
+
771
+		if (\count(\array_filter($outflows, $nonzero)) == 0) {
772
+			return \NAN;
773
+		}
774
+
775
+		$pv_inflows  = self::npv($rate, $inflows);
776
+		$pv_outflows = self::npv($rate, $outflows);
777
+
778
+		return $pv_inflows / $pv_outflows;
779
+	}
780 780
 }
Please login to merge, or discard this patch.
markrogoyski/math-php/src/NumericalAnalysis/RootFinding/NewtonsMethod.php 1 patch
Indentation   +43 added lines, -43 removed lines patch added patch discarded remove patch
@@ -12,50 +12,50 @@
 block discarded – undo
12 12
  */
13 13
 class NewtonsMethod
14 14
 {
15
-    /**
16
-     * Use Newton's Method to find the x which produces $target = $function(x) value
17
-     * $args is an array of parameters to pass to $function, but having the element that
18
-     * will be changed and serve as the initial guess in position $position.
19
-     *
20
-     * @param callable     $function     f(x) callback function
21
-     * @param array<mixed> $args         Parameters to pass to callback function. The initial value for the
22
-     *                                   parameter of interest must be in this array.
23
-     * @param int|float    $target       Value of f(x) we a trying to solve for
24
-     * @param float        $tol          Tolerance; How close to the actual solution we would like.
25
-     * @param int          $position     Which element in the $args array will be changed; also serves as initial guess
26
-     * @param int          $iterations
27
-     *
28
-     * @return int|float
29
-     *
30
-     * @throws Exception\OutOfBoundsException if the tolerance is not valid
31
-     */
32
-    public static function solve(callable $function, array $args, $target, float $tol, int $position = 0, int $iterations = 100)
33
-    {
34
-        Validation::tolerance($tol);
15
+	/**
16
+	 * Use Newton's Method to find the x which produces $target = $function(x) value
17
+	 * $args is an array of parameters to pass to $function, but having the element that
18
+	 * will be changed and serve as the initial guess in position $position.
19
+	 *
20
+	 * @param callable     $function     f(x) callback function
21
+	 * @param array<mixed> $args         Parameters to pass to callback function. The initial value for the
22
+	 *                                   parameter of interest must be in this array.
23
+	 * @param int|float    $target       Value of f(x) we a trying to solve for
24
+	 * @param float        $tol          Tolerance; How close to the actual solution we would like.
25
+	 * @param int          $position     Which element in the $args array will be changed; also serves as initial guess
26
+	 * @param int          $iterations
27
+	 *
28
+	 * @return int|float
29
+	 *
30
+	 * @throws Exception\OutOfBoundsException if the tolerance is not valid
31
+	 */
32
+	public static function solve(callable $function, array $args, $target, float $tol, int $position = 0, int $iterations = 100)
33
+	{
34
+		Validation::tolerance($tol);
35 35
 
36
-        // Initialize
37
-        $args1 = $args;
38
-        $guess = $args[$position];
39
-        $i     = 0;
36
+		// Initialize
37
+		$args1 = $args;
38
+		$guess = $args[$position];
39
+		$i     = 0;
40 40
 
41
-        do {
42
-            $args1[$position] = $guess + $tol; // load the initial guess into the arguments
43
-            $args[$position]  = $guess;        // load the initial guess into the arguments
44
-            $y                = $function(...$args);
45
-            $y_at_xplusdelx   = $function(...$args1);
46
-            $slope            = ($y_at_xplusdelx - $y) / $tol;
47
-            $del_y            = $target - $y;
48
-            if (\abs($slope) < $tol) {
49
-                return \NAN;
50
-            }
51
-            $guess            = $del_y / $slope + $guess;
52
-            $dif              = \abs($del_y);
53
-            $i++;
54
-        } while ($dif > $tol && $i < $iterations);
41
+		do {
42
+			$args1[$position] = $guess + $tol; // load the initial guess into the arguments
43
+			$args[$position]  = $guess;        // load the initial guess into the arguments
44
+			$y                = $function(...$args);
45
+			$y_at_xplusdelx   = $function(...$args1);
46
+			$slope            = ($y_at_xplusdelx - $y) / $tol;
47
+			$del_y            = $target - $y;
48
+			if (\abs($slope) < $tol) {
49
+				return \NAN;
50
+			}
51
+			$guess            = $del_y / $slope + $guess;
52
+			$dif              = \abs($del_y);
53
+			$i++;
54
+		} while ($dif > $tol && $i < $iterations);
55 55
 
56
-        if ($dif > $tol) {
57
-            return \NAN;
58
-        }
59
-        return $guess;
60
-    }
56
+		if ($dif > $tol) {
57
+			return \NAN;
58
+		}
59
+		return $guess;
60
+	}
61 61
 }
Please login to merge, or discard this patch.
markrogoyski/math-php/src/NumericalAnalysis/RootFinding/Validation.php 1 patch
Indentation   +27 added lines, -27 removed lines patch added patch discarded remove patch
@@ -9,32 +9,32 @@
 block discarded – undo
9 9
  */
10 10
 class Validation
11 11
 {
12
-    /**
13
-     * Throw an exception if the tolerance is negative.
14
-     *
15
-     * @param int|float $tol Tolerance; How close to the actual solution we would like.
16
-     *
17
-     * @throws Exception\OutOfBoundsException if $tol (the tolerance) is negative
18
-     */
19
-    public static function tolerance($tol): void
20
-    {
21
-        if ($tol < 0) {
22
-            throw new Exception\OutOfBoundsException('Tolerance must be greater than zero.');
23
-        }
24
-    }
12
+	/**
13
+	 * Throw an exception if the tolerance is negative.
14
+	 *
15
+	 * @param int|float $tol Tolerance; How close to the actual solution we would like.
16
+	 *
17
+	 * @throws Exception\OutOfBoundsException if $tol (the tolerance) is negative
18
+	 */
19
+	public static function tolerance($tol): void
20
+	{
21
+		if ($tol < 0) {
22
+			throw new Exception\OutOfBoundsException('Tolerance must be greater than zero.');
23
+		}
24
+	}
25 25
 
26
-    /**
27
-     * Verify that the start and end of of an interval are distinct numbers.
28
-     *
29
-     * @param int|float $a The start of the interval
30
-     * @param int|float $b The end of the interval
31
-     *
32
-     * @throws Exception\BadDataException if $a = $b
33
-     */
34
-    public static function interval($a, $b): void
35
-    {
36
-        if ($a === $b) {
37
-            throw new Exception\BadDataException('Start point and end point of interval cannot be the same.');
38
-        }
39
-    }
26
+	/**
27
+	 * Verify that the start and end of of an interval are distinct numbers.
28
+	 *
29
+	 * @param int|float $a The start of the interval
30
+	 * @param int|float $b The end of the interval
31
+	 *
32
+	 * @throws Exception\BadDataException if $a = $b
33
+	 */
34
+	public static function interval($a, $b): void
35
+	{
36
+		if ($a === $b) {
37
+			throw new Exception\BadDataException('Start point and end point of interval cannot be the same.');
38
+		}
39
+	}
40 40
 }
Please login to merge, or discard this patch.