Failed Conditions
Push — master ( 2dd953...76ac40 )
by Roman
01:26
created

ExcelGoalSeek::calculate()   D

Complexity

Conditions 26
Paths 60

Size

Total Lines 119
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 49
CRAP Score 26.2909

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 54
c 2
b 0
f 0
dl 0
loc 119
ccs 49
cts 53
cp 0.9245
rs 4.1666
cc 26
nc 60
nop 11
crap 26.2909

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
    private $debugEnabled;
13
    /**
14
     * @var LoggerInterface|null
15
     */
16
    private $logger;
17
18 10
    public function __construct(LoggerInterface $logger = null)
19
    {
20 10
        $this->logger = $logger;
21 10
    }
22 10
23
    private function debug($message, array $context = []): void
24 9
    {
25
        if ($this->debugEnabled instanceof LoggerInterface) {
26 9
            $this->logger->debug($message, $context);
0 ignored issues
show
Bug introduced by
The method debug() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

26
            $this->logger->/** @scrutinizer ignore-call */ 
27
                           debug($message, $context);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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