ExcelGoalSeek::debug()   A
last analyzed

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\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