Passed
Push — master ( e3a7c6...34fca2 )
by Roman
02:00
created

ExcelGoalSeek::checkLimitRestriction()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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