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 $count;
  127. private $percent;
  128. private $withoutNames;
  129.  
  130. public function __construct($count, $percent, array $withoutNames = [])
  131. {
  132. $this->count = $count;
  133. $this->percent = $percent;
  134. $this->withoutNames = $withoutNames;
  135. }
  136.  
  137. public function match(ProductCollection $pc)
  138. {
  139. return $pc->getWithoutNames($this->withoutNames)->getCount() >= $this->count;
  140. }
  141.  
  142. public function getPercent()
  143. {
  144. return $this->percent;
  145. }
  146. }
  147.  
  148. interface CombinationDiscountInterface
  149. {
  150. public function getSumAfterApplyingDiscount(ProductCollection $productCollection);
  151. }
  152.  
  153. // Скидка, формирующаяся на основе комбинации определённых товаров
  154. class CombinationDiscount implements CombinationDiscountInterface
  155. {
  156. private $productNames;
  157. private $percent;
  158.  
  159. public function __construct(array $productNames, $percent)
  160. {
  161. $this->productNames = $productNames;
  162. $this->percent = $percent;
  163. }
  164.  
  165. public function getSumAfterApplyingDiscount(ProductCollection $pc)
  166. {
  167. if ($pc->hasNames($this->productNames)) {
  168. $deleted = $pc->deleteByNames($this->productNames);
  169. return $deleted->getSum() - $deleted->getSum() * $this->percent;
  170. }
  171. return 0;
  172. }
  173. }
  174.  
  175. // Если одновременно выбраны А и один из [K,L,M], то стоимость выбранного продукта...
  176. class CombinationOneOfDiscount implements CombinationDiscountInterface
  177. {
  178. private $productName;
  179. private $oneOf;
  180. private $percent;
  181.  
  182. public function __construct($productName, array $oneOf, $percent)
  183. {
  184. $this->productName = $productName;
  185. $this->oneOf = $oneOf;
  186. $this->percent = $percent;
  187. }
  188.  
  189. public function getSumAfterApplyingDiscount(ProductCollection $pc)
  190. {
  191. $oneOf = $pc->getOneOfName($this->oneOf);
  192. $product = $pc->getByName($this->productName);
  193. if ($oneOf && $pc->hasNames([$this->productName])) {
  194. $pc->deleteByNames([$oneOf->name, $product->name]);
  195. return $product->price + ($oneOf->price - $oneOf->price * $this->percent);
  196. }
  197. return 0;
  198. }
  199. }
  200.  
  201. class Calculator
  202. {
  203. private $combinationDiscounts;
  204. private $productCollection;
  205. private $countDiscounts;
  206.  
  207. public function __construct(ProductCollection $productCollection)
  208. {
  209. $this->productCollection = $productCollection;
  210. $this->combinationDiscounts = [];
  211. $this->countDiscounts = [];
  212. }
  213.  
  214. public function addCombinationDiscount(CombinationDiscountInterface $cd)
  215. {
  216. $this->combinationDiscounts[] = $cd;
  217. return $this;
  218. }
  219.  
  220. public function addCountDiscount(CountDiscount $cd)
  221. {
  222. $this->countDiscounts[] = $cd;
  223. return $this;
  224. }
  225.  
  226. public function calculateTotalPrice()
  227. {
  228. $totalPrice = 0;
  229. $discountFromOrderSum = $this->getDiscountFromOrderSum();
  230.  
  231. foreach ($this->combinationDiscounts as $combinationDiscount) {
  232. $totalPrice += $combinationDiscount->getSumAfterApplyingDiscount($this->productCollection);
  233. }
  234.  
  235. $totalPrice += $this->productCollection->getSum();
  236.  
  237. return $totalPrice - $discountFromOrderSum;
  238. }
  239.  
  240. private function getDiscountFromOrderSum()
  241. {
  242. foreach ($this->countDiscounts as $countDiscount) {
  243. if ($countDiscount->match($this->productCollection)) {
  244. return $this->productCollection->getSum() * $countDiscount->getPercent();
  245. }
  246. }
  247. return 0;
  248. }
  249. }
  250.  
  251. // ======================================================================
  252.  
  253. $pc = new ProductCollection([
  254. new Product('a', 100),
  255. new Product('b', 300),
  256. new Product('c', 200),
  257. new Product('d', 200),
  258. new Product('e', 100),
  259. new Product('c', 100),
  260. ]);
  261.  
  262. // Тесты класса ProductCollection
  263. assert($pc->getCount() === 6);
  264. assert($pc->hasNames(['a', 'c', 'e']) === true);
  265. assert($pc->hasNames(['a', 'Z']) === false);
  266. assert($pc->getOneOfName(['a', 'Z']) == true);
  267. assert($pc->getOneOfName(['Q', 'Z']) == false);
  268. assert($pc->getSum() === 1000);
  269. assert($pc->getWithoutNames(['a', 'b', 'e'])->getSum() === 500);
  270.  
  271. $c = new Calculator($pc);
  272. $c
  273. // Если пользователь выбрал одновременно 3 продукта, он получает скидку 5% от суммы заказа
  274. ->addCountDiscount(new CountDiscount(3, 0.05, ['a', 'c']))
  275. // Если пользователь выбрал одновременно 4 продукта, он получает скидку 10% от суммы заказа
  276. ->addCountDiscount(new CountDiscount(4, 0.1, ['a', 'c']))
  277. // Если пользователь выбрал одновременно 5 продуктов, он получает скидку 20% от суммы заказа
  278. ->addCountDiscount(new CountDiscount(5, 0.2, ['a', 'c']))
  279. ;
  280.  
  281. // Суммарно - 1000
  282. // сработал первый Discount, т.к. осталось 3 товара после того, как избавились от всех A,C
  283. // 1000 минус 5 процентов от 1000
  284. assert($c->calculateTotalPrice() == 950);
  285.  
  286. // ======================================================================
  287.  
  288. $pc2 = new ProductCollection([
  289. new Product('a', 100),
  290. new Product('a', 100),
  291. new Product('a', 100),
  292. new Product('b', 100),
  293. new Product('b', 100),
  294. ]);
  295.  
  296. $pc2->deleteByNames(['a', 'a', 'b']);
  297. assert($pc2->getCount() === 2);
  298.  
  299. // ======================================================================
  300.  
  301. $pc3 = new ProductCollection([
  302. new Product('a', 50),
  303. new Product('b', 50),
  304. new Product('e', 50),
  305. new Product('f', 25),
  306. new Product('g', 25),
  307. ]);
  308.  
  309. $c = new Calculator($pc3);
  310. $c
  311. // Если одновременно выбраны А и B, то их суммарная стоимость уменьшается на 10% (для каждой пары А и B)
  312. ->addCombinationDiscount(new CombinationDiscount(['a', 'b'], 0.1))
  313. // Если одновременно выбраны E,F,G, то их суммарная стоимость уменьшается на 5% (для каждой тройки E,F,G)
  314. ->addCombinationDiscount(new CombinationDiscount(['e', 'f', 'g'], 0.05))
  315. ;
  316.  
  317. assert($c->calculateTotalPrice() == 90 + 95);
  318.  
  319. // ======================================================================
  320.  
  321. $pc4 = new ProductCollection([
  322. new Product('a', 100),
  323. new Product('k', 100),
  324. new Product('p', 100),
  325. ]);
  326.  
  327. $c = new Calculator($pc4);
  328. $c
  329. // Если одновременно выбраны А и один из [K,L,M], то стоимость выбранного продукта уменьшается на 5%
  330. ->addCombinationDiscount(new CombinationOneOfDiscount('a', ['k', 'l', 'm'], 0.05))
  331. ;
  332.  
  333. assert($c->calculateTotalPrice() == 100 + 95 + 100);
  334.  
  335. // ======================================================================
  336.  
  337. $pc5 = new ProductCollection([
  338. new Product('a', 100), // 1 шаг
  339. new Product('b', 100), // 1 шаг
  340. new Product('c', 100), // 4 шаг
  341. new Product('d', 100), // 2 шаг
  342. new Product('e', 100), // 2 шаг
  343. new Product('f', 100), // 4 шаг
  344. new Product('g', 100), // 4 шаг
  345. new Product('h', 100), // 4 шаг
  346. new Product('i', 100), // 4 шаг
  347. new Product('j', 100), // 3 шаг
  348. new Product('a', 100), // 3 шаг
  349. ]);
  350.  
  351. $c = new Calculator($pc5);
  352. $c
  353. ->addCountDiscount(new CountDiscount(3, 0.05, ['a', 'c']))
  354. ->addCountDiscount(new CountDiscount(4, 0.1, ['a', 'c']))
  355. ->addCountDiscount(new CountDiscount(5, 0.2, ['a', 'c']))
  356. ->addCombinationDiscount(new CombinationDiscount(['a', 'b'], 0.1))
  357. ->addCombinationDiscount(new CombinationDiscount(['d', 'e'], 0.05))
  358. ->addCombinationDiscount(new CombinationDiscount(['f', 'e', 'g'], 0.05))
  359. ->addCombinationDiscount(new CombinationOneOfDiscount('a', ['k', 'j', 'm'], 0.05))
  360. ;
  361.  
  362. // За вычетом всех AC остаётся больше 5-и продуктов, от totalPrice нужно будет вычитать 20 процентов "суммы заказа"
  363. // Сработал Discount для AB, теперь к totalPrice вместо 100 + 100 нужно добавить 90 + 90 = 180 (шаг 1)
  364. // Сработал Discount для DE, к totalPrice вместо 200 нужно прибавить 95 + 95 = 190 (шаг 2)
  365. // Не сработал Discount для FEG, так как E уже использовался
  366. // CombinationOneOfDiscount сработал, так как есть j (шаг 3), выходит a + 5 процентов от j = 100 + 95
  367. // Складываем оставшиеся cfghi = 5 * 100 (шаг 4)
  368. // Считаем: 180 + 190 + 195 + 500 = 1065
  369. // 20 процентов от 1100 это 55
  370. assert($c->calculateTotalPrice() == 1065 - 55);
Success #stdin #stdout 0.01s 52488KB
stdin
Standard input is empty
stdout
Standard output is empty