Passed
Push — master ( e3a7c6...34fca2 )
by Roman
02:00
created

ExcelGoalSeek::calculate()   D

Complexity

Conditions 24
Paths 34

Size

Total Lines 104
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 44
CRAP Score 24.0474

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 45
c 4
b 0
f 0
dl 0
loc 104
ccs 44
cts 46
cp 0.9565
rs 4.1666
cc 24
nc 34
nop 11
crap 24.0474

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace P4lv\ExcelGoalSeek;
4
5
use P4lv\ExcelGoalSeek\Exception\ExcelGoalSeekException;
6
use P4lv\ExcelGoalSeek\Exception\GoalNeverReached;
7
use P4lv\ExcelGoalSeek\Exception\GoalReachedNotEnough;
8
use Psr\Log\LoggerInterface;
9
10
class ExcelGoalSeek
11
{
12
    /**
13
     * @var LoggerInterface|null
14
     */
15
    private $logger;
16
17
    public function __construct(LoggerInterface $logger = null)
18 10
    {
19
        $this->logger = $logger;
20 10
    }
21 10
22 10
    private function debug($message, array $context = []): void
23
    {
24 9
        if ($this->logger instanceof LoggerInterface) {
25
            $this->logger->debug($message, $context);
26 9
        }
27 4
    }
28
29 9
    public function calculate(
30
        $functionGS,
31 10
        $goal,
32
        $decimal_places,
33
        $incremental_modifier = 1,
34
        $max_loops_round = 0,
35
        $max_loops_dec = 0,
36
        $lock_min = ['num' => null, 'goal' => null],
37
        $lock_max = ['num' => null, 'goal' => null],
38
        $slope = null,
39
        $randomized = false,
40
        $start_from = 0.1
41
    )
42
    {
43
        //If goal found has more than this difference, return null as it is not found
44
        $maximum_acceptable_difference = 0.1;
45 10
        $max_loops_round++;
46 1
47
        $this->checkLimitRestriction($functionGS, $max_loops_round);
48
49 9
        $this->debug(sprintf("Iteration %d; min value = %s; max value = %s; slope %s", $max_loops_round, $lock_min['num'], $lock_max['num'], $slope));
50
51 9
        //If I have the goal  limited to a unit, I seek decimals
52
        if ($lock_min['num'] !== null && $lock_max['num'] !== null && abs(abs($lock_max['num']) - abs($lock_min['num'])) <= 1) {
53 9
54 1
            //No decimal , return result
55
            if ($lock_min['num'] == $lock_max['num']) {
56 1
                return $lock_min['num'];
57
            }
58
59 9
            //Seek decimals
60
            foreach (range(1, $decimal_places, 1) as $decimal) {
61
                $decimal_step = 1 / (10 ** $decimal);
62 9
63
                $difference = abs(round(abs($lock_max['num']), $decimal) - round(abs($lock_min['num']), $decimal));
64
65 8
                while ($difference - ($decimal_step / 10) > $decimal_step && $max_loops_dec < (2000 * $decimal_places)) {
66 2
                    $max_loops_dec++;
67
68
                    $aux_obj_num = round(($lock_min['num'] + $lock_max['num']) / 2, $decimal);
69
                    $aux_obj = $this->$functionGS($aux_obj_num);
70 6
71 6
                    $this->debug(sprintf("Decimal iteration %d; min value = %s; max value = %s; value %s", $max_loops_dec, $lock_min['num'], $lock_max['num'], $aux_obj));
72
73 6
                    //Like when I look without decimals
74
                    [$lock_min, $lock_max] = $this->lookWithoutDecimals($aux_obj, $goal, $aux_obj_num, $lock_min, $lock_max, $slope);
75 6
                    //End Like when I look without decimals
76 6
                    $difference = abs(round(abs($lock_max['num']), $decimal) - round(abs($lock_min['num']), $decimal));
77
                }//End while
78 6
            }//End foreach
79 6
80
81 6
            if ($max_loops_dec > 2000 * $decimal_places) {
82 6
                throw new GoalNeverReached('Goal never reached [2000]');
83
            }
84
85
            if (!is_nan($lock_min['goal']) && abs(abs($lock_min['goal']) - abs($goal)) < $maximum_acceptable_difference) {
86 6
                return round($lock_min['num'], $decimal_places - 1);
87
            }
88 6
89
            throw new GoalReachedNotEnough('Goal reached not enough');
90
        }
91
92
        //First iteration, try with zero
93 6
        $aux_obj_num = $this->getAuxObjNum($lock_min['num'], $lock_max['num'], $start_from, $incremental_modifier);
94
95
        $aux_obj = $this->$functionGS($aux_obj_num);
96
97 6
        $this->debug(sprintf("Testing (with initial value) %s%d with value %s", $aux_obj_num != $start_from ? '' : '(with initial value)', $aux_obj_num, $aux_obj));
98 6
99
        if ($slope === null) {
100
            $aux_slope = $this->$functionGS($aux_obj_num + 0.1);
101
102
            if (is_nan($aux_slope) || is_nan($aux_obj)) {
103
                $slope = null; //If slope is null
104
            } elseif ($aux_slope - $aux_obj > 0) {
105 9
                $slope = 1;
106 9
            } else {
107
                $slope = -1;
108 9
            }
109 8
        }
110 8
111
        //Test if formule can give me non valid values, i.e.: sqrt of negative value
112 8
        if (!is_nan($aux_obj)) {
113
            //Is goal without decimals?
114
            list($lock_min, $lock_max) = $this->lookWithoutDecimals($aux_obj, $goal, $aux_obj_num, $lock_min, $lock_max, $slope);
115 9
        } else {
116 1
            if (($lock_min['num'] === null && $lock_max['num'] === null) || $randomized) {
117 1
                $nuevo_start_from = random_int(-500, 500);
118
119 1
                return $this->calculate($functionGS, $goal, $decimal_places, $incremental_modifier + 1, $max_loops_round, $max_loops_dec, $lock_min, $lock_max, $slope, true, $nuevo_start_from);
120
            } //First iteration is null
121
122 8
            if ($lock_min['num'] !== null && abs(abs($aux_obj_num) - abs($lock_min['num'])) < 1) {
123 8
                $lock_max['num'] = $aux_obj_num;
124
            }
125
            if ($lock_max['num'] !== null && abs(abs($aux_obj_num) - abs($lock_max['num'])) < 1) {
126 9
                $lock_min['num'] = $aux_obj_num;
127 9
            }
128 9
129
            return $this->calculate($functionGS, $goal, $decimal_places, $incremental_modifier + 1, $max_loops_round, $max_loops_dec, $lock_min, $lock_max, $slope, $randomized, $start_from);
130 9
        }
131 9
132
        return $this->calculate($functionGS, $goal, $decimal_places, $incremental_modifier, $max_loops_round, $max_loops_dec, $lock_min, $lock_max, $slope, $randomized, $start_from);
133
    }
134 9
135 9
    private function lookWithoutDecimals($aux_obj, $goal, $aux_obj_num, $lock_min, $lock_max, $slope): array
136
    {
137 9
        if ($aux_obj == $goal) {
138
            $lock_min['num'] = $aux_obj_num;
139 9
            $lock_min['goal'] = $aux_obj;
140 9
141
            $lock_max['num'] = $aux_obj_num;
142
            $lock_max['goal'] = $aux_obj;
143
        }
144
145
        $going_up = false;
146
        if ($aux_obj < $goal) {
147 9
            $going_up = true;
148
        }
149 9
        if ($aux_obj > $goal) {
150
            $going_up = false;
151
        }
152
        if ($slope == -1) {
153
            $going_up = !$going_up;
154
        }
155
156
        if ($going_up) {
157
            if ($lock_min['num'] !== null && $aux_obj_num < $lock_min['num']) {
158
                $lock_max['num'] = $lock_min['num'];
159
                $lock_max['goal'] = $lock_min['goal'];
160
            }
161
162
            $lock_min['num'] = $aux_obj_num;
163
            $lock_min['goal'] = $aux_obj;
164
        }
165
166
        if (!$going_up) {
167 9
            if ($lock_max['num'] !== null && $lock_max['num'] < $aux_obj_num) {
168
                $lock_min['num'] = $lock_max['num'];
169
                $lock_min['goal'] = $lock_max['goal'];
170 9
            }
171
172 9
            $lock_max['num'] = $aux_obj_num;
173 8
            $lock_max['goal'] = $aux_obj;
174 8
        }
175
        return [$lock_min, $lock_max];
176 8
    }
177 8
178
    /**
179
     * @param $lockMinNum
180 9
     * @param $lockMaxNum
181 9
     * @param $start_from
182 8
     * @param $incremental_modifier
183
     * @return float|int|mixed
184 9
     */
185 9
    private function getAuxObjNum($lockMinNum, $lockMaxNum, $start_from, $incremental_modifier)
186
    {
187 9
        $aux_obj_num = null;
188
        if ($lockMinNum === null && $lockMaxNum === null) {
189
            $aux_obj_num = $start_from;
190
        } //Lower limit found, searching higher limit with * 10
191 9
        elseif ($lockMinNum !== null && $lockMaxNum === null) {
192 8
            if ($lockMinNum == $start_from) {
193
                $aux_obj_num = 1;
194
            } else {
195
                $aux_obj_num = $lockMinNum * (10 / $incremental_modifier);
196
            }
197 8
        } //Higher limit found, searching lower limit with * -10
198 8
        elseif ($lockMinNum === null && $lockMaxNum !== null) {
199
            if ($lockMaxNum == $start_from) {
200
                $aux_obj_num = -1;
201 9
            } else {
202 9
                $aux_obj_num = $lockMaxNum * (10 / $incremental_modifier);
203
            }
204
        } //I have both limits, searching between them without decimals
205
        elseif ($lockMinNum !== null && $lockMaxNum !== null) {
206
            $aux_obj_num = round(($lockMinNum + $lockMaxNum) / 2);
207 9
        }
208 9
        return $aux_obj_num;
209
    }
210 9
211
    /**
212
     * @param string $functionGS
213
     * @param int $max_loops_round
214
     * @throws ExcelGoalSeekException
215
     */
216
    private function checkLimitRestriction(string $functionGS, int $max_loops_round): void
217
    {
218
        if (empty($functionGS)) {
219
            throw new ExcelGoalSeekException('Function callback expected');
220
        }
221
222
        if ($max_loops_round > 100) {
223
            throw new GoalNeverReached();
224
        }
225
    }
226
}
227