Failed Conditions
Push — master ( 0e0722...2dd953 )
by Roman
02:19
created

ExcelGoalSeek::calculate()   F

Complexity

Conditions 36
Paths 348

Size

Total Lines 137
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 51
CRAP Score 44.9596

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 67
c 1
b 0
f 0
dl 0
loc 137
ccs 51
cts 63
cp 0.8095
rs 1.4833
cc 36
nc 348
nop 11
crap 44.9596

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->debugEnabled = $logger === null ? false : true;
21 10
        $this->logger = $logger;
22 10
    }
23
24 9
    private function debug($message, array $context = []): void
25
    {
26 9
        if ($this->debugEnabled) {
27 4
            $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

27
            $this->logger->/** @scrutinizer ignore-call */ 
28
                           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...
28
        }
29 9
    }
30
31 10
    public function calculate(
32
        $functionGS,
33
        $goal,
34
        $decimal_places,
35
        $incremental_modifier = 1,
36
        $max_loops_round = 0,
37
        $max_loops_dec = 0,
38
        $lock_min = ['num' => null, 'goal' => null],
39
        $lock_max = ['num' => null, 'goal' => null],
40
        $slope = null,
41
        $randomized = false,
42
        $start_from = 0.1
43
    )
44
    {
45 10
        if (empty($functionGS)) {
46 1
            throw new ExcelGoalSeekException('Function callback expected');
47
        }
48
49 9
        $max_loops_round++;
50
51 9
        $maximum_acceptable_difference = 0.1;//If goal found has more than this difference, return null as it is not found
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
        if ($lock_min['num'] === null && $lock_max['num'] === null) {
106 9
            $aux_obj_num = $start_from;
107
        } //Lower limit found, searching higher limit with * 10
108 9
        elseif ($lock_min['num'] !== null && $lock_max['num'] === null) {
109 8
            if ($lock_min['num'] == $start_from) {
110 8
                $aux_obj_num = 1;
111
            } else {
112 8
                $aux_obj_num = $lock_min['num'] * (10 / $incremental_modifier);
113
            }
114
        } //Higher limit found, searching lower limit with * -10
115 9
        elseif ($lock_min['num'] === null && $lock_max['num'] !== null) {
116 1
            if ($lock_max['num'] == $start_from) {
117 1
                $aux_obj_num = -1;
118
            } else {
119 1
                $aux_obj_num = $lock_max['num'] * (10 / $incremental_modifier);
120
            }
121
        } //I have both limits, searching between them without decimals
122 8
        elseif ($lock_min['num'] !== null && $lock_max['num'] !== null) {
123 8
            $aux_obj_num = round(($lock_min['num'] + $lock_max['num']) / 2);
124
        }
125
126 9
        if ($aux_obj_num != $start_from) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $aux_obj_num does not seem to be defined for all execution paths leading up to this point.
Loading history...
127 9
            $aux_obj = $this->$functionGS($aux_obj_num);
128 9
            $this->debug('Testing ' . $aux_obj_num . ' with value ' . $aux_obj);
129
        } else {
130 9
            $aux_obj = $this->$functionGS($aux_obj_num);
131 9
            $this->debug('Testing (with initial value) ' . $aux_obj_num . ' with value ' . $aux_obj);
132
        }
133
134 9
        if ($slope == null) {
135 9
            $aux_slope = $this->$functionGS($aux_obj_num + 0.1);
136
137 9
            if (is_nan($aux_slope) || is_nan($aux_obj)) {
138
                $slope = null; //If slope is null
139 9
            } elseif ($aux_slope - $aux_obj > 0) {
140 9
                $slope = 1;
141
            } else {
142
                $slope = -1;
143
            }
144
        }
145
146
        //Test if formule can give me non valid values, i.e.: sqrt of negative value
147 9
        if (!is_nan($aux_obj)) {
148
            //Is goal without decimals?
149 9
            list($lock_min, $lock_max) = $this->lookWithoutDecimals($aux_obj, $goal, $aux_obj_num, $lock_min, $lock_max, $slope);
150
        } else {
151
            if (($lock_min['num'] === null && $lock_max['num'] === null) || $randomized) {
152
                $nuevo_start_from = random_int(-500, 500);
153
154
                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);
155
            } //First iteration is null
156
157
            if ($lock_min['num'] !== null && abs(abs($aux_obj_num) - abs($lock_min['num'])) < 1) {
158
                $lock_max['num'] = $aux_obj_num;
159
            }
160
            if ($lock_max['num'] !== null && abs(abs($aux_obj_num) - abs($lock_max['num'])) < 1) {
161
                $lock_min['num'] = $aux_obj_num;
162
            }
163
164
            return $this->calculate($functionGS, $goal, $decimal_places, $incremental_modifier + 1, $max_loops_round, $max_loops_dec, $lock_min, $lock_max, $slope, $randomized, $start_from);
165
        }
166
167 9
        return $this->calculate($functionGS, $goal, $decimal_places, $incremental_modifier, $max_loops_round, $max_loops_dec, $lock_min, $lock_max, $slope, $randomized, $start_from);
168
    }
169
170 9
    private function lookWithoutDecimals($aux_obj, $goal, $aux_obj_num, $lock_min, $lock_max, $slope): array
171
    {
172 9
        if ($aux_obj == $goal) {
173 8
            $lock_min['num'] = $aux_obj_num;
174 8
            $lock_min['goal'] = $aux_obj;
175
176 8
            $lock_max['num'] = $aux_obj_num;
177 8
            $lock_max['goal'] = $aux_obj;
178
        }
179
180 9
        $going_up = false;
181 9
        if ($aux_obj < $goal) {
182 8
            $going_up = true;
183
        }
184 9
        if ($aux_obj > $goal) {
185 9
            $going_up = false;
186
        }
187 9
        if ($slope == -1) {
188
            $going_up = !$going_up;
189
        }
190
191 9
        if ($going_up) {
192 8
            if ($lock_min['num'] !== null && $aux_obj_num < $lock_min['num']) {
193
                $lock_max['num'] = $lock_min['num'];
194
                $lock_max['goal'] = $lock_min['goal'];
195
            }
196
197 8
            $lock_min['num'] = $aux_obj_num;
198 8
            $lock_min['goal'] = $aux_obj;
199
        }
200
201 9
        if (!$going_up) {
202 9
            if ($lock_max['num'] !== null && $lock_max['num'] < $aux_obj_num) {
203
                $lock_min['num'] = $lock_max['num'];
204
                $lock_min['goal'] = $lock_max['goal'];
205
            }
206
207 9
            $lock_max['num'] = $aux_obj_num;
208 9
            $lock_max['goal'] = $aux_obj;
209
        }
210 9
        return [$lock_min, $lock_max];
211
    }
212
}
213