Failed Conditions
Pull Request — master (#4420)
by Owen
15:03
created

Periodic::presentValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 18
ccs 6
cts 6
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 2
crap 3
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Variable;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
6
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
7
8
class Periodic
9
{
10
    const FINANCIAL_MAX_ITERATIONS = 128;
11
12
    const FINANCIAL_PRECISION = 1.0e-08;
13
14
    /**
15
     * IRR.
16
     *
17
     * Returns the internal rate of return for a series of cash flows represented by the numbers in values.
18
     * These cash flows do not have to be even, as they would be for an annuity. However, the cash flows must occur
19
     * at regular intervals, such as monthly or annually. The internal rate of return is the interest rate received
20
     * for an investment consisting of payments (negative values) and income (positive values) that occur at regular
21
     * periods.
22
     *
23
     * Excel Function:
24
     *        IRR(values[,guess])
25
     *
26
     * @param mixed $values An array or a reference to cells that contain numbers for which you want
27
     *                                    to calculate the internal rate of return.
28
     *                                Values must contain at least one positive value and one negative value to
29
     *                                    calculate the internal rate of return.
30
     * @param mixed $guess A number that you guess is close to the result of IRR
31
     */
32 10
    public static function rate(mixed $values, mixed $guess = 0.1): string|float
33
    {
34 10
        if (!is_array($values)) {
35
            return ExcelError::VALUE();
36
        }
37 10
        $values = Functions::flattenArray($values);
38 10
        $guess = Functions::flattenSingleValue($guess);
39
        if (!is_numeric($guess)) {
40
            return ExcelError::VALUE();
41 10
        }
42 10
43 10
        // create an initial range, with a root somewhere between 0 and guess
44 10
        $x1 = 0.0;
45 10
        $x2 = $guess;
46 10
        $f1 = self::presentValue($x1, $values);
47 7
        $f2 = self::presentValue($x2, $values);
48
        for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
49 9
            if (($f1 * $f2) < 0.0) {
50 4
                break;
51
            }
52 6
            if (abs($f1) < abs($f2)) {
53
                $f1 = self::presentValue($x1 += 1.6 * ($x1 - $x2), $values);
54
            } else {
55 10
                $f2 = self::presentValue($x2 += 1.6 * ($x2 - $x1), $values);
56 3
            }
57
        }
58
        if (($f1 * $f2) > 0.0) {
59 7
            return ExcelError::VALUE();
60 7
        }
61 1
62 1
        $f = self::presentValue($x1, $values);
63
        if ($f < 0.0) {
64 6
            $rtb = $x1;
65 6
            $dx = $x2 - $x1;
66
        } else {
67
            $rtb = $x2;
68 7
            $dx = $x1 - $x2;
69 7
        }
70 7
71 7
        for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
72 7
            $dx *= 0.5;
73 7
            $x_mid = $rtb + $dx;
74
            $f_mid = self::presentValue($x_mid, $values);
75 7
            if ($f_mid <= 0.0) {
76 7
                $rtb = $x_mid;
77
            }
78
            if ((abs($f_mid) < self::FINANCIAL_PRECISION) || (abs($dx) < self::FINANCIAL_PRECISION)) {
79
                return $x_mid;
80
            }
81
        }
82
83
        return ExcelError::VALUE();
84
    }
85
86
    /**
87
     * MIRR.
88
     *
89
     * Returns the modified internal rate of return for a series of periodic cash flows. MIRR considers both
90
     *        the cost of the investment and the interest received on reinvestment of cash.
91
     *
92
     * Excel Function:
93
     *        MIRR(values,finance_rate, reinvestment_rate)
94
     *
95
     * @param mixed $values An array or a reference to cells that contain a series of payments and
96
     *                         income occurring at regular intervals.
97
     *                      Payments are negative value, income is positive values.
98
     * @param mixed $financeRate The interest rate you pay on the money used in the cash flows
99
     * @param mixed $reinvestmentRate The interest rate you receive on the cash flows as you reinvest them
100 12
     *
101
     * @return float|string Result, or a string containing an error
102 12
     */
103
    public static function modifiedRate(mixed $values, mixed $financeRate, mixed $reinvestmentRate): string|float
104
    {
105 12
        if (!is_array($values)) {
106 12
            return ExcelError::DIV0();
107 12
        }
108 12
        $values = Functions::flattenArray($values);
109
        /** @var float */
110 12
        $financeRate = Functions::flattenSingleValue($financeRate);
111 12
        /** @var float */
112
        $reinvestmentRate = Functions::flattenSingleValue($reinvestmentRate);
113 12
        $n = count($values);
114 12
115 12
        $rr = 1.0 + $reinvestmentRate;
116 11
        $fr = 1.0 + $financeRate;
117
118 10
        $npvPos = $npvNeg = 0.0;
119
        foreach ($values as $i => $v) {
120
            if ($v >= 0) {
121
                $npvPos += $v / $rr ** $i;
122 12
            } else {
123 3
                $npvNeg += $v / $fr ** $i;
124
            }
125
        }
126 9
127 9
        if ($npvNeg === 0.0 || $npvPos === 0.0) {
128
            return ExcelError::DIV0();
129 9
        }
130
131
        $mirr = ((-$npvPos * $rr ** $n)
132
                / ($npvNeg * ($rr))) ** (1.0 / ($n - 1)) - 1.0;
133
134
        return is_finite($mirr) ? $mirr : ExcelError::NAN();
135
    }
136
137
    /**
138
     * NPV.
139 15
     *
140
     * Returns the Net Present Value of a cash flow series given a discount rate.
141 15
     *
142
     * @param array $args
143 15
     */
144 15
    public static function presentValue(mixed $rate, ...$args): int|float
145
    {
146
        $returnValue = 0;
147 15
148 15
        /** @var float */
149
        $rate = Functions::flattenSingleValue($rate);
150 15
        $aArgs = Functions::flattenArray($args);
151 15
152
        // Calculate
153
        $countArgs = count($aArgs);
154
        for ($i = 1; $i <= $countArgs; ++$i) {
155 15
            // Is it a numeric value?
156
            if (is_numeric($aArgs[$i - 1])) {
157
                $returnValue += $aArgs[$i - 1] / (1 + $rate) ** $i;
158
            }
159
        }
160
161
        return $returnValue;
162
    }
163
}
164