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