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

ExcelGoalSeek::debug()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 2
nc 2
nop 2
crap 2
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