1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Constant\Periodic; |
4
|
|
|
|
5
|
|
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
6
|
|
|
use PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\CashFlowValidations; |
7
|
|
|
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; |
8
|
|
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
9
|
|
|
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
10
|
|
|
|
11
|
|
|
class Interest |
12
|
|
|
{ |
13
|
|
|
private const FINANCIAL_MAX_ITERATIONS = 128; |
14
|
|
|
|
15
|
|
|
private const FINANCIAL_PRECISION = 1.0e-08; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* IPMT. |
19
|
|
|
* |
20
|
|
|
* Returns the interest payment for a given period for an investment based on periodic, constant payments |
21
|
|
|
* and a constant interest rate. |
22
|
|
|
* |
23
|
|
|
* Excel Function: |
24
|
|
|
* IPMT(rate,per,nper,pv[,fv][,type]) |
25
|
|
|
* |
26
|
|
|
* @param mixed $interestRate Interest rate per period |
27
|
|
|
* @param mixed $period Period for which we want to find the interest |
28
|
|
|
* @param mixed $numberOfPeriods Number of periods |
29
|
|
|
* @param mixed $presentValue Present Value |
30
|
|
|
* @param mixed $futureValue Future Value |
31
|
|
|
* @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period |
32
|
|
|
*/ |
33
|
32 |
|
public static function payment( |
34
|
|
|
mixed $interestRate, |
35
|
|
|
mixed $period, |
36
|
|
|
mixed $numberOfPeriods, |
37
|
|
|
mixed $presentValue, |
38
|
|
|
mixed $futureValue = 0, |
39
|
|
|
mixed $type = FinancialConstants::PAYMENT_END_OF_PERIOD |
40
|
|
|
): string|float { |
41
|
32 |
|
$interestRate = Functions::flattenSingleValue($interestRate); |
42
|
32 |
|
$period = Functions::flattenSingleValue($period); |
43
|
32 |
|
$numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); |
44
|
32 |
|
$presentValue = Functions::flattenSingleValue($presentValue); |
45
|
32 |
|
$futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue); |
46
|
32 |
|
$type = Functions::flattenSingleValue($type) ?? FinancialConstants::PAYMENT_END_OF_PERIOD; |
47
|
|
|
|
48
|
|
|
try { |
49
|
32 |
|
$interestRate = CashFlowValidations::validateRate($interestRate); |
50
|
31 |
|
$period = CashFlowValidations::validateInt($period); |
51
|
30 |
|
$numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods); |
52
|
29 |
|
$presentValue = CashFlowValidations::validatePresentValue($presentValue); |
53
|
28 |
|
$futureValue = CashFlowValidations::validateFutureValue($futureValue); |
54
|
27 |
|
$type = CashFlowValidations::validatePeriodType($type); |
55
|
7 |
|
} catch (Exception $e) { |
56
|
7 |
|
return $e->getMessage(); |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
// Validate parameters |
60
|
25 |
|
if ($period <= 0 || $period > $numberOfPeriods) { |
61
|
2 |
|
return ExcelError::NAN(); |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
// Calculate |
65
|
23 |
|
$interestAndPrincipal = new InterestAndPrincipal( |
66
|
23 |
|
$interestRate, |
67
|
23 |
|
$period, |
68
|
23 |
|
$numberOfPeriods, |
69
|
23 |
|
$presentValue, |
70
|
23 |
|
$futureValue, |
71
|
23 |
|
$type |
72
|
23 |
|
); |
73
|
|
|
|
74
|
23 |
|
return $interestAndPrincipal->interest(); |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* ISPMT. |
79
|
|
|
* |
80
|
|
|
* Returns the interest payment for an investment based on an interest rate and a constant payment schedule. |
81
|
|
|
* |
82
|
|
|
* Excel Function: |
83
|
|
|
* =ISPMT(interest_rate, period, number_payments, pv) |
84
|
|
|
* |
85
|
|
|
* @param mixed $interestRate is the interest rate for the investment |
86
|
|
|
* @param mixed $period is the period to calculate the interest rate. It must be betweeen 1 and number_payments. |
87
|
|
|
* @param mixed $numberOfPeriods is the number of payments for the annuity |
88
|
|
|
* @param mixed $principleRemaining is the loan amount or present value of the payments |
89
|
|
|
*/ |
90
|
14 |
|
public static function schedulePayment(mixed $interestRate, mixed $period, mixed $numberOfPeriods, mixed $principleRemaining): string|float |
91
|
|
|
{ |
92
|
14 |
|
$interestRate = Functions::flattenSingleValue($interestRate); |
93
|
14 |
|
$period = Functions::flattenSingleValue($period); |
94
|
14 |
|
$numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); |
95
|
14 |
|
$principleRemaining = Functions::flattenSingleValue($principleRemaining); |
96
|
|
|
|
97
|
|
|
try { |
98
|
14 |
|
$interestRate = CashFlowValidations::validateRate($interestRate); |
99
|
13 |
|
$period = CashFlowValidations::validateInt($period); |
100
|
12 |
|
$numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods); |
101
|
11 |
|
$principleRemaining = CashFlowValidations::validateFloat($principleRemaining); |
102
|
4 |
|
} catch (Exception $e) { |
103
|
4 |
|
return $e->getMessage(); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
// Validate parameters |
107
|
10 |
|
if ($period <= 0 || $period > $numberOfPeriods) { |
108
|
2 |
|
return ExcelError::NAN(); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
// Return value |
112
|
8 |
|
$returnValue = 0; |
113
|
|
|
|
114
|
|
|
// Calculate |
115
|
8 |
|
$principlePayment = ($principleRemaining * 1.0) / ($numberOfPeriods * 1.0); |
116
|
8 |
|
for ($i = 0; $i <= $period; ++$i) { |
117
|
8 |
|
$returnValue = $interestRate * $principleRemaining * -1; |
118
|
8 |
|
$principleRemaining -= $principlePayment; |
119
|
|
|
// principle needs to be 0 after the last payment, don't let floating point screw it up |
120
|
8 |
|
if ($i == $numberOfPeriods) { |
121
|
1 |
|
$returnValue = 0.0; |
122
|
|
|
} |
123
|
|
|
} |
124
|
|
|
|
125
|
8 |
|
return $returnValue; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* RATE. |
130
|
|
|
* |
131
|
|
|
* Returns the interest rate per period of an annuity. |
132
|
|
|
* RATE is calculated by iteration and can have zero or more solutions. |
133
|
|
|
* If the successive results of RATE do not converge to within 0.0000001 after 20 iterations, |
134
|
|
|
* RATE returns the #NUM! error value. |
135
|
|
|
* |
136
|
|
|
* Excel Function: |
137
|
|
|
* RATE(nper,pmt,pv[,fv[,type[,guess]]]) |
138
|
|
|
* |
139
|
|
|
* @param mixed $numberOfPeriods The total number of payment periods in an annuity |
140
|
|
|
* @param mixed $payment The payment made each period and cannot change over the life of the annuity. |
141
|
|
|
* Typically, pmt includes principal and interest but no other fees or taxes. |
142
|
|
|
* @param mixed $presentValue The present value - the total amount that a series of future payments is worth now |
143
|
|
|
* @param mixed $futureValue The future value, or a cash balance you want to attain after the last payment is made. |
144
|
|
|
* If fv is omitted, it is assumed to be 0 (the future value of a loan, |
145
|
|
|
* for example, is 0). |
146
|
|
|
* @param mixed $type A number 0 or 1 and indicates when payments are due: |
147
|
|
|
* 0 or omitted At the end of the period. |
148
|
|
|
* 1 At the beginning of the period. |
149
|
|
|
* @param mixed $guess Your guess for what the rate will be. |
150
|
|
|
* If you omit guess, it is assumed to be 10 percent. |
151
|
|
|
*/ |
152
|
24 |
|
public static function rate( |
153
|
|
|
mixed $numberOfPeriods, |
154
|
|
|
mixed $payment, |
155
|
|
|
mixed $presentValue, |
156
|
|
|
mixed $futureValue = 0.0, |
157
|
|
|
mixed $type = FinancialConstants::PAYMENT_END_OF_PERIOD, |
158
|
|
|
mixed $guess = 0.1 |
159
|
|
|
): string|float { |
160
|
24 |
|
$numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); |
161
|
24 |
|
$payment = Functions::flattenSingleValue($payment); |
162
|
24 |
|
$presentValue = Functions::flattenSingleValue($presentValue); |
163
|
24 |
|
$futureValue = Functions::flattenSingleValue($futureValue) ?? 0.0; |
164
|
24 |
|
$type = Functions::flattenSingleValue($type) ?? FinancialConstants::PAYMENT_END_OF_PERIOD; |
165
|
24 |
|
$guess = Functions::flattenSingleValue($guess) ?? 0.1; |
166
|
|
|
|
167
|
|
|
try { |
168
|
24 |
|
$numberOfPeriods = CashFlowValidations::validateFloat($numberOfPeriods); |
169
|
23 |
|
$payment = CashFlowValidations::validateFloat($payment); |
170
|
22 |
|
$presentValue = CashFlowValidations::validatePresentValue($presentValue); |
171
|
21 |
|
$futureValue = CashFlowValidations::validateFutureValue($futureValue); |
172
|
20 |
|
$type = CashFlowValidations::validatePeriodType($type); |
173
|
19 |
|
$guess = CashFlowValidations::validateFloat($guess); |
174
|
6 |
|
} catch (Exception $e) { |
175
|
6 |
|
return $e->getMessage(); |
176
|
|
|
} |
177
|
|
|
|
178
|
18 |
|
$rate = $guess; |
179
|
|
|
// rest of code adapted from python/numpy |
180
|
18 |
|
$close = false; |
181
|
18 |
|
$iter = 0; |
182
|
18 |
|
while (!$close && $iter < self::FINANCIAL_MAX_ITERATIONS) { |
183
|
18 |
|
$nextdiff = self::rateNextGuess($rate, $numberOfPeriods, $payment, $presentValue, $futureValue, $type); |
184
|
18 |
|
if (!is_numeric($nextdiff)) { |
185
|
2 |
|
break; |
186
|
|
|
} |
187
|
16 |
|
$rate1 = $rate - $nextdiff; |
188
|
16 |
|
$close = abs($rate1 - $rate) < self::FINANCIAL_PRECISION; |
189
|
16 |
|
++$iter; |
190
|
16 |
|
$rate = $rate1; |
191
|
|
|
} |
192
|
|
|
|
193
|
18 |
|
return $close ? $rate : ExcelError::NAN(); |
194
|
|
|
} |
195
|
|
|
|
196
|
18 |
|
private static function rateNextGuess(float $rate, float $numberOfPeriods, float $payment, float $presentValue, float $futureValue, int $type): string|float |
197
|
|
|
{ |
198
|
18 |
|
if ($rate == 0.0) { |
199
|
1 |
|
return ExcelError::NAN(); |
200
|
|
|
} |
201
|
17 |
|
$tt1 = ($rate + 1) ** $numberOfPeriods; |
202
|
17 |
|
$tt2 = ($rate + 1) ** ($numberOfPeriods - 1); |
203
|
17 |
|
$numerator = $futureValue + $tt1 * $presentValue + $payment * ($tt1 - 1) * ($rate * $type + 1) / $rate; |
204
|
17 |
|
$denominator = $numberOfPeriods * $tt2 * $presentValue - $payment * ($tt1 - 1) |
205
|
17 |
|
* ($rate * $type + 1) / ($rate * $rate) + $numberOfPeriods |
206
|
17 |
|
* $payment * $tt2 * ($rate * $type + 1) / $rate + $payment * ($tt1 - 1) * $type / $rate; |
207
|
17 |
|
if ($denominator == 0) { |
208
|
1 |
|
return ExcelError::NAN(); |
209
|
|
|
} |
210
|
|
|
|
211
|
16 |
|
return $numerator / $denominator; |
212
|
|
|
} |
213
|
|
|
} |
214
|
|
|
|