diff --git a/python/book-store/.exercism/metadata.json b/python/book-store/.exercism/metadata.json new file mode 100644 index 0000000..2b67f8b --- /dev/null +++ b/python/book-store/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"python","exercise":"book-store","id":"cef3e9ecc90a4fcc9e92b878f280657a","url":"https://exercism.io/my/solutions/cef3e9ecc90a4fcc9e92b878f280657a","handle":"DmitryKokorin","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/python/book-store/README.md b/python/book-store/README.md new file mode 100644 index 0000000..a84050e --- /dev/null +++ b/python/book-store/README.md @@ -0,0 +1,115 @@ +# Book Store + +To try and encourage more sales of different books from a popular 5 book +series, a bookshop has decided to offer discounts on multiple book purchases. + +One copy of any of the five books costs $8. + +If, however, you buy two different books, you get a 5% +discount on those two books. + +If you buy 3 different books, you get a 10% discount. + +If you buy 4 different books, you get a 20% discount. + +If you buy all 5, you get a 25% discount. + +Note: that if you buy four books, of which 3 are +different titles, you get a 10% discount on the 3 that +form part of a set, but the fourth book still costs $8. + +Your mission is to write a piece of code to calculate the +price of any conceivable shopping basket (containing only +books of the same series), giving as big a discount as +possible. + +For example, how much does this basket of books cost? + +- 2 copies of the first book +- 2 copies of the second book +- 2 copies of the third book +- 1 copy of the fourth book +- 1 copy of the fifth book + +One way of grouping these 8 books is: + +- 1 group of 5 --> 25% discount (1st,2nd,3rd,4th,5th) +- +1 group of 3 --> 10% discount (1st,2nd,3rd) + +This would give a total of: + +- 5 books at a 25% discount +- +3 books at a 10% discount + +Resulting in: + +- 5 x (8 - 2.00) == 5 x 6.00 == $30.00 +- +3 x (8 - 0.80) == 3 x 7.20 == $21.60 + +For a total of $51.60 + +However, a different way to group these 8 books is: + +- 1 group of 4 books --> 20% discount (1st,2nd,3rd,4th) +- +1 group of 4 books --> 20% discount (1st,2nd,3rd,5th) + +This would give a total of: + +- 4 books at a 20% discount +- +4 books at a 20% discount + +Resulting in: + +- 4 x (8 - 1.60) == 4 x 6.40 == $25.60 +- +4 x (8 - 1.60) == 4 x 6.40 == $25.60 + +For a total of $51.20 + +And $51.20 is the price with the biggest discount. + + +## Exception messages + +Sometimes it is necessary to raise an exception. When you do this, you should include a meaningful error message to +indicate what the source of the error is. This makes your code more readable and helps significantly with debugging. Not +every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include +a message. + +To raise a message with an exception, just write it as an argument to the exception type. For example, instead of +`raise Exception`, you should write: + +```python +raise Exception("Meaningful message indicating the source of the error") +``` + +## Running the tests + +To run the tests, run `pytest book_store_test.py` + +Alternatively, you can tell Python to run the pytest module: +`python -m pytest book_store_test.py` + +### Common `pytest` options + +- `-v` : enable verbose output +- `-x` : stop running tests on first failure +- `--ff` : run failures from previous test before running other test cases + +For other options, see `python -m pytest -h` + +## Submitting Exercises + +Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/book-store` directory. + +You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`. + +For more detailed information about running tests, code style and linting, +please see [Running the Tests](http://exercism.io/tracks/python/tests). + +## Source + +Inspired by the harry potter kata from Cyber-Dojo. [http://cyber-dojo.org](http://cyber-dojo.org) + +## Submitting Incomplete Solutions + +It's possible to submit an incomplete solution so you can see how others have completed the exercise. diff --git a/python/book-store/book_store.py b/python/book-store/book_store.py new file mode 100644 index 0000000..6c47d02 --- /dev/null +++ b/python/book-store/book_store.py @@ -0,0 +1,33 @@ +from collections import Counter +from itertools import combinations + +DISCOUNTS = [0, 0, 5, 10, 20, 25] +BOOK_COST = 8 + + +def get_discount(counter): + + xs = sorted([i for i in counter if i != 0], reverse=True) + + if not xs: + return 0 + + discount = 0 + + for group_size in range(1, len(xs) + 1): + for xs1 in combinations(xs, len(xs)): + xs1 = [item - 1 if index < group_size else item + for index, item in enumerate(xs1)] + curr_discount = ( + DISCOUNTS[group_size]*group_size + get_discount(xs1) + ) + if curr_discount > discount: + discount = curr_discount + + return discount + + +def total(basket): + + return BOOK_COST*(len(basket)*100 + - get_discount(Counter(basket).values())) diff --git a/python/book-store/book_store_test.py b/python/book-store/book_store_test.py new file mode 100644 index 0000000..f7e0348 --- /dev/null +++ b/python/book-store/book_store_test.py @@ -0,0 +1,83 @@ +import unittest + +from book_store import total + +# Tests adapted from `problem-specifications//canonical-data.json` + + +class BookStoreTest(unittest.TestCase): + def test_only_a_single_book(self): + basket = [1] + self.assertEqual(total(basket), 800) + + def test_two_of_the_same_book(self): + basket = [2, 2] + self.assertEqual(total(basket), 1600) + + def test_empty_basket(self): + basket = [] + self.assertEqual(total(basket), 0) + + def test_two_different_books(self): + basket = [1, 2] + self.assertEqual(total(basket), 1520) + + def test_three_different_books(self): + basket = [1, 2, 3] + self.assertEqual(total(basket), 2160) + + def test_four_different_books(self): + basket = [1, 2, 3, 4] + self.assertEqual(total(basket), 2560) + + def test_five_different_books(self): + basket = [1, 2, 3, 4, 5] + self.assertEqual(total(basket), 3000) + + def test_two_groups_of_four_is_cheaper_than_group_of_five_plus_group_of_three(self): + basket = [1, 1, 2, 2, 3, 3, 4, 5] + self.assertEqual(total(basket), 5120) + + def test_two_groups_of_four_is_cheaper_than_groups_of_five_and_three(self): + basket = [1, 1, 2, 3, 4, 4, 5, 5] + self.assertEqual(total(basket), 5120) + + def test_group_of_four_plus_group_of_two_is_cheaper_than_two_groups_of_three(self): + basket = [1, 1, 2, 2, 3, 4] + self.assertEqual(total(basket), 4080) + + def test_two_each_of_first_4_books_and_1_copy_each_of_rest(self): + basket = [1, 1, 2, 2, 3, 3, 4, 4, 5] + self.assertEqual(total(basket), 5560) + + def test_two_copies_of_each_book(self): + basket = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5] + self.assertEqual(total(basket), 6000) + + def test_three_copies_of_first_book_and_2_each_of_remaining(self): + basket = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 1] + self.assertEqual(total(basket), 6800) + + def test_three_each_of_first_2_books_and_2_each_of_remaining_books(self): + basket = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 1, 2] + self.assertEqual(total(basket), 7520) + + def test_four_groups_of_four_are_cheaper_than_two_groups_each_of_five_and_three( + self + ): + basket = [1, 1, 2, 2, 3, 3, 4, 5, 1, 1, 2, 2, 3, 3, 4, 5] + self.assertEqual(total(basket), 10240) + + # Additional tests for this track + + def test_two_groups_of_four_and_a_group_of_five(self): + basket = [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5] + self.assertEqual(total(basket), 8120) + + def test_shuffled_book_order(self): + basket = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3] + self.assertEqual(total(basket), 8120) + + +if __name__ == "__main__": + unittest.main()