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

src/ExcelGoalSeek/ExcelGoalSeek.php (1 issue)

Labels
Severity
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
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