PageRenderTime 52ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/magento/app/code/core/Mage/Catalog/Model/Layer/Filter/Price/Algorithm.php

https://bitbucket.org/jit_bec/shopifine
PHP | 610 lines | 364 code | 58 blank | 188 comment | 81 complexity | 8bf78e33f5420c26770f820e2649b1a4 MD5 | raw file
Possible License(s): LGPL-3.0
  1. <?php
  2. /**
  3. * Magento
  4. *
  5. * NOTICE OF LICENSE
  6. *
  7. * This source file is subject to the Open Software License (OSL 3.0)
  8. * that is bundled with this package in the file LICENSE.txt.
  9. * It is also available through the world-wide-web at this URL:
  10. * http://opensource.org/licenses/osl-3.0.php
  11. * If you did not receive a copy of the license and are unable to
  12. * obtain it through the world-wide-web, please send an email
  13. * to license@magentocommerce.com so we can send you a copy immediately.
  14. *
  15. * DISCLAIMER
  16. *
  17. * Do not edit or add to this file if you wish to upgrade Magento to newer
  18. * versions in the future. If you wish to customize Magento for your
  19. * needs please refer to http://www.magentocommerce.com for more information.
  20. *
  21. * @category Mage
  22. * @package Mage_Catalog
  23. * @copyright Copyright (c) 2012 Magento Inc. (http://www.magentocommerce.com)
  24. * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
  25. */
  26. /**
  27. * Algorithm for layer price filter
  28. *
  29. * @category Mage
  30. * @package Mage_Catalog
  31. * @author Magento Core Team <core@magentocommerce.com>
  32. */
  33. class Mage_Catalog_Model_Layer_Filter_Price_Algorithm
  34. {
  35. /**
  36. * Rounding factor coefficient
  37. */
  38. const TEN_POWER_ROUNDING_FACTOR = 4;
  39. /**
  40. * Interval deflection coefficient
  41. */
  42. const INTERVAL_DEFLECTION_LIMIT = .3;
  43. /**
  44. * Standard normal distribution's a/2 quantile
  45. * Depends on predefined a. In case of a=0.05
  46. */
  47. const STANDARD_NORMAL_DISTRIBUTION = 1.96;
  48. /**
  49. * Min and Max number of intervals
  50. */
  51. const MIN_INTERVALS_NUMBER = 2;
  52. const MAX_INTERVALS_NUMBER = 10;
  53. /**
  54. * Upper prices limit
  55. *
  56. * @var null|float
  57. */
  58. protected $_upperLimit = null;
  59. /**
  60. * Lower prices limit
  61. *
  62. * @var null|float
  63. */
  64. protected $_lowerLimit = null;
  65. /**
  66. * Number of segmentation intervals
  67. *
  68. * @var null|int
  69. */
  70. protected $_intervalsNumber = null;
  71. /**
  72. * Upper limits of skipped quantiles
  73. *
  74. * @var array
  75. */
  76. protected $_skippedQuantilesUpperLimits = array();
  77. /**
  78. * Total count of prices
  79. *
  80. * @var int
  81. */
  82. protected $_count = 0;
  83. /**
  84. * Prices model
  85. *
  86. * @var null|Mage_Catalog_Model_Layer_Filter_Price
  87. */
  88. protected $_pricesModel = null;
  89. /**
  90. * Current quantile interval
  91. *
  92. * @var array [from, to]
  93. */
  94. protected $_quantileInterval = array(0, 0);
  95. /**
  96. * Prices of current quantile
  97. *
  98. * @var array
  99. */
  100. protected $_prices = array();
  101. /**
  102. * Max price
  103. *
  104. * @var float
  105. */
  106. protected $_maxPrice = 0;
  107. /**
  108. * Min price
  109. *
  110. * @var float
  111. */
  112. protected $_minPrice = 0;
  113. /**
  114. * Last price query limiter
  115. *
  116. * @var array [index, value]
  117. */
  118. protected $_lastPriceLimiter = array(null, 0);
  119. /**
  120. * Set lower and upper limit for algorithm
  121. *
  122. * @param null|float $lowerLimit
  123. * @param null|float $upperLimit
  124. * @return Mage_Catalog_Model_Layer_Filter_Price_Algorithm
  125. */
  126. public function setLimits($lowerLimit = null, $upperLimit = null)
  127. {
  128. $this->_lowerLimit = empty($lowerLimit) ? null : (float)$lowerLimit;
  129. $this->_upperLimit = empty($upperLimit) ? null : (float)$upperLimit;
  130. return $this;
  131. }
  132. /**
  133. * Search first index of price, that satisfy conditions to be 'greater or equal' than $value
  134. * Returns -1 if index was not found
  135. *
  136. * @param float $value
  137. * @param null|array $limits search [from, to]
  138. * @return int
  139. */
  140. protected function _binarySearch($value, $limits = null)
  141. {
  142. if (empty($this->_prices)) {
  143. return -1;
  144. }
  145. if (!is_array($limits)) {
  146. $limits = array();
  147. }
  148. if (!isset($limits[0])) {
  149. $limits[0] = 0;
  150. }
  151. if (!isset($limits[1])) {
  152. $limits[1] = count($this->_prices) - 1;
  153. }
  154. if ($limits[0] > $limits[1] || $this->_prices[$limits[1]] < $value) {
  155. return -1;
  156. }
  157. if ($limits[1] - $limits[0] <= 1) {
  158. return ($this->_prices[$limits[0]] < $value) ? $limits[1] : $limits[0];
  159. }
  160. $separator = floor(($limits[0] + $limits[1]) / 2);
  161. if ($this->_prices[$separator] < $value) {
  162. $limits[0] = $separator + 1;
  163. } else {
  164. $limits[1] = $separator;
  165. }
  166. return $this->_binarySearch($value, array($limits[0], $limits[1]));
  167. }
  168. /**
  169. * Set prices statistics
  170. *
  171. * @param float $min
  172. * @param float $max
  173. * @param float $standardDeviation
  174. * @param int $count
  175. * @return Mage_Catalog_Model_Layer_Filter_Price_Algorithm
  176. */
  177. public function setStatistics($min, $max, $standardDeviation, $count)
  178. {
  179. $this->_count = $count;
  180. $this->_minPrice = $min;
  181. $this->_maxPrice = $max;
  182. $priceRange = $max - $min;
  183. if ($count < 2 || ($priceRange <= 0)) {
  184. //Same price couldn't be separated with several intervals
  185. $this->_intervalsNumber = 1;
  186. return $this;
  187. }
  188. if ($standardDeviation <= 0) {
  189. $intervalsNumber = pow(10, self::TEN_POWER_ROUNDING_FACTOR);
  190. } else {
  191. $intervalsNumber = $priceRange * pow($count, 1 / 3) / (3.5 * $standardDeviation);
  192. }
  193. $this->_intervalsNumber = max(ceil($intervalsNumber), self::MIN_INTERVALS_NUMBER);
  194. $this->_intervalsNumber = (int)min($this->_intervalsNumber, self::MAX_INTERVALS_NUMBER);
  195. return $this;
  196. }
  197. /**
  198. * Set prices model
  199. *
  200. * @param Mage_Catalog_Model_Layer_Filter_Price $pricesModel
  201. * @return Mage_Catalog_Model_Layer_Filter_Price_Algorithm
  202. */
  203. public function setPricesModel($pricesModel)
  204. {
  205. $this->_pricesModel = $pricesModel;
  206. return $this;
  207. }
  208. /**
  209. * Get amount of segmentation intervals
  210. *
  211. * @return int
  212. */
  213. public function getIntervalsNumber()
  214. {
  215. if (!is_null($this->_intervalsNumber)) {
  216. return $this->_intervalsNumber;
  217. }
  218. return 1;
  219. }
  220. /**
  221. * Get intervals number with checking skipped quantiles
  222. *
  223. * @return int
  224. */
  225. protected function _getCalculatedIntervalsNumber()
  226. {
  227. return max(1, $this->getIntervalsNumber() - count($this->_skippedQuantilesUpperLimits));
  228. }
  229. /**
  230. * Get quantile
  231. *
  232. * @param int $quantileNumber should be from 1 to n-1 where n is number of intervals
  233. * @return float|null
  234. */
  235. protected function _getQuantile($quantileNumber)
  236. {
  237. if ($quantileNumber < 1 || $quantileNumber >= $this->getIntervalsNumber()) {
  238. return 0;
  239. }
  240. return $quantileNumber * $this->_count / $this->getIntervalsNumber() - .5;
  241. }
  242. /**
  243. * Get quantile interval
  244. *
  245. * @param int $quantileNumber should be from 1 to n-1 where n is number of intervals
  246. * @return null|array [floatMin,floatMax]
  247. */
  248. protected function _getQuantileInterval($quantileNumber)
  249. {
  250. if ($quantileNumber < 1 || $quantileNumber >= $this->getIntervalsNumber()) {
  251. return null;
  252. }
  253. $quantile = $this->_getQuantile($quantileNumber);
  254. $deflectionLimit = floor($this->_count / 2 / $this->getIntervalsNumber());
  255. $limits = array(
  256. min(floor($quantile - $deflectionLimit), floor($quantile)),
  257. max(ceil($quantile + $deflectionLimit - 1), ceil($quantile)),
  258. );
  259. $deflection = self::STANDARD_NORMAL_DISTRIBUTION
  260. * sqrt($this->_count * $quantileNumber * ($this->getIntervalsNumber() - $quantileNumber))
  261. / $this->getIntervalsNumber();
  262. $left = max(floor($quantile - $deflection - 1), $limits[0], 0);
  263. if (array_key_exists($quantileNumber - 1, $this->_skippedQuantilesUpperLimits)
  264. && $left > $this->_skippedQuantilesUpperLimits[$quantileNumber - 1]
  265. ) {
  266. $left = $this->_skippedQuantilesUpperLimits[$quantileNumber - 1];
  267. }
  268. $right = min(ceil($quantile + $deflection), $limits[1], $this->_count - 1);
  269. return array($left, $right);
  270. }
  271. /**
  272. * Merge new round prices with old ones
  273. *
  274. * @param array $oldRoundPrices
  275. * @param array $newRoundPrices
  276. * @return void
  277. */
  278. protected function _mergeRoundPrices(&$oldRoundPrices, &$newRoundPrices)
  279. {
  280. foreach ($newRoundPrices as $roundingFactor => $roundPriceValues) {
  281. if (array_key_exists($roundingFactor, $oldRoundPrices)) {
  282. $oldRoundPrices[$roundingFactor] = array_unique(array_merge(
  283. $oldRoundPrices[$roundingFactor],
  284. $roundPriceValues
  285. ));
  286. } else {
  287. $oldRoundPrices[$roundingFactor] = $roundPriceValues;
  288. }
  289. }
  290. }
  291. /**
  292. * Find price separator for the quantile
  293. *
  294. * @param int $quantileNumber should be from 1 to n-1 where n is number of intervals
  295. * @return array|null
  296. */
  297. protected function _findPriceSeparator($quantileNumber)
  298. {
  299. if ($quantileNumber < 1 || $quantileNumber >= $this->getIntervalsNumber()) {
  300. return null;
  301. }
  302. $prices = array();
  303. $quantileInterval = $this->_getQuantileInterval($quantileNumber);
  304. $intervalPricesCount = $quantileInterval[1] - $quantileInterval[0] + 1;
  305. $offset = $quantileInterval[0];
  306. if (!is_null($this->_lastPriceLimiter[0])) {
  307. $offset -= $this->_lastPriceLimiter[0];
  308. }
  309. if ($offset < 0) {
  310. $intervalPricesCount += $offset;
  311. $prices = array_slice(
  312. $this->_prices,
  313. $this->_lastPriceLimiter[0] + $offset - $this->_quantileInterval[0],
  314. -$offset
  315. );
  316. $offset = 0;
  317. }
  318. $lowerPrice = $this->_lastPriceLimiter[1];
  319. if (!is_null($this->_lowerLimit)) {
  320. $lowerPrice = max($lowerPrice, $this->_lowerLimit);
  321. }
  322. if ($intervalPricesCount >= 0) {
  323. $prices = array_merge($prices, $this->_pricesModel->loadPrices(
  324. $intervalPricesCount + 1,
  325. $offset,
  326. $lowerPrice,
  327. $this->_upperLimit
  328. ));
  329. }
  330. $lastPrice = $prices[$intervalPricesCount - 1];
  331. $bestRoundPrice = array();
  332. if ($lastPrice == $prices[0]) {
  333. if ($quantileNumber == 1 && $offset) {
  334. $additionalPrices = $this->_pricesModel
  335. ->loadPreviousPrices($lastPrice, $quantileInterval[0], $this->_lowerLimit);
  336. if ($additionalPrices) {
  337. $quantileInterval[0] -= count($additionalPrices);
  338. $prices = array_merge($additionalPrices, $prices);
  339. $bestRoundPrice = $this->_findRoundPrice(
  340. $prices[0] + Mage_Catalog_Model_Resource_Layer_Filter_Price::MIN_POSSIBLE_PRICE / 10,
  341. $lastPrice,
  342. false
  343. );
  344. }
  345. }
  346. if ($quantileNumber == $this->getIntervalsNumber() - 1) {
  347. $pricesCount = count($prices);
  348. if ($prices[$pricesCount - 1] > $lastPrice) {
  349. $additionalPrices = array($prices[$pricesCount - 1]);
  350. } else {
  351. $additionalPrices = $this->_pricesModel->loadNextPrices(
  352. $lastPrice,
  353. $this->_count - $quantileInterval[0] - count($prices),
  354. $this->_upperLimit
  355. );
  356. }
  357. if ($additionalPrices) {
  358. $quantileInterval[1] = $quantileInterval[0] + count($prices) - 1;
  359. if ($prices[$pricesCount - 1] <= $lastPrice) {
  360. $quantileInterval[1] += count($additionalPrices);
  361. $prices = array_merge($prices, $additionalPrices);
  362. }
  363. $upperBestRoundPrice = $this->_findRoundPrice(
  364. $lastPrice + Mage_Catalog_Model_Resource_Layer_Filter_Price::MIN_POSSIBLE_PRICE / 10,
  365. $prices[count($prices) - 1],
  366. false
  367. );
  368. $this->_mergeRoundPrices($bestRoundPrice, $upperBestRoundPrice);
  369. }
  370. }
  371. } else {
  372. $bestRoundPrice = $this->_findRoundPrice(
  373. $prices[0] + Mage_Catalog_Model_Resource_Layer_Filter_Price::MIN_POSSIBLE_PRICE / 10,
  374. $lastPrice
  375. );
  376. }
  377. $this->_quantileInterval = $quantileInterval;
  378. $this->_prices = $prices;
  379. if (empty($bestRoundPrice)) {
  380. $this->_skippedQuantilesUpperLimits[$quantileNumber] = $quantileInterval[1];
  381. return $bestRoundPrice;
  382. }
  383. $pricesCount = count($prices);
  384. if ($prices[$pricesCount - 1] > $lastPrice) {
  385. $this->_lastPriceLimiter = array($quantileInterval[0] + $pricesCount - 1, $prices[$pricesCount - 1]);
  386. }
  387. ksort($bestRoundPrice, SORT_NUMERIC);
  388. foreach ($bestRoundPrice as $index => &$bestRoundPriceValues) {
  389. if (empty($bestRoundPriceValues)) {
  390. unset($bestRoundPrice[$index]);
  391. } else {
  392. sort($bestRoundPriceValues);
  393. }
  394. }
  395. return array_reverse($bestRoundPrice);
  396. }
  397. /**
  398. * Find max rounding factor with given price range
  399. *
  400. * @param float $lowerPrice
  401. * @param float $upperPrice
  402. * @param bool $returnEmpty whether empty result is acceptable
  403. * @param null|float $roundingFactor if given, checks for range to contain the factor
  404. * @return false|array
  405. */
  406. protected function _findRoundPrice($lowerPrice, $upperPrice, $returnEmpty = true, $roundingFactor = null)
  407. {
  408. $lowerPrice = round($lowerPrice, 3);
  409. $upperPrice = round($upperPrice, 3);
  410. if (!is_null($roundingFactor)) {
  411. // Can't separate if prices are equal
  412. if ($lowerPrice >= $upperPrice) {
  413. if ($lowerPrice > $upperPrice || $returnEmpty) {
  414. return false;
  415. }
  416. }
  417. // round is used for such examples: (1194.32 / 0.02) or (5 / 100000)
  418. $lowerDivision = ceil(round($lowerPrice / $roundingFactor, self::TEN_POWER_ROUNDING_FACTOR + 3));
  419. $upperDivision = floor(round($upperPrice / $roundingFactor, self::TEN_POWER_ROUNDING_FACTOR + 3));
  420. $result = array();
  421. if ($upperDivision <= 0 || $upperDivision - $lowerDivision > 10) {
  422. return $result;
  423. }
  424. for ($i = $lowerDivision; $i <= $upperDivision; ++$i) {
  425. $result[] = round($i * $roundingFactor, 2);
  426. }
  427. return $result;
  428. }
  429. $result = array();
  430. $tenPower = pow(10, self::TEN_POWER_ROUNDING_FACTOR);
  431. $roundingFactorCoefficients = array(10, 5, 2);
  432. while ($tenPower >= Mage_Catalog_Model_Resource_Layer_Filter_Price::MIN_POSSIBLE_PRICE) {
  433. if ($tenPower == Mage_Catalog_Model_Resource_Layer_Filter_Price::MIN_POSSIBLE_PRICE) {
  434. $roundingFactorCoefficients[] = 1;
  435. }
  436. foreach ($roundingFactorCoefficients as $roundingFactorCoefficient) {
  437. $roundingFactorCoefficient *= $tenPower;
  438. $roundPrices = $this->_findRoundPrice(
  439. $lowerPrice, $upperPrice, $returnEmpty, $roundingFactorCoefficient
  440. );
  441. if ($roundPrices) {
  442. $index = round($roundingFactorCoefficient
  443. / Mage_Catalog_Model_Resource_Layer_Filter_Price::MIN_POSSIBLE_PRICE);
  444. $result[$index] = $roundPrices;
  445. }
  446. }
  447. $tenPower /= 10;
  448. }
  449. return empty($result) ? array(1 => array()) : $result;
  450. }
  451. /**
  452. * Get separator nearest to quantile among the separators
  453. *
  454. * @param int $quantileNumber
  455. * @param array $separators
  456. * @return bool|array [deflection, separatorPrice, $priceIndex]
  457. */
  458. protected function _findBestSeparator($quantileNumber, $separators)
  459. {
  460. $result = false;
  461. $i = 0;
  462. $pricesCount = count($this->_prices);
  463. while ($i < $pricesCount && !empty($separators)) {
  464. $i = $this->_binarySearch($separators[0], array($i));
  465. if ($i == -1) {
  466. break;
  467. }
  468. $separator = array_shift($separators);
  469. $deflection = abs($quantileNumber * $this->_count
  470. - ($this->_quantileInterval[0] + $i) * $this->_getCalculatedIntervalsNumber());
  471. if (!$result || $deflection < $result[0]) {
  472. $result = array($deflection, $separator, $i);
  473. }
  474. }
  475. return $result ? $result : false;
  476. }
  477. /**
  478. * Calculate separators, each contains 'from', 'to' and 'count'
  479. *
  480. * @return array
  481. */
  482. public function calculateSeparators()
  483. {
  484. $result = array();
  485. $lastCount = 0;
  486. $intervalFirstPrice = $this->_minPrice;
  487. $lastSeparator = is_null($this->_lowerLimit) ? 0 : $this->_lowerLimit;
  488. for ($i = 1; $i < $this->getIntervalsNumber(); ++$i) {
  489. $separator = $this->_findPriceSeparator($i);
  490. if (empty($separator)) {
  491. continue;
  492. }
  493. if ($this->_quantileInterval[0] == 0) {
  494. $intervalFirstPrice = $this->_prices[0];
  495. }
  496. $separatorCandidate = false;
  497. $newIntervalFirstPrice = $intervalFirstPrice;
  498. $newLastSeparator = $lastSeparator;
  499. $pricesPerInterval = $this->_count / $this->_getCalculatedIntervalsNumber();
  500. while (!empty($separator) && !array_key_exists($i, $result)) {
  501. $separatorsPortion = array_shift($separator);
  502. $bestSeparator = $this->_findBestSeparator($i, $separatorsPortion);
  503. if ($bestSeparator && $bestSeparator[2] > 0) {
  504. $isEqualPrice = ($intervalFirstPrice == $this->_prices[$bestSeparator[2] - 1])
  505. ? $this->_prices[0]
  506. : false;
  507. $count = $bestSeparator[2] + $this->_quantileInterval[0] - $lastCount;
  508. $separatorData = array(
  509. 'from' => ($isEqualPrice !== false) ? $isEqualPrice : $lastSeparator,
  510. 'to' => ($isEqualPrice !== false) ? $isEqualPrice : $bestSeparator[1],
  511. 'count' => $count,
  512. );
  513. if (abs(1 - $count / $pricesPerInterval) <= self::INTERVAL_DEFLECTION_LIMIT) {
  514. $newLastSeparator = $bestSeparator[1];
  515. $newIntervalFirstPrice = $this->_prices[$bestSeparator[2]];
  516. $result[$i] = $separatorData;
  517. } elseif (!$separatorCandidate || $bestSeparator[0] < $separatorCandidate[0]) {
  518. $separatorCandidate = array(
  519. $bestSeparator[0],
  520. $separatorData,
  521. $bestSeparator[1],
  522. $this->_prices[$bestSeparator[2]]
  523. );
  524. }
  525. }
  526. }
  527. if (!array_key_exists($i, $result) && $separatorCandidate) {
  528. $newLastSeparator = $separatorCandidate[2];
  529. $newIntervalFirstPrice = $separatorCandidate[3];
  530. $result[$i] = $separatorCandidate[1];
  531. }
  532. if (array_key_exists($i, $result)) {
  533. $lastSeparator = $newLastSeparator;
  534. $intervalFirstPrice = $newIntervalFirstPrice;
  535. $priceIndex = $this->_binarySearch($lastSeparator);
  536. $lastCount += $result[$i]['count'];
  537. if ($priceIndex != -1 && $lastSeparator > $this->_lastPriceLimiter[1]) {
  538. $this->_lastPriceLimiter = array($priceIndex + $this->_quantileInterval[0], $lastSeparator);
  539. }
  540. }
  541. }
  542. if ($this->_lastPriceLimiter[0] < $this->_count) {
  543. $isEqualPrice = ($intervalFirstPrice == $this->_maxPrice) ? $intervalFirstPrice : false;
  544. $result[$this->getIntervalsNumber()] = array(
  545. 'from' => $isEqualPrice ? $isEqualPrice : $lastSeparator,
  546. 'to' => $isEqualPrice ? $isEqualPrice : (is_null($this->_upperLimit) ? '' : $this->_upperLimit),
  547. 'count' => $this->_count - $lastCount,
  548. );
  549. }
  550. return array_values($result);
  551. }
  552. }