Magento 2 Documentation  2.3
Documentation for Magento 2 CMS v2.3 (December 2018)
Algorithm.php
Go to the documentation of this file.
1 <?php
7 
15 class Algorithm
16 {
20  const MIN_POSSIBLE_VALUE = .01;
21 
26 
31 
37 
42 
44 
50  protected $_upperLimit = null;
51 
57  protected $_lowerLimit = null;
58 
64  protected $_intervalsNumber = null;
65 
72 
78  protected $_count = 0;
79 
85  protected $_quantileInterval = [0, 0];
86 
92  protected $_values = [];
93 
99  protected $_maxValue = 0;
100 
106  protected $_minValue = 0;
107 
113  protected $_lastValueLimiter = [null, 0];
114 
122  public function setLimits($lowerLimit = null, $upperLimit = null)
123  {
124  $this->_lowerLimit = empty($lowerLimit) ? null : (double)$lowerLimit;
125  $this->_upperLimit = empty($upperLimit) ? null : (double)$upperLimit;
126 
127  return $this;
128  }
129 
139  public function setStatistics($min, $max, $standardDeviation, $count)
140  {
141  $this->_count = $count;
142  $this->_minValue = $min;
143  $this->_maxValue = $max;
144  $valueRange = $max - $min;
145  if ($count < 2 || $valueRange <= 0) {
146  //Same value couldn't be separated with several intervals
147  $this->_intervalsNumber = 1;
148 
149  return $this;
150  }
151 
152  if ($standardDeviation <= 0) {
153  $intervalsNumber = pow(10, self::TEN_POWER_ROUNDING_FACTOR);
154  } else {
155  $intervalsNumber = $valueRange * pow($count, 1 / 3) / (3.5 * $standardDeviation);
156  }
157  $this->_intervalsNumber = max(ceil($intervalsNumber), self::MIN_INTERVALS_NUMBER);
158  $this->_intervalsNumber = (int)min($this->_intervalsNumber, self::MAX_INTERVALS_NUMBER);
159 
160  return $this;
161  }
162 
171  public function calculateSeparators(IntervalInterface $interval)
172  {
173  $result = [];
174  $lastCount = 0;
175  $intervalFirstValue = $this->_minValue;
176  $lastSeparator = $this->_lowerLimit === null ? 0 : $this->_lowerLimit;
177 
178  for ($intervalNumber = 1; $intervalNumber < $this->getIntervalsNumber(); ++$intervalNumber) {
179  $separator = $this->_findValueSeparator($intervalNumber, $interval);
180  if (empty($separator)) {
181  continue;
182  }
183  if ($this->_quantileInterval[0] == 0) {
184  $intervalFirstValue = $this->_values[0];
185  }
186  $separatorCandidate = false;
187  $newIntervalFirstValue = $intervalFirstValue;
188  $newLastSeparator = $lastSeparator;
189 
190  $valuesPerInterval = $this->_count / $this->_getCalculatedIntervalsNumber();
191  while (!empty($separator) && !array_key_exists($intervalNumber, $result)) {
192  $separatorsPortion = array_shift($separator);
193  $bestSeparator = $this->_findBestSeparator($intervalNumber, $separatorsPortion);
194  if ($bestSeparator && $bestSeparator[2] > 0) {
195  $isEqualValue = $intervalFirstValue ==
196  $this->_values[$bestSeparator[2] - 1] ? $this->_values[0] : false;
197  $count = $bestSeparator[2] + $this->_quantileInterval[0] - $lastCount;
198  $separatorData = [
199  'from' => $isEqualValue !== false ? $isEqualValue : $lastSeparator,
200  'to' => $isEqualValue !== false ? $isEqualValue : $bestSeparator[1],
201  'count' => $count,
202  ];
203  if (abs(1 - $count / $valuesPerInterval) <= self::INTERVAL_DEFLECTION_LIMIT) {
204  $newLastSeparator = $bestSeparator[1];
205  $newIntervalFirstValue = $this->_values[$bestSeparator[2]];
206  $result[$intervalNumber] = $separatorData;
207  } elseif (!$separatorCandidate || $bestSeparator[0] < $separatorCandidate[0]) {
208  $separatorCandidate = [
209  $bestSeparator[0],
210  $separatorData,
211  $bestSeparator[1],
212  $this->_values[$bestSeparator[2]],
213  ];
214  }
215  }
216  }
217 
218  if (!array_key_exists($intervalNumber, $result) && $separatorCandidate) {
219  $newLastSeparator = $separatorCandidate[2];
220  $newIntervalFirstValue = $separatorCandidate[3];
221  $result[$intervalNumber] = $separatorCandidate[1];
222  }
223 
224  if (array_key_exists($intervalNumber, $result)) {
225  $lastSeparator = $newLastSeparator;
226  $intervalFirstValue = $newIntervalFirstValue;
227  $valueIndex = $this->_binarySearch($lastSeparator);
228  $lastCount += $result[$intervalNumber]['count'];
229  if ($valueIndex != -1 && $lastSeparator > $this->_lastValueLimiter[1]) {
230  $this->_lastValueLimiter = [$valueIndex + $this->_quantileInterval[0], $lastSeparator];
231  }
232  }
233  }
234  if ($this->_lastValueLimiter[0] < $this->_count) {
235  $isEqualValue = $intervalFirstValue == $this->_maxValue ? $intervalFirstValue : false;
236  $result[$this->getIntervalsNumber()] = [
237  'from' => $isEqualValue ? $isEqualValue : $lastSeparator,
238  'to' => $isEqualValue ? $isEqualValue : ($this->_upperLimit === null ? '' : $this->_upperLimit),
239  'count' => $this->_count - $lastCount,
240  ];
241  }
242 
243  return array_values($result);
244  }
245 
251  protected function getIntervalsNumber()
252  {
253  if ($this->_intervalsNumber !== null) {
255  }
256 
257  return 1;
258  }
259 
270  protected function _findValueSeparator($quantileNumber, IntervalInterface $interval)
271  {
272  if ($quantileNumber < 1 || $quantileNumber >= $this->getIntervalsNumber()) {
273  return null;
274  }
275 
276  $values = [];
277  $quantileInterval = $this->_getQuantileInterval($quantileNumber);
278  $intervalValuesCount = $quantileInterval[1] - $quantileInterval[0] + 1;
279  $offset = $quantileInterval[0];
280  if ($this->_lastValueLimiter[0] !== null) {
281  $offset -= $this->_lastValueLimiter[0];
282  }
283  if ($offset < 0) {
284  $intervalValuesCount += $offset;
285  $values = array_slice(
286  $this->_values,
287  $this->_lastValueLimiter[0] + $offset - $this->_quantileInterval[0],
288  -$offset
289  );
290  $offset = 0;
291  }
292  $lowerValue = $this->_lastValueLimiter[1];
293  if ($this->_lowerLimit !== null) {
294  $lowerValue = max($lowerValue, $this->_lowerLimit);
295  }
296  if ($intervalValuesCount >= 0) {
297  $values = array_merge(
298  $values,
299  $interval->load($intervalValuesCount + 1, $offset, $lowerValue, $this->_upperLimit)
300  );
301  }
302  $lastValue = $values[$intervalValuesCount - 1];
303  $bestRoundValue = [];
304  if ($lastValue == $values[0]) {
305  if ($quantileNumber == 1 && $offset) {
306  $additionalValues = $interval->loadPrevious($lastValue, $quantileInterval[0], $this->_lowerLimit);
307  if ($additionalValues) {
308  $quantileInterval[0] -= count($additionalValues);
309  $values = array_merge($additionalValues, $values);
310  $bestRoundValue = $this->_findRoundValue(
311  $values[0] + self::MIN_POSSIBLE_VALUE / 10,
312  $lastValue,
313  false
314  );
315  }
316  }
317  if ($quantileNumber == $this->getIntervalsNumber() - 1) {
318  $valuesCount = count($values);
319  if ($values[$valuesCount - 1] > $lastValue) {
320  $additionalValues = [$values[$valuesCount - 1]];
321  } else {
322  $additionalValues = $interval->loadNext(
323  $lastValue,
324  $this->_count - $quantileInterval[0] - count($values),
325  $this->_upperLimit
326  );
327  }
328  if ($additionalValues) {
329  $quantileInterval[1] = $quantileInterval[0] + count($values) - 1;
330  if ($values[$valuesCount - 1] <= $lastValue) {
331  $quantileInterval[1] += count($additionalValues);
332  $values = array_merge($values, $additionalValues);
333  }
334  $upperBestRoundValue = $this->_findRoundValue(
335  $lastValue + self::MIN_POSSIBLE_VALUE / 10,
336  $values[count($values) - 1],
337  false
338  );
339  $this->_mergeRoundValues($bestRoundValue, $upperBestRoundValue);
340  }
341  }
342  } else {
343  $bestRoundValue = $this->_findRoundValue(
344  $values[0] + self::MIN_POSSIBLE_VALUE / 10,
345  $lastValue
346  );
347  }
348 
349  $this->_quantileInterval = $quantileInterval;
350  $this->_values = $values;
351 
352  if (empty($bestRoundValue)) {
353  $this->_skippedQuantilesUpperLimits[$quantileNumber] = $quantileInterval[1];
354 
355  return $bestRoundValue;
356  }
357 
358  $valuesCount = count($values);
359  if ($values[$valuesCount - 1] > $lastValue) {
360  $this->_lastValueLimiter = [$quantileInterval[0] + $valuesCount - 1, $values[$valuesCount - 1]];
361  }
362 
363  ksort($bestRoundValue, SORT_NUMERIC);
364  foreach ($bestRoundValue as $index => &$bestRoundValueValues) {
365  if (empty($bestRoundValueValues)) {
366  unset($bestRoundValue[$index]);
367  } else {
368  sort($bestRoundValueValues);
369  }
370  }
371 
372  return array_reverse($bestRoundValue);
373  }
374 
381  protected function _getQuantileInterval($quantileNumber)
382  {
383  if ($quantileNumber < 1 || $quantileNumber >= $this->getIntervalsNumber()) {
384  return null;
385  }
386  $quantile = $this->_getQuantile($quantileNumber);
387  $deflectionLimit = floor($this->_count / 2 / $this->getIntervalsNumber());
388  $limits = [
389  min(floor($quantile - $deflectionLimit), floor($quantile)),
390  max(ceil($quantile + $deflectionLimit - 1), ceil($quantile)),
391  ];
392 
393  $sqrtParam = $this->_count * $quantileNumber * ($this->getIntervalsNumber() - $quantileNumber);
394  $deflection = self::STANDARD_NORMAL_DISTRIBUTION * sqrt($sqrtParam) / $this->getIntervalsNumber();
395  $left = max(floor($quantile - $deflection - 1), $limits[0], 0);
396  if (array_key_exists($quantileNumber - 1, $this->_skippedQuantilesUpperLimits)
397  && $left > $this->_skippedQuantilesUpperLimits[$quantileNumber - 1]
398  ) {
399  $left = $this->_skippedQuantilesUpperLimits[$quantileNumber - 1];
400  }
401  $right = min(ceil($quantile + $deflection), $limits[1], $this->_count - 1);
402 
403  return [$left, $right];
404  }
405 
412  protected function _getQuantile($quantileNumber)
413  {
414  if ($quantileNumber < 1 || $quantileNumber >= $this->getIntervalsNumber()) {
415  return 0;
416  }
417 
418  return $quantileNumber * $this->_count / $this->getIntervalsNumber() - .5;
419  }
420 
432  protected function _findRoundValue($lowerValue, $upperValue, $returnEmpty = true, $roundingFactor = null)
433  {
434  $lowerValue = round($lowerValue, 3);
435  $upperValue = round($upperValue, 3);
436 
437  if ($roundingFactor !== null) {
438  // Can't separate if values are equal
439  if ($lowerValue >= $upperValue) {
440  if ($lowerValue > $upperValue || $returnEmpty) {
441  return false;
442  }
443  }
444  // round is used for such examples: (1194.32 / 0.02) or (5 / 100000)
445  $lowerDivision = ceil(round($lowerValue / $roundingFactor, self::TEN_POWER_ROUNDING_FACTOR + 3));
446  $upperDivision = floor(round($upperValue / $roundingFactor, self::TEN_POWER_ROUNDING_FACTOR + 3));
447 
448  $result = [];
449  if ($upperDivision <= 0 || $upperDivision - $lowerDivision > 10) {
450  return $result;
451  }
452 
453  for ($i = $lowerDivision; $i <= $upperDivision; ++$i) {
454  $result[] = round($i * $roundingFactor, 2);
455  }
456 
457  return $result;
458  }
459 
460  $result = [];
461  $tenPower = pow(10, self::TEN_POWER_ROUNDING_FACTOR);
462  $roundingFactorCoefficients = [10, 5, 2];
463  while ($tenPower >= self::MIN_POSSIBLE_VALUE) {
464  if ($tenPower == self::MIN_POSSIBLE_VALUE) {
465  $roundingFactorCoefficients[] = 1;
466  }
467  foreach ($roundingFactorCoefficients as $roundingFactorCoefficient) {
468  $roundingFactorCoefficient *= $tenPower;
469  $roundValues = $this->_findRoundValue(
470  $lowerValue,
471  $upperValue,
472  $returnEmpty,
473  $roundingFactorCoefficient
474  );
475  if ($roundValues) {
476  $index = round(
477  $roundingFactorCoefficient /
478  self::MIN_POSSIBLE_VALUE
479  );
480  $result[$index] = $roundValues;
481  }
482  }
483  $tenPower /= 10;
484  }
485 
486  return empty($result) ? [1 => []] : $result;
487  }
488 
496  protected function _mergeRoundValues(&$oldRoundValues, &$newRoundValues)
497  {
498  foreach ($newRoundValues as $roundingFactor => $roundValueValues) {
499  if (array_key_exists($roundingFactor, $oldRoundValues)) {
500  $oldRoundValues[$roundingFactor] = array_unique(
501  array_merge($oldRoundValues[$roundingFactor], $roundValueValues)
502  );
503  } else {
504  $oldRoundValues[$roundingFactor] = $roundValueValues;
505  }
506  }
507  }
508 
514  protected function _getCalculatedIntervalsNumber()
515  {
516  return max(1, $this->getIntervalsNumber() - count($this->_skippedQuantilesUpperLimits));
517  }
518 
526  protected function _findBestSeparator($quantileNumber, $separators)
527  {
528  $result = false;
529 
530  $i = 0;
531  $valuesCount = count($this->_values);
532  while ($i < $valuesCount && !empty($separators)) {
533  $i = $this->_binarySearch($separators[0], [$i]);
534  if ($i == -1) {
535  break;
536  }
537 
538  $separator = array_shift($separators);
539 
540  $deflection = abs(
541  $quantileNumber * $this->_count -
542  ($this->_quantileInterval[0] +
544  );
545  if (!$result || $deflection < $result[0]) {
546  $result = [$deflection, $separator, $i];
547  }
548  }
549 
550  return $result ? $result : false;
551  }
552 
563  protected function _binarySearch($value, $limits = null)
564  {
565  if (empty($this->_values)) {
566  return -1;
567  }
568 
569  if (!is_array($limits)) {
570  $limits = [];
571  }
572  if (!isset($limits[0])) {
573  $limits[0] = 0;
574  }
575  if (!isset($limits[1])) {
576  $limits[1] = count($this->_values) - 1;
577  }
578 
579  if ($limits[0] > $limits[1] || $this->_values[$limits[1]] < $value) {
580  return -1;
581  }
582 
583  if ($limits[1] - $limits[0] <= 1) {
584  return $this->_values[$limits[0]] < $value ? $limits[1] : $limits[0];
585  }
586 
587  $separator = floor(($limits[0] + $limits[1]) / 2);
588  if ($this->_values[$separator] < $value) {
589  $limits[0] = $separator + 1;
590  } else {
591  $limits[1] = $separator;
592  }
593 
594  return $this->_binarySearch($value, [$limits[0], $limits[1]]);
595  }
596 }
_findRoundValue($lowerValue, $upperValue, $returnEmpty=true, $roundingFactor=null)
Definition: Algorithm.php:432
elseif(isset( $params[ 'redirect_parent']))
Definition: iframe.phtml:17
load($limit, $offset=null, $lower=null, $upper=null)
$count
Definition: recent.phtml:13
$values
Definition: options.phtml:88
setLimits($lowerLimit=null, $upperLimit=null)
Definition: Algorithm.php:122
calculateSeparators(IntervalInterface $interval)
Definition: Algorithm.php:171
$value
Definition: gender.phtml:16
_mergeRoundValues(&$oldRoundValues, &$newRoundValues)
Definition: Algorithm.php:496
_findValueSeparator($quantileNumber, IntervalInterface $interval)
Definition: Algorithm.php:270
_findBestSeparator($quantileNumber, $separators)
Definition: Algorithm.php:526
loadNext($data, $rightIndex, $upper=null)
setStatistics($min, $max, $standardDeviation, $count)
Definition: Algorithm.php:139
$i
Definition: gallery.phtml:31
$index
Definition: list.phtml:44