ExcelGoalSeek::calculate()   D
last analyzed

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