Failed Conditions
CANCELLED  
Push — master ( d77ca3...4fec13 )
by Roman
01:45
created

ExcelGoalSeek::calculate()   D

Complexity

Conditions 26
Paths 36

Size

Total Lines 111
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 48
CRAP Score 26.1374

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 48
c 3
b 0
f 0
dl 0
loc 111
ccs 48
cts 51
cp 0.9412
rs 4.1666
cc 26
nc 36
nop 11
crap 26.1374

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