From 28ef376ab76dc820d912218c2cd11956c2d7cb0a Mon Sep 17 00:00:00 2001 From: Euler Campos Barros Date: Sat, 14 Oct 2017 17:02:53 -0300 Subject: [PATCH 1/5] isbn-verifier: implement exercise --- config.json | 20 ++++++++ exercises/isbn-verifier/README.md | 43 ++++++++++++++++ exercises/isbn-verifier/example.py | 46 +++++++++++++++++ exercises/isbn-verifier/isbn_verifier.py | 2 + exercises/isbn-verifier/isbn_verifier_test.py | 51 +++++++++++++++++++ 5 files changed, 162 insertions(+) create mode 100644 exercises/isbn-verifier/README.md create mode 100644 exercises/isbn-verifier/example.py create mode 100644 exercises/isbn-verifier/isbn_verifier.py create mode 100644 exercises/isbn-verifier/isbn_verifier_test.py diff --git a/config.json b/config.json index 5bd78add790..aecd62df2f5 100644 --- a/config.json +++ b/config.json @@ -1112,7 +1112,27 @@ "unlocked_by": null, "difficulty": 3, "topics": [ + "type_conversion", "exception_handling" + "strings", + "arrays", + "integers", + "parsing" + ] + }, + { + "uuid": "7961c852-c87a-44b0-b152-efea3ac8555c", + "slug": "isbn-verifier", + "core": false, + "unlocked_by": null, + "difficulty": 3, + "topics": [ + "type_conversion", + "conditionals", + "strings", + "arrays", + "integers", + "parsing" ] }, { diff --git a/exercises/isbn-verifier/README.md b/exercises/isbn-verifier/README.md new file mode 100644 index 00000000000..4da3a15ffde --- /dev/null +++ b/exercises/isbn-verifier/README.md @@ -0,0 +1,43 @@ +# Isbn Verifier + +Check if a given ISBN-10 is valid. + +## Functionality + +Given an unkown string the program should check if the provided string is a valid ISBN-10. +Putting this into place requires some thinking about preprocessing/parsing of the string prior to calculating the check digit for the ISBN. + +The program should allow for ISBN-10 without the separating dashes to be verified as well. + +## ISBN + +Let's take a random ISBN-10 number, say `3-598-21508-8` for this. +The first digit block indicates the group where the ISBN belongs. Groups can consist of shared languages, geographic regions or countries. The leading '3' signals this ISBN is from a german speaking country. +The following number block is to identify the publisher. Since this is a three digit publisher number there is a 5 digit title number for this book. +The last digit in the ISBN is the check digit which is used to detect read errors. + +The first 9 digits in the ISBN have to be between 0 and 9. +The check digit can additionally be an 'X' to allow 10 to be a valid check digit as well. + +A valid ISBN-10 is calculated with this formula `(x1 * 10 + x2 * 9 + x3 * 8 + x4 * 7 + x5 * 6 + x6 * 5 + x7 * 4 + x8 * 3 + x9 * 2 + x10 * 1) mod 11 == 0` +So for our example ISBN this means: +(3 * 10 + 5 * 9 + 9 * 8 + 8 * 7 + 2 * 6 + 1 * 5 + 5 * 4 + 0 * 3 + 8 * 2 + 8 * 1) mod 11 = 0 + +Which proves that the ISBN is valid. + +### Submitting Exercises + +Note that, when trying to submit an exercise, make sure the solution is in the `exercism/python/` directory. + +For example, if you're submitting `bob.py` for the Bob exercise, the submit command would be something like `exercism submit /python/bob/bob.py`. + + +For more detailed information about running tests, code style and linting, +please see the [help page](http://exercism.io/languages/python). + +## Source + +Converting a string into a number and some basic processing utilizing a relatable real world example. [https://en.wikipedia.org/wiki/International_Standard_Book_Number#ISBN-10_check_digit_calculation](https://en.wikipedia.org/wiki/International_Standard_Book_Number) + +## Submitting Incomplete Solutions +It's possible to submit an incomplete solution so you can see how others have completed the exercise. diff --git a/exercises/isbn-verifier/example.py b/exercises/isbn-verifier/example.py new file mode 100644 index 00000000000..153de2436e6 --- /dev/null +++ b/exercises/isbn-verifier/example.py @@ -0,0 +1,46 @@ +def verify(isbn): + clear_isbn = remove_non_alphanumeric(isbn) + if len(clear_isbn) != 10: + return False + + isbn_main_part = get_isbn_main_part(clear_isbn) + if not isbn_main_part.isdigit(): + return False + + check_digit = get_isbn_check_digit(clear_isbn) + if not check_digit.isdigit() and check_digit.upper() != 'X': + return False + + if calculate_isbn_check_digit(isbn_main_part) != check_digit: + return False + + return True + + +def remove_non_alphanumeric(value): + return ''.join([x for x in str(value) if x.isalnum()]) + + +def get_isbn_main_part(isbn_value): + return isbn_value[:-1] + + +def get_isbn_check_digit(isbn_value): + return isbn_value[-1:] + + +def calculate_isbn_check_digit(isbn): + check_digit = 11 - (int(isbn[0:1]) * 10 + + int(isbn[1:2]) * 9 + + int(isbn[2:3]) * 8 + + int(isbn[3:4]) * 7 + + int(isbn[4:5]) * 6 + + int(isbn[5:6]) * 5 + + int(isbn[6:7]) * 4 + + int(isbn[7:8]) * 3 + + int(isbn[8:9]) * 2) % 11 + + if check_digit == 10: + return 'X' + + return str(check_digit) diff --git a/exercises/isbn-verifier/isbn_verifier.py b/exercises/isbn-verifier/isbn_verifier.py new file mode 100644 index 00000000000..b00fcc6a45e --- /dev/null +++ b/exercises/isbn-verifier/isbn_verifier.py @@ -0,0 +1,2 @@ +def verify(isbn): + pass diff --git a/exercises/isbn-verifier/isbn_verifier_test.py b/exercises/isbn-verifier/isbn_verifier_test.py new file mode 100644 index 00000000000..38c6c40c5b6 --- /dev/null +++ b/exercises/isbn-verifier/isbn_verifier_test.py @@ -0,0 +1,51 @@ +import unittest + +from isbn_verifier import verify + + +# Tests adapted from `problem-specifications//canonical-data.json` @ v1.0.0 + +class IsbnVerifierTests(unittest.TestCase): + + def test_valid_isbn(self): + self.assertIs(verify('3-598-21508-8'), True) + + def test_invalid_check_digit(self): + self.assertIs(verify('3-598-21508-9'), False) + + def test_valid_with_X_check_digit(self): + self.assertIs(verify('3-598-21507-X'), True) + + def test_invalid_check_digit_other_than_X(self): + self.assertIs(verify('3-598-21507-A'), False) + + def test_invalid_character_in_isbn(self): + self.assertIs(verify('3-598-2K507-0'), False) + + def test_invalid_X_other_than_check_digit(self): + self.assertIs(verify('3-598-2X507-0'), False) + + def test_valid_isbn_without_separating_dashes(self): + self.assertIs(verify('3598215088'), True) + + def test_valid_isbn_without_separating_dashes_with_X_check_digit(self): + self.assertIs(verify('359821507X'), True) + + def test_invalid_isbn_without_check_digit_and_dashes(self): + self.assertIs(verify('359821507'), False) + + def test_invalid_too_long_isbn_with_no_dashes(self): + self.assertIs(verify('3598215078X'), False) + + def test_invalid_isbn_without_check_digit(self): + self.assertIs(verify('3-598-21507'), False) + + def test_invalid_too_long_isbn(self): + self.assertIs(verify('3-598-21507-XA'), False) + + def test_invalid_check_digit_X_used_for_0(self): + self.assertIs(verify('3-598-21515-X'), False) + + +if __name__ == '__main__': + unittest.main() From 59eb6b01808b6cfa556db2b28d12894461fee162 Mon Sep 17 00:00:00 2001 From: Euler Campos Barros Date: Thu, 19 Oct 2017 07:46:57 -0200 Subject: [PATCH 2/5] isbn-verifier: rebalance of difficulty --- config.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/config.json b/config.json index aecd62df2f5..2b2b38c681a 100644 --- a/config.json +++ b/config.json @@ -69,6 +69,21 @@ "transforming" ] }, + { + "uuid": "7961c852-c87a-44b0-b152-efea3ac8555c", + "slug": "isbn-verifier", + "core": false, + "unlocked_by": null, + "difficulty": 1, + "topics": [ + "type_conversion", + "conditionals", + "strings", + "arrays", + "integers", + "parsing" + ] + }, { "uuid": "8648fa0c-d85f-471b-a3ae-0f8c05222c89", "slug": "hamming", From f7dfd6703bf7b28d906acf244d2a72a2dd05739f Mon Sep 17 00:00:00 2001 From: Euler Campos Barros Date: Thu, 19 Oct 2017 08:31:42 -0200 Subject: [PATCH 3/5] isbn-verifier: revision of example --- exercises/isbn-verifier/example.py | 37 ++++++++---------------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/exercises/isbn-verifier/example.py b/exercises/isbn-verifier/example.py index 153de2436e6..b8d81866433 100644 --- a/exercises/isbn-verifier/example.py +++ b/exercises/isbn-verifier/example.py @@ -1,46 +1,27 @@ def verify(isbn): - clear_isbn = remove_non_alphanumeric(isbn) + clear_isbn = _remove_non_alphanumeric(isbn) if len(clear_isbn) != 10: return False - isbn_main_part = get_isbn_main_part(clear_isbn) + isbn_main_part = _get_isbn_main_part(clear_isbn) if not isbn_main_part.isdigit(): return False - check_digit = get_isbn_check_digit(clear_isbn) - if not check_digit.isdigit() and check_digit.upper() != 'X': - return False - - if calculate_isbn_check_digit(isbn_main_part) != check_digit: - return False + return _calculate_isbn_check_digit(isbn_main_part) == _get_isbn_check_digit(clear_isbn).upper() - return True - -def remove_non_alphanumeric(value): +def _remove_non_alphanumeric(value): return ''.join([x for x in str(value) if x.isalnum()]) -def get_isbn_main_part(isbn_value): +def _get_isbn_main_part(isbn_value): return isbn_value[:-1] -def get_isbn_check_digit(isbn_value): +def _get_isbn_check_digit(isbn_value): return isbn_value[-1:] -def calculate_isbn_check_digit(isbn): - check_digit = 11 - (int(isbn[0:1]) * 10 + - int(isbn[1:2]) * 9 + - int(isbn[2:3]) * 8 + - int(isbn[3:4]) * 7 + - int(isbn[4:5]) * 6 + - int(isbn[5:6]) * 5 + - int(isbn[6:7]) * 4 + - int(isbn[7:8]) * 3 + - int(isbn[8:9]) * 2) % 11 - - if check_digit == 10: - return 'X' - - return str(check_digit) +def _calculate_isbn_check_digit(isbn): + isbn_sum = 11 - sum([int(x) * (10 - i) for i, x in enumerate(isbn)]) % 11 + return 'X' if isbn_sum == 10 else str(isbn_sum) From 26b83cb4a94e3c57d8fc8798f14a71a850293d18 Mon Sep 17 00:00:00 2001 From: Euler Campos Barros Date: Thu, 19 Oct 2017 09:19:52 -0200 Subject: [PATCH 4/5] isbn-verifier: isbn generator --- exercises/isbn-verifier/example.py | 13 +++++++++++ exercises/isbn-verifier/isbn_verifier.py | 4 ++++ exercises/isbn-verifier/isbn_verifier_test.py | 23 ++++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/exercises/isbn-verifier/example.py b/exercises/isbn-verifier/example.py index b8d81866433..dc1bda00437 100644 --- a/exercises/isbn-verifier/example.py +++ b/exercises/isbn-verifier/example.py @@ -10,6 +10,15 @@ def verify(isbn): return _calculate_isbn_check_digit(isbn_main_part) == _get_isbn_check_digit(clear_isbn).upper() +def isbn_generator(isbn): + clear_isbn = _remove_non_alphanumeric(isbn) + if len(clear_isbn) < 9 or len(clear_isbn) > 10: + raise ValueError() + + isbn_main_part = clear_isbn[:9] + return _format_isbn(isbn_main_part + _calculate_isbn_check_digit(isbn_main_part)) + + def _remove_non_alphanumeric(value): return ''.join([x for x in str(value) if x.isalnum()]) @@ -25,3 +34,7 @@ def _get_isbn_check_digit(isbn_value): def _calculate_isbn_check_digit(isbn): isbn_sum = 11 - sum([int(x) * (10 - i) for i, x in enumerate(isbn)]) % 11 return 'X' if isbn_sum == 10 else str(isbn_sum) + + +def _format_isbn(isbn): + return '-'.join([isbn[:1], isbn[1:4], isbn[4:9], isbn[9:]]) diff --git a/exercises/isbn-verifier/isbn_verifier.py b/exercises/isbn-verifier/isbn_verifier.py index b00fcc6a45e..78649325678 100644 --- a/exercises/isbn-verifier/isbn_verifier.py +++ b/exercises/isbn-verifier/isbn_verifier.py @@ -1,2 +1,6 @@ def verify(isbn): pass + + +def isbn_generator(isbn): + pass diff --git a/exercises/isbn-verifier/isbn_verifier_test.py b/exercises/isbn-verifier/isbn_verifier_test.py index 38c6c40c5b6..02548ae98a5 100644 --- a/exercises/isbn-verifier/isbn_verifier_test.py +++ b/exercises/isbn-verifier/isbn_verifier_test.py @@ -1,6 +1,7 @@ import unittest -from isbn_verifier import verify +# from isbn_verifier import verify +from example import verify, isbn_generator # Tests adapted from `problem-specifications//canonical-data.json` @ v1.0.0 @@ -47,5 +48,25 @@ def test_invalid_check_digit_X_used_for_0(self): self.assertIs(verify('3-598-21515-X'), False) +class IsbnGeneratorTests(unittest.TestCase): + + def test_valid_isbn(self): + self.assertEqual(isbn_generator('3-598-21508-8'), '3-598-21508-8') + + def test_valid_isbn_without_separating_dashes_with_X_check_digit(self): + self.assertEqual(isbn_generator('359821507X'), '3-598-21507-X') + + def test_valid_isbn_without_check_digit(self): + self.assertEqual(isbn_generator('359821508'), '3-598-21508-8') + + def test_invalid_isbn_too_short(self): + with self.assertRaises(Exception): + isbn_generator('3-598-2350') + + def test_invalid_isbn_too_long(self): + with self.assertRaises(Exception): + isbn_generator('359723508XA') + + if __name__ == '__main__': unittest.main() From a722832add08163d261cedf785c323a63dfb1f5b Mon Sep 17 00:00:00 2001 From: Euler Campos Barros Date: Sun, 29 Oct 2017 15:50:25 -0200 Subject: [PATCH 5/5] isbn-verifier: ISBN-13 generator from ISBN-10 number --- config.json | 17 +-------- exercises/isbn-verifier/example.py | 36 +++++++++++-------- exercises/isbn-verifier/isbn_verifier.py | 4 +++ exercises/isbn-verifier/isbn_verifier_test.py | 22 ++++++++++-- 4 files changed, 47 insertions(+), 32 deletions(-) diff --git a/config.json b/config.json index 2b2b38c681a..dbf3e36d7a8 100644 --- a/config.json +++ b/config.json @@ -1128,22 +1128,7 @@ "difficulty": 3, "topics": [ "type_conversion", - "exception_handling" - "strings", - "arrays", - "integers", - "parsing" - ] - }, - { - "uuid": "7961c852-c87a-44b0-b152-efea3ac8555c", - "slug": "isbn-verifier", - "core": false, - "unlocked_by": null, - "difficulty": 3, - "topics": [ - "type_conversion", - "conditionals", + "exception_handling", "strings", "arrays", "integers", diff --git a/exercises/isbn-verifier/example.py b/exercises/isbn-verifier/example.py index dc1bda00437..aa23cb0c71d 100644 --- a/exercises/isbn-verifier/example.py +++ b/exercises/isbn-verifier/example.py @@ -3,11 +3,14 @@ def verify(isbn): if len(clear_isbn) != 10: return False - isbn_main_part = _get_isbn_main_part(clear_isbn) + isbn_main_part = clear_isbn[:-1] if not isbn_main_part.isdigit(): return False - return _calculate_isbn_check_digit(isbn_main_part) == _get_isbn_check_digit(clear_isbn).upper() + calculated_digit = _calculate_isbn10_check_digit(isbn_main_part) + check_digit = clear_isbn[-1:].upper() + + return calculated_digit == check_digit def isbn_generator(isbn): @@ -16,25 +19,30 @@ def isbn_generator(isbn): raise ValueError() isbn_main_part = clear_isbn[:9] - return _format_isbn(isbn_main_part + _calculate_isbn_check_digit(isbn_main_part)) - + isbn = isbn_main_part + _calculate_isbn10_check_digit(isbn_main_part) + return '-'.join([isbn[:1], isbn[1:4], isbn[4:9], isbn[9:]]) -def _remove_non_alphanumeric(value): - return ''.join([x for x in str(value) if x.isalnum()]) +def isbn13_generator_from_isbn10(isbn10): + clear_isbn = _remove_non_alphanumeric(isbn10) + if len(clear_isbn) != 10: + raise ValueError() -def _get_isbn_main_part(isbn_value): - return isbn_value[:-1] + isbn_main_part = '978' + clear_isbn[:9] + isbn = isbn_main_part + _calculate_isbn13_check_digit(isbn_main_part) + return '-'.join([isbn[:3], isbn[3:4], isbn[4:6], isbn[6:12], isbn[12:13]]) -def _get_isbn_check_digit(isbn_value): - return isbn_value[-1:] +def _remove_non_alphanumeric(value): + return ''.join([x for x in str(value) if x.isalnum()]) -def _calculate_isbn_check_digit(isbn): - isbn_sum = 11 - sum([int(x) * (10 - i) for i, x in enumerate(isbn)]) % 11 +def _calculate_isbn10_check_digit(isbn): + isbn_sum = sum([int(x) * (i + 1) for i, x in enumerate(isbn)]) % 11 return 'X' if isbn_sum == 10 else str(isbn_sum) -def _format_isbn(isbn): - return '-'.join([isbn[:1], isbn[1:4], isbn[4:9], isbn[9:]]) +def _calculate_isbn13_check_digit(isbn): + isbn_sum = sum([int(x) * (3 if i % 2 == 1 else 1) + for i, x in enumerate(isbn)]) % 10 + return str(isbn_sum) if isbn_sum == 0 else str(10 - isbn_sum) diff --git a/exercises/isbn-verifier/isbn_verifier.py b/exercises/isbn-verifier/isbn_verifier.py index 78649325678..5a7996d3cb1 100644 --- a/exercises/isbn-verifier/isbn_verifier.py +++ b/exercises/isbn-verifier/isbn_verifier.py @@ -4,3 +4,7 @@ def verify(isbn): def isbn_generator(isbn): pass + + +def isbn13_generator_from_isbn10(isbn10): + pass diff --git a/exercises/isbn-verifier/isbn_verifier_test.py b/exercises/isbn-verifier/isbn_verifier_test.py index 02548ae98a5..c6b0cd3ce15 100644 --- a/exercises/isbn-verifier/isbn_verifier_test.py +++ b/exercises/isbn-verifier/isbn_verifier_test.py @@ -1,7 +1,6 @@ import unittest -# from isbn_verifier import verify -from example import verify, isbn_generator +from isbn_verifier import verify, isbn_generator, isbn13_generator_from_isbn10 # Tests adapted from `problem-specifications//canonical-data.json` @ v1.0.0 @@ -68,5 +67,24 @@ def test_invalid_isbn_too_long(self): isbn_generator('359723508XA') +class Isbn13GeneratorTests(unittest.TestCase): + + def test_valid_isbn(self): + self.assertEqual(isbn13_generator_from_isbn10( + '3-598-21508-8'), '978-3-59-821508-7') + + def test_valid_isbn_without_separating_dashes_with_X_check_digit(self): + self.assertEqual(isbn13_generator_from_isbn10( + '359821507X'), '978-3-59-821507-0') + + def test_invalid_isbn_too_short(self): + with self.assertRaises(Exception): + isbn13_generator_from_isbn10('3-598-23506') + + def test_invalid_isbn_too_long(self): + with self.assertRaises(Exception): + isbn13_generator_from_isbn10('3597235089X') + + if __name__ == '__main__': unittest.main()