In my previous post Practicing Python, I started work on this exercise at Codementor. I got a decent solution working, but it still had a few deficiencies. Namely:
- It did not account for dashes in the ISBN
- Some of the error handling was not complete (namely making sure that the last digit was either numeric or an “x” character)
- The test code was also incomplete (did not test the last digit as “x” or a ISBN with dashes)
Because of this, I decided to spend a little bit of my practice time enhancing the code. I also combined all of my methods/functions into a class because in the future, I’d like to maybe build an application that can display and run my practice exercises. I considered splitting things into modules, but I preferred the cleaner file structure of a class as well as the opportunity to use some polymorphism later on to “process” these exercises.
I like to do test driven development, so the first thing I did was to compose and add some tests that would exercise these scenarios:
isbn = "155404295X" assert ISBNExercise.validate_isbn10(isbn) is True isbn = "1-55404-295-X" assert ISBNExercise.validate_isbn10(isbn) is True isbn = "1-55404-294-X" assert ISBNExercise.validate_isbn10(isbn) is False
The next thing I did was create a method to remove the dashes from the ISBN string. This is a nice “one liner” in Python using the str.replace
method:
def remove_dashes(code_string: str) -> str: return code_string.replace("-", "")
Then, I updated my main method to use this new remove_dashes
helper method as well as adding in the additional error handling:
def validate_isbn10(code_string: str) -> bool: # Remove dashes from the string isbn_string = ISBNExercise.remove_dashes(code_string) # Reject if string is wrong length, or is not numeric (including special case for digit 10) if len(isbn_string) != 10 or \ not isbn_string[0:9].isnumeric() or \ not (isbn_string[9].isnumeric() or isbn_string[9].lower() == "x"): return False checksum = 0 for i in range(0, 10): if i == 9 and isbn_string[i].lower() == "x": digit = 10 else: digit = int(isbn_string[i]) checksum += digit * (10 - i) return (checksum % 11) == 0
Now that I finished this, all of my tests are passing and I feel like my ISBN processor is much more robust. I think that tomorrow I will convert the test function into something that uses the Python unit test infrastructure instead.
If you’re interested, here is my complete class as it exists now:
class ISBNExercise: @staticmethod def remove_dashes(code_string: str) -> str: return code_string.replace("-", "") @staticmethod def validate_isbn10(code_string: str) -> bool: # Remove dashes from the string isbn_string = ISBNExercise.remove_dashes(code_string) # Reject if string is wrong length, or is not numeric (including special case for digit 10) if len(isbn_string) != 10 or \ not isbn_string[0:9].isnumeric() or \ not (isbn_string[9].isnumeric() or isbn_string[9].lower() == "x"): return False checksum = 0 for i in range(0, 10): if i == 9 and isbn_string[i].lower() == "x": digit = 10 else: digit = int(isbn_string[i]) checksum += digit * (10 - i) return (checksum % 11) == 0 @staticmethod def test_isbns(): isbn = "123" assert ISBNExercise.validate_isbn10(isbn) is False isbn = "0136091814" assert ISBNExercise.validate_isbn10(isbn) is True isbn = "1616550416" assert ISBNExercise.validate_isbn10(isbn) is False isbn = "0553418025" assert ISBNExercise.validate_isbn10(isbn) is True isbn = "3859574859" assert ISBNExercise.validate_isbn10(isbn) is False isbn = "155404295X" assert ISBNExercise.validate_isbn10(isbn) is True isbn = "1-55404-295-X" assert ISBNExercise.validate_isbn10(isbn) is True isbn = "1-55404-294-X" assert ISBNExercise.validate_isbn10(isbn) is False def run_exercise(self): self.test_isbns() print("ISBN Exercise - Complete!") if __name__ == '__main__': ex = ISBNExercise() ex.run_exercise()