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