Failed Conditions
Push — master ( ea97af...216db0 )
by
unknown
15:51 queued 07:40
created

Interest   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 201
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 20
eloc 87
dl 0
loc 201
ccs 86
cts 86
cp 1
rs 10
c 0
b 0
f 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
A rateNextGuess() 0 16 3
B schedulePayment() 0 36 6
A payment() 0 42 5
B rate() 0 42 6
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