diff --git a/python/book-store/.exercism/metadata.json b/python/book-store/.exercism/metadata.json new file mode 100644 index 0000000..047f7ab --- /dev/null +++ b/python/book-store/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"python","exercise":"book-store","id":"93ec2b6783614c0383678eefca903467","url":"https://exercism.io/my/solutions/93ec2b6783614c0383678eefca903467","handle":"Xevion","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..d65d12d --- /dev/null +++ b/python/book-store/README.md @@ -0,0 +1,117 @@ +# 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 the appropriate command below ([why they are different](https://github.com/pytest-dev/pytest/issues/1629#issue-161422224)): + +- Python 2.7: `py.test book_store_test.py` +- Python 3.4+: `pytest book_store_test.py` + +Alternatively, you can tell Python to run the pytest module (allowing the same command to be used regardless of Python version): +`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..0fff14b --- /dev/null +++ b/python/book-store/book_store.py @@ -0,0 +1,14 @@ +from itertools import combinations_with_replacement +PRICES = {1: 8, 2: 16 * 0.95, 3: 24 * 0.90, 4: 32 * 0.80, 5: 40 * 0.75} + + +def total(books): + num_books = len(books) + unique = len(set(books)) + counts = [books.count(book) for book in books] + try: + combos = combinations_with_replacement(range(1, unique+1), max(counts)) + except ValueError: + return 0.00 + combos = (combo for combo in combos if sum(combo) == num_books) + return round(min(sum(PRICES[c] for c in combo) for combo in combos)*100) \ No newline at end of file diff --git a/python/book-store/book_store_test.py b/python/book-store/book_store_test.py new file mode 100644 index 0000000..b82cf16 --- /dev/null +++ b/python/book-store/book_store_test.py @@ -0,0 +1,60 @@ +import unittest + +from book_store import total + + +# Tests adapted from `problem-specifications//canonical-data.json` @ v1.4.0 + +class BookStoreTest(unittest.TestCase): + def test_only_a_single_book(self): + self.assertEqual(total([1]), 800) + + def test_two_of_the_same_book(self): + self.assertEqual(total([2, 2]), 1600) + + def test_empty_basket(self): + self.assertEqual(total([]), 0) + + def test_two_different_books(self): + self.assertEqual(total([1, 2]), 1520) + + def test_three_different_books(self): + self.assertEqual(total([1, 2, 3]), 2160) + + def test_four_different_books(self): + self.assertEqual(total([1, 2, 3, 4]), 2560) + + def test_five_different_books(self): + self.assertEqual(total([1, 2, 3, 4, 5]), 3000) + + def test_two_groups_of_4_is_cheaper_than_group_of_5_plus_group_of_3(self): + self.assertEqual(total([1, 1, 2, 2, 3, 3, 4, 5]), 5120) + + def test_two_groups_of_4_is_cheaper_than_groups_of_5_and_3(self): + self.assertEqual(total([1, 1, 2, 3, 4, 4, 5, 5]), 5120) + + def test_group_of_4_plus_group_of_2_is_cheaper_than_2_groups_of_3(self): + self.assertEqual(total([1, 1, 2, 2, 3, 4]), 4080) + + def test_two_each_of_first_4_books_and_1_copy_each_of_rest(self): + self.assertEqual(total([1, 1, 2, 2, 3, 3, 4, 4, 5]), 5560) + + def test_two_copies_of_each_book(self): + self.assertEqual(total([1, 1, 2, 2, 3, 3, 4, 4, 5, 5]), 6000) + + def test_three_copies_of_first_book_and_2_each_of_remaining(self): + self.assertEqual( + total([1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 1]), 6800) + + def test_three_each_of_first_2_books_and_2_each_of_remaining_books(self): + self.assertEqual( + total([1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 1, 2]), 7520) + + def test_four_groups_of_4_are_cheaper_than_2_groups_each_of_5_and_3(self): + self.assertEqual( + total([1, 1, 2, 2, 3, 3, 4, 5, 1, 1, 2, 2, 3, 3, 4, 5]), + 10240) + + +if __name__ == '__main__': + unittest.main() diff --git a/python/run-length-encoding/.exercism/metadata.json b/python/run-length-encoding/.exercism/metadata.json new file mode 100644 index 0000000..e657d48 --- /dev/null +++ b/python/run-length-encoding/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"python","exercise":"run-length-encoding","id":"d794356bb2f64e1998420f4ed754c3e0","url":"https://exercism.io/my/solutions/d794356bb2f64e1998420f4ed754c3e0","handle":"Xevion","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/python/run-length-encoding/README.md b/python/run-length-encoding/README.md new file mode 100644 index 0000000..e602f4c --- /dev/null +++ b/python/run-length-encoding/README.md @@ -0,0 +1,73 @@ +# Run Length Encoding + +Implement run-length encoding and decoding. + +Run-length encoding (RLE) is a simple form of data compression, where runs +(consecutive data elements) are replaced by just one data value and count. + +For example we can represent the original 53 characters with only 13. + +```text +"WWWWWWWWWWWWBWWWWWWWWWWWWBBBWWWWWWWWWWWWWWWWWWWWWWWWB" -> "12WB12W3B24WB" +``` + +RLE allows the original data to be perfectly reconstructed from +the compressed data, which makes it a lossless data compression. + +```text +"AABCCCDEEEE" -> "2AB3CD4E" -> "AABCCCDEEEE" +``` + +For simplicity, you can assume that the unencoded string will only contain +the letters A through Z (either lower or upper case) and whitespace. This way +data to be encoded will never contain any numbers and numbers inside data to +be decoded always represent the count for the following character. + +## 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 the appropriate command below ([why they are different](https://github.com/pytest-dev/pytest/issues/1629#issue-161422224)): + +- Python 2.7: `py.test run_length_encoding_test.py` +- Python 3.4+: `pytest run_length_encoding_test.py` + +Alternatively, you can tell Python to run the pytest module (allowing the same command to be used regardless of Python version): +`python -m pytest run_length_encoding_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/run-length-encoding` 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 + +Wikipedia [https://en.wikipedia.org/wiki/Run-length_encoding](https://en.wikipedia.org/wiki/Run-length_encoding) + +## 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/run-length-encoding/run_length_encoding.py b/python/run-length-encoding/run_length_encoding.py new file mode 100644 index 0000000..ec98670 --- /dev/null +++ b/python/run-length-encoding/run_length_encoding.py @@ -0,0 +1,7 @@ +import re + +def encode(string): + return ''.join(map(lambda item : '{}{}'.format(len(item.group(0)), item.group(0)[0]) if len(item.group(0)) > 1 else item.group(0), re.finditer(re.compile(r'(.)\1{0,}'), string))) + +def decode(string): + return ''.join([sub[-1] * int(sub[:-1]) if len(sub) >= 2 else sub for sub in re.findall(re.compile(r'(\d*.)'), string)]) \ No newline at end of file diff --git a/python/run-length-encoding/run_length_encoding_test.py b/python/run-length-encoding/run_length_encoding_test.py new file mode 100644 index 0000000..1122e5b --- /dev/null +++ b/python/run-length-encoding/run_length_encoding_test.py @@ -0,0 +1,54 @@ +import unittest + +from run_length_encoding import encode, decode + + +# Tests adapted from `problem-specifications//canonical-data.json` @ v1.1.0 + +class RunLengthEncodingTest(unittest.TestCase): + def test_encode_empty_string(self): + self.assertMultiLineEqual(encode(''), '') + + def test_encode_single_characters_only_are_encoded_without_count(self): + self.assertMultiLineEqual(encode('XYZ'), 'XYZ') + + def test_encode_string_with_no_single_characters(self): + self.assertMultiLineEqual(encode('AABBBCCCC'), '2A3B4C') + + def test_encode_single_characters_mixed_with_repeated_characters(self): + self.assertMultiLineEqual( + encode('WWWWWWWWWWWWBWWWWWWWWWWWWBBBWWWWWWWWWWWWWWWWWWWWWWWWB'), + '12WB12W3B24WB') + + def test_encode_multiple_whitespace_mixed_in_string(self): + self.assertMultiLineEqual(encode(' hsqq qww '), '2 hs2q q2w2 ') + + def test_encode_lowercase_characters(self): + self.assertMultiLineEqual(encode('aabbbcccc'), '2a3b4c') + + def test_decode_empty_string(self): + self.assertMultiLineEqual(decode(''), '') + + def test_decode_single_characters_only(self): + self.assertMultiLineEqual(decode('XYZ'), 'XYZ') + + def test_decode_string_with_no_single_characters(self): + self.assertMultiLineEqual(decode('2A3B4C'), 'AABBBCCCC') + + def test_decode_single_characters_with_repeated_characters(self): + self.assertMultiLineEqual( + decode('12WB12W3B24WB'), + 'WWWWWWWWWWWWBWWWWWWWWWWWWBBBWWWWWWWWWWWWWWWWWWWWWWWWB') + + def test_decode_multiple_whitespace_mixed_in_string(self): + self.assertMultiLineEqual(decode('2 hs2q q2w2 '), ' hsqq qww ') + + def test_decode_lower_case_string(self): + self.assertMultiLineEqual(decode('2a3b4c'), 'aabbbcccc') + + def test_combination(self): + self.assertMultiLineEqual(decode(encode('zzz ZZ zZ')), 'zzz ZZ zZ') + + +if __name__ == '__main__': + unittest.main()