diff --git a/config.json b/config.json index 5bd78add790..dbf3e36d7a8 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", @@ -1112,7 +1127,12 @@ "unlocked_by": null, "difficulty": 3, "topics": [ - "exception_handling" + "type_conversion", + "exception_handling", + "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..aa23cb0c71d --- /dev/null +++ b/exercises/isbn-verifier/example.py @@ -0,0 +1,48 @@ +def verify(isbn): + clear_isbn = _remove_non_alphanumeric(isbn) + if len(clear_isbn) != 10: + return False + + isbn_main_part = clear_isbn[:-1] + if not isbn_main_part.isdigit(): + return False + + calculated_digit = _calculate_isbn10_check_digit(isbn_main_part) + check_digit = clear_isbn[-1:].upper() + + return calculated_digit == check_digit + + +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] + isbn = isbn_main_part + _calculate_isbn10_check_digit(isbn_main_part) + return '-'.join([isbn[:1], isbn[1:4], isbn[4:9], isbn[9:]]) + + +def isbn13_generator_from_isbn10(isbn10): + clear_isbn = _remove_non_alphanumeric(isbn10) + if len(clear_isbn) != 10: + raise ValueError() + + 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 _remove_non_alphanumeric(value): + return ''.join([x for x in str(value) if x.isalnum()]) + + +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 _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 new file mode 100644 index 00000000000..5a7996d3cb1 --- /dev/null +++ b/exercises/isbn-verifier/isbn_verifier.py @@ -0,0 +1,10 @@ +def verify(isbn): + pass + + +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 new file mode 100644 index 00000000000..c6b0cd3ce15 --- /dev/null +++ b/exercises/isbn-verifier/isbn_verifier_test.py @@ -0,0 +1,90 @@ +import unittest + +from isbn_verifier import verify, isbn_generator, isbn13_generator_from_isbn10 + + +# 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) + + +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') + + +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()