fork download
  1. <?php
  2.  
  3. set_error_handler(function ($errno, $errstr, $errfile, $errline ) {
  4. throw new ErrorException($errstr, $errno, 0, $errfile, $errline);
  5. });
  6.  
  7. class Calculator
  8. {
  9. /**
  10.   * @param DiscountCollection $discountCollection
  11.   * @param ProductCollection $productCollection
  12.   * @return int|float
  13.   */
  14. public function calculateTotalPrice(
  15. DiscountCollection $discountCollection,
  16. ProductCollection $productCollection
  17. ) {
  18. $pricesSum = function ($p, $n) { return $p + $n->price; };
  19. $usedProducts = new ProductCollection();
  20. $totalPrice = 0;
  21.  
  22. foreach ($discountCollection as $discount) {
  23. $notUsedProducts = $productCollection->difference($usedProducts);
  24. $discountResult = $discount->getDiscountResult($notUsedProducts);
  25. $matchedProducts = $discountResult->getMatchedProducts();
  26. $matchedPrice = $matchedProducts->reduce($pricesSum, 0);
  27. $totalPrice += $matchedPrice - $matchedPrice * $discountResult->getPercent();
  28. $usedProducts = $usedProducts->merge($matchedProducts);
  29. }
  30.  
  31. $totalPrice += $productCollection->difference($usedProducts)->reduce($pricesSum, 0);
  32.  
  33. return $totalPrice;
  34. }
  35. }
  36.  
  37. interface DiscountInterface
  38. {
  39. /**
  40.   * @param ProductCollection $notUsedProducts
  41.   * @return DiscountResult
  42.   */
  43. public function getDiscountResult(ProductCollection $notUsedProducts);
  44. }
  45.  
  46. class CombinationDiscount implements DiscountInterface
  47. {
  48. private $names;
  49. private $percent;
  50.  
  51. public function __construct(array $names, $percent)
  52. {
  53. $this->names = $names;
  54. $this->percent = $percent;
  55. }
  56.  
  57. /**
  58.   * @param ProductCollection $notUsedProducts
  59.   * @return DiscountResult
  60.   */
  61. public function getDiscountResult(ProductCollection $notUsedProducts) {
  62. $productsUsedInThisDiscount = new ProductCollection();
  63.  
  64. foreach ($this->names as $name) {
  65. $matchedProduct = $notUsedProducts->getFirstByName($name);
  66. if ($matchedProduct) {
  67. $notUsedProducts->removeProduct($matchedProduct);
  68. $productsUsedInThisDiscount->addProduct($matchedProduct);
  69. } else {
  70. return new DiscountResult(new ProductCollection(), $this->percent);
  71. }
  72. }
  73.  
  74. return new DiscountResult($productsUsedInThisDiscount, $this->percent);
  75. }
  76. }
  77.  
  78. class CombinationOneOfDiscount implements DiscountInterface
  79. {
  80. private $oneOfNames;
  81. private $percent;
  82. private $productName;
  83.  
  84. public function __construct($productName, array $oneOfNames, $percent)
  85. {
  86. $this->oneOfNames = $oneOfNames;
  87. $this->percent = $percent;
  88. $this->productName = $productName;
  89. }
  90.  
  91. /**
  92.   * @param ProductCollection $notUsedProducts
  93.   * @return DiscountResult
  94.   */
  95. public function getDiscountResult(ProductCollection $notUsedProducts) {
  96. $product = $notUsedProducts->getFirstByName($this->productName);
  97. $firstByName = $notUsedProducts->getFirstByNames($this->oneOfNames);
  98.  
  99. if ($product && $firstByName) {
  100. return new DiscountResult(new ProductCollection([$product, $firstByName]), $this->percent);
  101. }
  102. return new DiscountResult(new ProductCollection(), 0);
  103. }
  104. }
  105.  
  106. class CountDiscount implements DiscountInterface
  107. {
  108. private $exceptNames;
  109. private $countPercentMap;
  110.  
  111. public function __construct(array $countPercentMap, array $exceptNames = [])
  112. {
  113. $this->countPercentMap = $countPercentMap;
  114. $this->exceptNames = $exceptNames;
  115. }
  116.  
  117. /**
  118.   * @param ProductCollection $notUsedProducts
  119.   * @return DiscountResult
  120.   */
  121. public function getDiscountResult(ProductCollection $notUsedProducts) {
  122. $matchedProducts = $notUsedProducts->getAllExceptNames($this->exceptNames);
  123. $countMatched = $matchedProducts->count();
  124.  
  125. if (array_key_exists($countMatched, $this->countPercentMap)) {
  126. return new DiscountResult($matchedProducts, $this->countPercentMap[$countMatched]);
  127. }
  128. $maxCount = max(array_keys($this->countPercentMap));
  129. if ($countMatched > $maxCount) {
  130. return new DiscountResult($matchedProducts, $this->countPercentMap[$maxCount]);
  131. }
  132. return new DiscountResult(new ProductCollection(), 0);
  133. }
  134. }
  135.  
  136. class DiscountCollection implements IteratorAggregate
  137. {
  138. /**
  139.   * @var DiscountInterface[]
  140.   */
  141. private $discounts;
  142.  
  143. public function __construct(array $discounts = [])
  144. {
  145. $this->discounts = [];
  146. foreach ($discounts as $discount) {
  147. $this->addDiscount($discount);
  148. }
  149. }
  150.  
  151. public function addDiscount(DiscountInterface $discount)
  152. {
  153. $this->discounts[] = $discount;
  154. }
  155.  
  156. /**
  157.   * @return ArrayIterator|DiscountInterface[]
  158.   */
  159. public function getIterator()
  160. {
  161. return new ArrayIterator($this->discounts);
  162. }
  163. }
  164.  
  165. class ProductCollection implements IteratorAggregate
  166. {
  167. private $products;
  168.  
  169. public function __construct(array $products = [])
  170. {
  171. $this->products = [];
  172. foreach ($products as $product) {
  173. $this->addProduct($product);
  174. }
  175. }
  176.  
  177. public function addProduct(Product $product)
  178. {
  179. $this->products[] = $product;
  180. }
  181.  
  182. /**
  183.   * @param Product $product
  184.   * @return Product|false
  185.   */
  186. public function removeProduct(Product $product)
  187. {
  188. for ($i = 0; $i < count($this->products); $i++) {
  189. if ($this->products[$i] === $product) {
  190. return array_splice($this->products, $i, 1)[0];
  191. }
  192. }
  193. return false;
  194. }
  195.  
  196. public function reduce(callable $callable, $initial)
  197. {
  198. return array_reduce($this->products, $callable, $initial);
  199. }
  200.  
  201. /**
  202.   * @param ProductCollection $productCollection
  203.   * @return ProductCollection
  204.   */
  205. public function merge(ProductCollection $productCollection)
  206. {
  207. return new self(array_merge($this->products, $productCollection->toArray()));
  208. }
  209.  
  210. /**
  211.   * @param ProductCollection $productCollection
  212.   * @return ProductCollection
  213.   */
  214. public function difference(ProductCollection $productCollection)
  215. {
  216. $pc = new self($this->products);
  217. foreach ($productCollection as $product) {
  218. $pc->removeProduct($product);
  219. }
  220. return $pc;
  221. }
  222.  
  223. /**
  224.   * @param $name
  225.   * @return Product|null
  226.   */
  227. public function getFirstByName($name)
  228. {
  229. foreach ($this->products as $product) {
  230. if ($product->name === $name) {
  231. return $product;
  232. }
  233. }
  234. return null;
  235. }
  236.  
  237. /**
  238.   * @param array $names
  239.   * @return Product|null
  240.   */
  241. public function getFirstByNames(array $names)
  242. {
  243. foreach ($names as $name) {
  244. $product = $this->getFirstByName($name);
  245. if ($product) {
  246. return $product;
  247. }
  248. }
  249. return null;
  250. }
  251.  
  252. /**
  253.   * @param array $names
  254.   * @return ProductCollection
  255.   */
  256. public function getAllExceptNames(array $names)
  257. {
  258. $allExceptNames = array_filter(
  259. $this->products,
  260. function (Product $p) use ($names) {
  261. return !in_array($p->name, $names, true);
  262. }
  263. );
  264.  
  265. return new self($allExceptNames);
  266. }
  267.  
  268. public function toArray()
  269. {
  270. return $this->products;
  271. }
  272.  
  273. /**
  274.   * @param Product $product
  275.   * @return bool
  276.   */
  277. public function containsProduct(Product $product)
  278. {
  279. return in_array($product, $this->products, true);
  280. }
  281.  
  282. /**
  283.   * @return bool
  284.   */
  285. public function isEmpty()
  286. {
  287. return count($this->products) === 0;
  288. }
  289.  
  290. /**
  291.   * @return int
  292.   */
  293. public function count()
  294. {
  295. return count($this->products);
  296. }
  297.  
  298. /**
  299.   * @return ArrayIterator|Product[]
  300.   */
  301. public function getIterator()
  302. {
  303. return new ArrayIterator($this->products);
  304. }
  305. }
  306.  
  307. class DiscountResult
  308. {
  309. /**
  310.   * @var ProductCollection
  311.   */
  312. private $matchedProducts;
  313. private $percent;
  314.  
  315. public function __construct(ProductCollection $matchedProducts, $percent)
  316. {
  317. $this->matchedProducts = $matchedProducts;
  318. $this->percent = $percent;
  319. }
  320.  
  321. /**
  322.   * @return ProductCollection
  323.   */
  324. public function getMatchedProducts()
  325. {
  326. return $this->matchedProducts;
  327. }
  328.  
  329. public function getPercent()
  330. {
  331. return $this->percent;
  332. }
  333. }
  334.  
  335. class Product
  336. {
  337. public $name;
  338. public $price;
  339.  
  340. public function __construct($name, $price)
  341. {
  342. $this->name = $name;
  343. $this->price = $price;
  344. }
  345. }
  346.  
  347. $a = new Product('a', 100);
  348. $a2 = new Product('a', 100);
  349. $b = new Product('b', 300);
  350. $c = new Product('c', 200);
  351. $d = new Product('d', 200);
  352. $e = new Product('e', 100);
  353. $c2 = new Product('c', 100);
  354.  
  355. // ProductCollection
  356. $pc = new ProductCollection([$a, $a2, $b, $c, $d, $e]);
  357. assert($pc->containsProduct($a));
  358. assert($pc->containsProduct(new Product('a', 1)) === false);
  359. assert($pc->getFirstByName('a') === $a);
  360. assert($pc->removeProduct(new Product('a', 1)) === false);
  361. assert($pc->removeProduct($a) === $a);
  362. assert($pc->containsProduct($a) === false);
  363. assert($pc->getFirstByName('a') === $a2);
  364. assert($pc->merge(new ProductCollection([$c2, $a])) == new ProductCollection([$a2, $b, $c, $d, $e, $c2, $a]));
  365. assert((new ProductCollection([$a, $b, $c]))->difference(new ProductCollection([$a, $c])) == new ProductCollection([$b]));
  366.  
  367. // CombinationDiscount
  368. $cd = new CombinationDiscount(['a', 'b'], 0.5);
  369. $allProducts = new ProductCollection([$a, $b, $c, $d, $e, $a2]);
  370. $notUsedProducts1 = $allProducts->difference(new ProductCollection([]));
  371. $notUsedProducts2 = $allProducts->difference(new ProductCollection([$a]));
  372. $notUsedProducts3 = $allProducts->difference(new ProductCollection([$a2, $a]));
  373. assert($cd->getDiscountResult($notUsedProducts1)->getMatchedProducts() == new ProductCollection([$a, $b]));
  374. assert($cd->getDiscountResult($notUsedProducts2)->getMatchedProducts() == new ProductCollection([$a2, $b]));
  375. assert($cd->getDiscountResult($notUsedProducts3)->getMatchedProducts()->isEmpty());
  376.  
  377. // Calculator with CombinationDiscount
  378. $c = new Calculator();
  379. assert(200 == $c->calculateTotalPrice(new DiscountCollection([$cd]), new ProductCollection([
  380. new Product('a', 100),
  381. new Product('a', 100),
  382. ])));
  383. assert(100 == $c->calculateTotalPrice(new DiscountCollection([$cd]), new ProductCollection([
  384. new Product('a', 100),
  385. new Product('b', 100),
  386. ])));
  387.  
  388. $discountCollection = new DiscountCollection([
  389. new CombinationDiscount(['a', 'b', 'c'], 0.2),
  390. new CombinationDiscount(['a', 'b', 'c', 'd'], 0.8),
  391. ]);
  392. $abcdCollection = new ProductCollection([
  393. new Product('a', 100),
  394. new Product('b', 100),
  395. new Product('c', 100),
  396. new Product('d', 100),
  397. ]);
  398. assert(100 + 300 - 300 * 0.2 == $c->calculateTotalPrice($discountCollection, $abcdCollection));
  399.  
  400. $discountCollection = new DiscountCollection([
  401. new CombinationDiscount(['a', 'b', 'c', 'd'], 0.8),
  402. new CombinationDiscount(['a', 'b', 'c'], 0.2),
  403. ]);
  404. assert(400 - 400 * 0.8 == $c->calculateTotalPrice($discountCollection, $abcdCollection));
  405.  
  406. // CountDiscount
  407. $cd = new CountDiscount([
  408. 1 => 0.3,
  409. 2 => 0.4,
  410. 3 => 0.5,
  411. ], ['a', 'b']);
  412.  
  413. assert(200 - 200 * 0.4 + 200 == $c->calculateTotalPrice(new DiscountCollection([$cd]), $abcdCollection));
  414. assert(200 == $c->calculateTotalPrice(new DiscountCollection([new CountDiscount([2 => 0.5])]), $abcdCollection));
  415.  
  416. // CombinationOneOfDiscount
  417. $cod = new CombinationOneOfDiscount('a', ['b', 'c', 'd'], 0.5);
  418. assert(200 + 100 == $c->calculateTotalPrice(new DiscountCollection([$cod]), $abcdCollection));
  419.  
  420. // Main test
  421. $discountCollection = new DiscountCollection([
  422. new CombinationDiscount(['a', 'b'], 0.1),
  423. new CombinationDiscount(['d', 'e'], 0.05),
  424. new CombinationDiscount(['f', 'e', 'g'], 0.05),
  425. new CombinationOneOfDiscount('a', ['k', 'j', 'm'], 0.05),
  426. new CountDiscount([3 => 0.05, 4 => 0.01, 5 => 0.02], ['a', 'c']),
  427. ]);
  428. $productCollection = new ProductCollection([
  429. new Product('a', 100),
  430. new Product('b', 100),
  431. new Product('c', 100),
  432. new Product('d', 100),
  433. new Product('e', 100),
  434. new Product('f', 100),
  435. new Product('g', 100),
  436. new Product('h', 100),
  437. new Product('i', 100),
  438. new Product('j', 100),
  439. new Product('a', 100),
  440. ]);
  441.  
  442. $answer = (200 - 200 * 0.1) + (200 - 200 * 0.05) + (200 - 200 * 0.05) + (400 - 400 * 0.01) + 100;
  443. assert($answer == $c->calculateTotalPrice($discountCollection, $productCollection));
Success #stdin #stdout 0s 52488KB
stdin
Standard input is empty
stdout
Standard output is empty