fork download
  1. <?php
  2.  
  3. /*
  4. Есть продукты A, B, C, D, E, F, G, H, I, J, K, L, M. Каждый продукт стоит определенную сумму.
  5. Есть набор правил расчета итоговой суммы:
  6. Если одновременно выбраны А и B, то их суммарная стоимость уменьшается на 10% (для каждой пары А и B)
  7. Если одновременно выбраны D и E, то их суммарная стоимость уменьшается на 5% (для каждой пары D и E)
  8. Если одновременно выбраны E,F,G, то их суммарная стоимость уменьшается на 5% (для каждой тройки E,F,G)
  9. Если одновременно выбраны А и один из [K,L,M], то стоимость выбранного продукта уменьшается на 5%
  10. Если пользователь выбрал одновременно 3 продукта, он получает скидку 5% от суммы заказа
  11. Если пользователь выбрал одновременно 4 продукта, он получает скидку 10% от суммы заказа
  12. Если пользователь выбрал одновременно 5 продуктов, он получает скидку 20% от суммы заказа
  13. Описанные скидки 5,6,7 не суммируются, применяется только одна из них
  14. Продукты A и C не участвуют в скидках 5,6,7
  15. Каждый товар может участвовать только в одной скидке. Скидки применяются последовательно в порядке описанном выше.
  16.  
  17. Необходимо написать программу на PHP с использованием ООП которая имея на входе набор продуктов (один продукт может встречаться несколько раз) рассчитывала суммарную их стоимость.
  18. */
  19.  
  20. class ProductCollection
  21. {
  22. private $products;
  23.  
  24. public function __construct(array $products = [])
  25. {
  26. foreach ($products as $product) {
  27. $this->addProduct($product);
  28. }
  29. }
  30.  
  31. public function addProduct(Product $product)
  32. {
  33. $this->products[] = $product;
  34. }
  35.  
  36. public function getByName($name)
  37. {
  38. foreach ($this->products as $product) {
  39. if ($product->name === $name) {
  40. return $product;
  41. }
  42. }
  43. return null;
  44. }
  45.  
  46. public function getCount()
  47. {
  48. return count($this->products);
  49. }
  50.  
  51. public function getWithoutNames(array $withoutNames)
  52. {
  53. $productsWithoutNames = array_filter(
  54. $this->products,
  55. function (Product $p) use ($withoutNames) {
  56. return !in_array($p->name, $withoutNames);
  57. }
  58. );
  59.  
  60. return new self($productsWithoutNames);
  61. }
  62.  
  63. public function getSum()
  64. {
  65. return array_reduce(
  66. $this->products,
  67. function ($prev, $next) { return $prev + $next->price; },
  68. 0
  69. );
  70. }
  71.  
  72. public function hasNames(array $names)
  73. {
  74. $productsNames = array_map(function ($p) { return $p->name; }, $this->products);
  75. return !array_diff($names, $productsNames);
  76. }
  77.  
  78. public function getOneOfName(array $names)
  79. {
  80. foreach ($this->products as $product) {
  81. if (in_array($product->name, $names)) {
  82. return $product;
  83. }
  84. }
  85. return null;
  86. }
  87.  
  88. public function deleteByNames(array $names)
  89. {
  90. $deleted = [];
  91.  
  92. foreach ($names as $name) {
  93. $deleted[] = $this->deleteByName($name);
  94. }
  95.  
  96. return new self($deleted);
  97. }
  98.  
  99. public function deleteByName($name)
  100. {
  101. $productCount = count($this->products);
  102. for ($i = 0; $i < $productCount; $i++) {
  103. if ($this->products[$i]->name === $name) {
  104. return array_splice($this->products, $i, 1)[0];
  105. }
  106. }
  107. return null;
  108. }
  109. }
  110.  
  111. class Product
  112. {
  113. public $name;
  114. public $price;
  115.  
  116. public function __construct($name, $price)
  117. {
  118. $this->name = $name;
  119. $this->price = $price;
  120. }
  121. }
  122.  
  123. // Скидка, формирующаяся на основе количества элементов в списке товаров
  124. class CountDiscount
  125. {
  126. private $criteria;
  127. private $percent;
  128. private $withoutNames;
  129.  
  130. public function __construct(CountCriteria $criteria, $percent, array $withoutNames = [])
  131. {
  132. $this->criteria = $criteria;
  133. $this->percent = $percent;
  134. $this->withoutNames = $withoutNames;
  135. }
  136.  
  137. public function match(ProductCollection $pc)
  138. {
  139. $countWithoutNames = $pc->getWithoutNames($this->withoutNames)->getCount();
  140. return $this->criteria->check($countWithoutNames);
  141. }
  142.  
  143. public function getPercent()
  144. {
  145. return $this->percent;
  146. }
  147. }
  148.  
  149. interface CombinationDiscountInterface
  150. {
  151. public function getSumAfterApplyingDiscount(ProductCollection $productCollection);
  152. }
  153.  
  154. // Скидка, формирующаяся на основе комбинации определённых товаров
  155. class CombinationDiscount implements CombinationDiscountInterface
  156. {
  157. private $productNames;
  158. private $percent;
  159.  
  160. public function __construct(array $productNames, $percent)
  161. {
  162. $this->productNames = $productNames;
  163. $this->percent = $percent;
  164. }
  165.  
  166. public function getSumAfterApplyingDiscount(ProductCollection $pc)
  167. {
  168. if ($pc->hasNames($this->productNames)) {
  169. $deleted = $pc->deleteByNames($this->productNames);
  170. return $deleted->getSum() - $deleted->getSum() * $this->percent;
  171. }
  172. return 0;
  173. }
  174. }
  175.  
  176. // Если одновременно выбраны А и один из [K,L,M], то стоимость выбранного продукта...
  177. class CombinationOneOfDiscount implements CombinationDiscountInterface
  178. {
  179. private $productName;
  180. private $oneOf;
  181. private $percent;
  182.  
  183. public function __construct($productName, array $oneOf, $percent)
  184. {
  185. $this->productName = $productName;
  186. $this->oneOf = $oneOf;
  187. $this->percent = $percent;
  188. }
  189.  
  190. public function getSumAfterApplyingDiscount(ProductCollection $pc)
  191. {
  192. $oneOf = $pc->getOneOfName($this->oneOf);
  193. $product = $pc->getByName($this->productName);
  194. if ($oneOf && $pc->hasNames([$this->productName])) {
  195. $pc->deleteByNames([$oneOf->name, $product->name]);
  196. return $product->price + ($oneOf->price - $oneOf->price * $this->percent);
  197. }
  198. return 0;
  199. }
  200. }
  201.  
  202. class Calculator
  203. {
  204. private $combinationDiscounts;
  205. private $productCollection;
  206. private $countDiscounts;
  207.  
  208. public function __construct(ProductCollection $productCollection)
  209. {
  210. $this->productCollection = $productCollection;
  211. $this->combinationDiscounts = [];
  212. $this->countDiscounts = [];
  213. }
  214.  
  215. public function addCombinationDiscount(CombinationDiscountInterface $cd)
  216. {
  217. $this->combinationDiscounts[] = $cd;
  218. return $this;
  219. }
  220.  
  221. public function addCountDiscount(CountDiscount $cd)
  222. {
  223. $this->countDiscounts[] = $cd;
  224. return $this;
  225. }
  226.  
  227. public function calculateTotalPrice()
  228. {
  229. $totalPrice = 0;
  230. $discountFromOrderSum = $this->getDiscountFromOrderSum();
  231.  
  232. foreach ($this->combinationDiscounts as $combinationDiscount) {
  233. $totalPrice += $combinationDiscount->getSumAfterApplyingDiscount($this->productCollection);
  234. }
  235.  
  236. $totalPrice += $this->productCollection->getSum();
  237.  
  238. return $totalPrice - $discountFromOrderSum;
  239. }
  240.  
  241. private function getDiscountFromOrderSum()
  242. {
  243. foreach ($this->countDiscounts as $countDiscount) {
  244. if ($countDiscount->match($this->productCollection)) {
  245. return $this->productCollection->getSum() * $countDiscount->getPercent();
  246. }
  247. }
  248.  
  249. return 0;
  250. }
  251. }
  252.  
  253. abstract class CountCriteria
  254. {
  255. protected $count;
  256.  
  257. public function __construct($count)
  258. {
  259. $this->count = $count;
  260. }
  261.  
  262. abstract public function check($givenCount);
  263. }
  264.  
  265. class EqualsCriteria extends CountCriteria
  266. {
  267. public function check($givenCount)
  268. {
  269. return $givenCount === $this->count;
  270. }
  271. }
  272.  
  273. class GreaterOrEqualsCriteria extends CountCriteria
  274. {
  275. public function check($givenCount)
  276. {
  277. return $givenCount >= $this->count;
  278. }
  279. }
  280. // ======================================================================
  281.  
  282. $pc = new ProductCollection([
  283. new Product('a', 100),
  284. new Product('b', 300),
  285. new Product('c', 200),
  286. new Product('d', 200),
  287. new Product('e', 100),
  288. new Product('c', 100),
  289. ]);
  290.  
  291. // Тесты класса ProductCollection
  292. assert($pc->getCount() === 6);
  293. assert($pc->hasNames(['a', 'c', 'e']) === true);
  294. assert($pc->hasNames(['a', 'Z']) === false);
  295. assert($pc->getOneOfName(['a', 'Z']) == true);
  296. assert($pc->getOneOfName(['Q', 'Z']) == false);
  297. assert($pc->getSum() === 1000);
  298. assert($pc->getWithoutNames(['a', 'b', 'e'])->getSum() === 500);
  299.  
  300. $c = new Calculator($pc);
  301. $c
  302. // Если пользователь выбрал одновременно 3 продукта, он получает скидку 5% от суммы заказа
  303. ->addCountDiscount(new CountDiscount(new EqualsCriteria(3), 0.05, ['a', 'c']))
  304. // Если пользователь выбрал одновременно 4 продукта, он получает скидку 10% от суммы заказа
  305. ->addCountDiscount(new CountDiscount(new EqualsCriteria(4), 0.1, ['a', 'c']))
  306. // Если пользователь выбрал одновременно 5 продуктов, он получает скидку 20% от суммы заказа
  307. ->addCountDiscount(new CountDiscount(new GreaterOrEqualsCriteria(5), 0.2, ['a', 'c']))
  308. ;
  309.  
  310. // Суммарно - 1000
  311. // сработал первый Discount, т.к. осталось 3 товара после того, как избавились от всех A,C
  312. // 1000 минус 5 процентов от 1000
  313. assert($c->calculateTotalPrice() == 950);
  314.  
  315.  
  316. // ======================================================================
  317.  
  318. $pc2 = new ProductCollection([
  319. new Product('a', 100),
  320. new Product('a', 100),
  321. new Product('a', 100),
  322. new Product('b', 100),
  323. new Product('b', 100),
  324. ]);
  325.  
  326. $pc2->deleteByNames(['a', 'a', 'b']);
  327. assert($pc2->getCount() === 2);
  328.  
  329. // ======================================================================
  330.  
  331. $pc3 = new ProductCollection([
  332. new Product('a', 50),
  333. new Product('b', 50),
  334. new Product('e', 50),
  335. new Product('f', 25),
  336. new Product('g', 25),
  337. ]);
  338.  
  339. $c = new Calculator($pc3);
  340. $c
  341. // Если одновременно выбраны А и B, то их суммарная стоимость уменьшается на 10% (для каждой пары А и B)
  342. ->addCombinationDiscount(new CombinationDiscount(['a', 'b'], 0.1))
  343. // Если одновременно выбраны E,F,G, то их суммарная стоимость уменьшается на 5% (для каждой тройки E,F,G)
  344. ->addCombinationDiscount(new CombinationDiscount(['e', 'f', 'g'], 0.05))
  345. ;
  346.  
  347. assert($c->calculateTotalPrice() == 90 + 95);
  348.  
  349. // ======================================================================
  350.  
  351. $pc4 = new ProductCollection([
  352. new Product('a', 100),
  353. new Product('k', 100),
  354. new Product('p', 100),
  355. ]);
  356.  
  357. $c = new Calculator($pc4);
  358. $c
  359. // Если одновременно выбраны А и один из [K,L,M], то стоимость выбранного продукта уменьшается на 5%
  360. ->addCombinationDiscount(new CombinationOneOfDiscount('a', ['k', 'l', 'm'], 0.05))
  361. ;
  362.  
  363. assert($c->calculateTotalPrice() == 100 + 95 + 100);
  364.  
  365.  
  366.  
  367. // ======================================================================
  368.  
  369. $pc5 = new ProductCollection([
  370. new Product('a', 100), // 1 шаг
  371. new Product('b', 100), // 1 шаг
  372. new Product('c', 100), // 4 шаг
  373. new Product('d', 100), // 2 шаг
  374. new Product('e', 100), // 2 шаг
  375. new Product('f', 100), // 4 шаг
  376. new Product('g', 100), // 4 шаг
  377. new Product('h', 100), // 4 шаг
  378. new Product('i', 100), // 4 шаг
  379. new Product('j', 100), // 3 шаг
  380. new Product('a', 100), // 3 шаг
  381. ]);
  382.  
  383. $c = new Calculator($pc5);
  384. $c
  385. ->addCountDiscount(new CountDiscount(new EqualsCriteria(3), 0.05, ['a', 'c']))
  386. ->addCountDiscount(new CountDiscount(new EqualsCriteria(4), 0.1, ['a', 'c']))
  387. ->addCountDiscount(new CountDiscount(new GreaterOrEqualsCriteria(5), 0.2, ['a', 'c']))
  388. ->addCombinationDiscount(new CombinationDiscount(['a', 'b'], 0.1))
  389. ->addCombinationDiscount(new CombinationDiscount(['d', 'e'], 0.05))
  390. ->addCombinationDiscount(new CombinationDiscount(['f', 'e', 'g'], 0.05))
  391. ->addCombinationDiscount(new CombinationOneOfDiscount('a', ['k', 'j', 'm'], 0.05))
  392. ;
  393.  
  394. // За вычетом всех AC остаётся больше 5-и продуктов, от totalPrice нужно будет вычитать 20 процентов "суммы заказа"
  395. // Сработал Discount для AB, теперь к totalPrice вместо 100 + 100 нужно добавить 90 + 90 = 180 (шаг 1)
  396. // Сработал Discount для DE, к totalPrice вместо 200 нужно прибавить 95 + 95 = 190 (шаг 2)
  397. // Не сработал Discount для FEG, так как E уже использовался
  398. // CombinationOneOfDiscount сработал, так как есть j (шаг 3), выходит a + 5 процентов от j = 100 + 95
  399. // Складываем оставшиеся cfghi = 5 * 100 (шаг 4)
  400. // Считаем: 180 + 190 + 195 + 500 = 1065
  401. // 20 процентов от 1100 это 220
  402. assert($c->calculateTotalPrice() == 1065 - 220);
Success #stdin #stdout 0.01s 52488KB
stdin
Standard input is empty
stdout
Standard output is empty